From 60cacf62e84552ce818b39e369e346f53ed3e7ad Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:37:52 +0800 Subject: [PATCH 1/5] feat: integrate real Git effects into event pump --- scripts/run-m5-2-event-pump-gate.mjs | 8 + .../coordinator-driven-no-plan-scenario.mjs | 527 +++++++++++++++--- src/validation/m5-2-event-pump-codex-gate.mjs | 99 +++- test/m5-2-event-pump-codex-gate.test.mjs | 2 + 4 files changed, 542 insertions(+), 94 deletions(-) diff --git a/scripts/run-m5-2-event-pump-gate.mjs b/scripts/run-m5-2-event-pump-gate.mjs index 10a7a5e..70f03b1 100644 --- a/scripts/run-m5-2-event-pump-gate.mjs +++ b/scripts/run-m5-2-event-pump-gate.mjs @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { execFileSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; import process from "node:process"; @@ -95,6 +96,13 @@ try { } result = await runM52OperatorSuppliedCodexEventPumpGate({ artifactsDirectory, + sourceRoot: execFileSync("git", ["rev-parse", "--show-toplevel"], { + encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + }).trim(), + validatedBaseSha: execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + }).trim(), + temporaryParent: os.tmpdir(), command, signal: shutdownController.signal, ...(parsed.model ? { model: parsed.model } : {}), diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 31b5ff2..543986b 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -18,9 +18,12 @@ import { } from "./live-agent-scenario.mjs"; import { DeterministicNoPlanCodexAdapter } from "./deterministic-no-plan-codex-adapter.mjs"; +import { createBoundedGitLoopFixture } from "./bounded-git-loop-fixture.mjs"; import { + INDEPENDENT_GIT_VERIFIER_TEST, independentGitClaimDigest, independentGitFindingDigest, + startIndependentGitVerifierService, verifyIndependentGitVerification, } from "./independent-git-verifier.mjs"; @@ -35,6 +38,10 @@ const DEPENDENT_ADAPTER_RECEIPT = Object.freeze({ const owner = Object.freeze({ kind: "user", principalId: "owner_no_plan_scenario" }); const sha = (character) => character.repeat(40); const digest = (value) => sha256Digest({ value }); +const REAL_EFFECT_RESOURCE = "artifact.txt"; +const REAL_EFFECT_SEED = "SEED\n"; +const REAL_EFFECT_IMPLEMENTATION = "BAD_COUNTEREXAMPLE\n"; +const REAL_EFFECT_FIX = "FIXED\n"; function scenarioError(code) { const error = new Error(code); @@ -99,11 +106,19 @@ function exactDecisionTool(messageId) { } const TOOLS = Object.freeze({ + implementationCommit: tool( + "threadmesh_commit_candidate", + "Write and commit the exact bounded implementation or fix candidate, then return Git evidence.", + ), implementation: tool("threadmesh_publish_artifact", "Publish the bounded implementation."), reviewRead: tool( "threadmesh_review_read_artifact", "Inspect the exact admitted artifact before reporting a finding.", ), + reviewReproduce: tool( + "threadmesh_reproduce_review_finding", + "Report a finding discovered from the detached reviewer checkout.", + ), review: tool("threadmesh_report_review_finding", "Publish the exact review finding."), fixApply: tool( "threadmesh_apply_review_fix", @@ -398,7 +413,8 @@ function recordDependentAdapterReceipt( async function runKickoff({ coordinator, runtime, actor, ref, event, args, cwd, recoveryDirectory, - ownedJournalPaths, + ownedJournalPaths, businessTools = [TOOLS.implementation], + publicationOrdinal = 0, onBusinessToolCall = null, prompt = null, }) { const actorPrincipal = principal(actor); const executionId = "intent_no_plan_user_kickoff"; @@ -411,8 +427,8 @@ async function runKickoff({ eventId: "event_authenticated_user_kickoff", actor, adapterIdempotencyKey, - promptDigest: sha256Digest(`Implement and publish source ${event.messageId}.`), - allowedTools: [TOOLS.implementation.name], + promptDigest: sha256Digest(prompt ?? `Implement and publish source ${event.messageId}.`), + allowedTools: businessTools.map(({ name }) => name), }, 0, actorPrincipal); const filename = path.join(recoveryDirectory, `${executionId}.json`); ownedJournalPaths.add(filename); @@ -421,9 +437,9 @@ async function runKickoff({ phase: "user-kickoff", cwd, ref, - prompt: `Implement and publish source ${event.messageId}.`, + prompt: prompt ?? `Implement and publish source ${event.messageId}.`, scenarioId: "coordinator_driven_no_plan", - allowedToolNames: [TOOLS.implementation.name], + allowedToolNames: businessTools.map(({ name }) => name), turnRecovery: { filename, executionId, async onOutcomeUnknown() {}, async onTerminalReconciliation() {}, @@ -440,6 +456,9 @@ async function runKickoff({ ); }, beforeToolCall: async (selected) => { + if (selected?.tool !== businessTools[selected?.ordinal]?.name) { + throw scenarioError("threadmesh_user_kickoff_tool_sequence_mismatch"); + } execution = coordinator.recordModelSelectedTurnToolAction(executionId, { turnId: selected.turnId, callId: selected.callId, ordinal: selected.ordinal, name: selected.tool, arguments: selected.arguments, @@ -447,7 +466,11 @@ async function runKickoff({ expectedActionHeadDigest: execution.actionHeadDigest, }, actorPrincipal); }, - async onToolCall() { return { commitSha: args.commitSha, published: true }; }, + async onToolCall(selected) { + return onBusinessToolCall + ? onBusinessToolCall(selected) + : { commitSha: args.commitSha, published: true }; + }, afterToolCall: async (completed) => { execution = coordinator.completeModelSelectedTurnToolAction(executionId, { turnId: completed.turnId, callId: completed.callId, ordinal: completed.ordinal, @@ -467,9 +490,14 @@ async function runKickoff({ expectedRecordDigest: turn.recoveryJournal.recordDigest, }); coordinator.publishLifecycleFromCompletedAction(executionId, { + actionOrdinal: publicationOrdinal, expectedTool: TOOLS.implementation.name, event, - expectedMaterial: { commitSha: args.commitSha }, + expectedMaterial: { + commitSha: execution.actions[publicationOrdinal] === undefined + ? args.commitSha + : JSON.parse(execution.actions[publicationOrdinal].argsJson).commitSha, + }, }, actorPrincipal); return { execution, turn }; } @@ -501,6 +529,10 @@ export async function runCoordinatorDrivenNoPlanScenario({ artifactsDirectory, runtime: providedRuntime = null, signal = null, + realEffects = false, + sourceRoot = null, + validatedBaseSha = null, + temporaryParent = null, injectPriorRelevant = false, injectFinalizationFailure = false, injectPreverifiedTamper = null, @@ -518,6 +550,12 @@ export async function runCoordinatorDrivenNoPlanScenario({ typeof providedRuntime?.runAdmittedToolTurn !== "function" || typeof providedRuntime?.deleteRole !== "function" )) || + typeof realEffects !== "boolean" || + (realEffects && ( + !path.isAbsolute(sourceRoot ?? "") || + !/^[a-f0-9]{40}$/u.test(validatedBaseSha ?? "") || + !path.isAbsolute(temporaryParent ?? "") + )) || (signal !== null && ( typeof signal !== "object" || typeof signal.aborted !== "boolean" )) || @@ -539,15 +577,55 @@ export async function runCoordinatorDrivenNoPlanScenario({ fs.mkdirSync(journalDirectory, { recursive: false, mode: 0o700 }); const ownedJournalPaths = new Set(); const databasePath = path.join(scenarioRunRoot, "coordinator-driven.sqlite"); - const { publicKey, privateKey } = generateKeyPairSync("ed25519"); - const trustAnchor = { - keyId: "threadmesh://independent-git-verifier/key/ephemeral", - algorithm: "ed25519", - actorId: "threadmesh-independent-git-verifier", - trustDomain: "threadmesh://independent-git-verifier", - policyId: "threadmesh://independent-git-verifier/policy/1", - publicKeyPem: publicKey.export({ type: "spki", format: "pem" }), - }; + let gitFixture = null; + let verifierService = null; + let verifierServiceClosed = !realEffects; + let gitFixtureCleanup = Object.freeze({ complete: !realEffects }); + let privateKey = null; + let trustAnchor; + try { + if (realEffects) { + gitFixture = createBoundedGitLoopFixture({ + sourceRoot, + validatedBaseSha, + temporaryParent, + seedFiles: { [REAL_EFFECT_RESOURCE]: REAL_EFFECT_SEED }, + }); + verifierService = await startIndependentGitVerifierService(); + trustAnchor = verifierService.trustAnchor; + } else { + const signing = generateKeyPairSync("ed25519"); + privateKey = signing.privateKey; + trustAnchor = { + keyId: "threadmesh://independent-git-verifier/key/ephemeral", + algorithm: "ed25519", + actorId: "threadmesh-independent-git-verifier", + trustDomain: "threadmesh://independent-git-verifier", + policyId: "threadmesh://independent-git-verifier/policy/1", + publicKeyPem: signing.publicKey.export({ type: "spki", format: "pem" }), + }; + } + } catch (error) { + try { + if (verifierService) { + await verifierService.close(); + verifierServiceClosed = true; + } + } catch {} + gitFixtureCleanup = gitFixture?.cleanup() ?? gitFixtureCleanup; + try { fs.rmSync(scenarioRunRoot, { recursive: true }); } catch {} + error.cleanup = { + complete: verifierServiceClosed && gitFixtureCleanup.complete === true && + !fs.existsSync(scenarioRunRoot), + roles: [], + verifierServiceClosed, + gitFixture: gitFixtureCleanup, + runRootRemoved: !fs.existsSync(scenarioRunRoot), + coordinatorRemoved: !fs.existsSync(databasePath), + remainingJournalCount: 0, + }; + throw error; + } let coordinatorClockSequence = 0; const coordinator = new SqliteCoordinator({ filename: databasePath, @@ -572,19 +650,26 @@ export async function runCoordinatorDrivenNoPlanScenario({ vd: grant("no_plan_v_dependent", actors.v, actors.dependent), ai: grant("no_plan_a_irrelevant", actors.a, actors.irrelevant), }; - const implementationSha = sha("3"); - const fixSha = sha("5"); - const findingDigest = independentGitFindingDigest({ + let implementationSha = realEffects ? null : sha("3"); + let fixSha = realEffects ? null : sha("5"); + let finding = realEffects ? null : Object.freeze({ resourcePath: "artifact.txt", counterexample: "BAD_COUNTEREXAMPLE", }); + let findingDigest = finding === null ? null : independentGitFindingDigest(finding); + let implementationEvidence = null; + let fixEvidence = null; + let reviewerCheckout = null; + let verifierCheckout = null; const artifactEvent = lifecycleEvent({ eventType: "artifact-ready", messageId: "msg_no_plan_artifact_0001", sender: actors.a, target: actors.r, relationshipId: grants.ar.relationshipId, - content: `Candidate ${implementationSha} is ready for exact review.`, + content: realEffects + ? "A bounded Git candidate is ready for detached review." + : `Candidate ${implementationSha} is ready for exact review.`, }); const reviewEvent = lifecycleEvent({ eventType: "review-failed", @@ -600,7 +685,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ sender: actors.a, target: actors.v, relationshipId: grants.av.relationshipId, - content: `Review fix ${fixSha} is ready for independent verification.`, + content: realEffects + ? "A direct-descendant Git fix is ready for independent verification." + : `Review fix ${fixSha} is ready for independent verification.`, }); const verifiedEvent = { ...lifecycleEvent({ @@ -609,7 +696,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ sender: actors.v, target: actors.dependent, relationshipId: grants.vd.relationshipId, - content: "The exact signed evidence chain passed trusted fixture verification.", + content: realEffects + ? "The exact Git chain passed the preconfigured process-isolated verifier." + : "The exact signed evidence chain passed trusted fixture verification.", }), freshness: { expectedRunId: "run-no-plan-dependent", @@ -638,26 +727,92 @@ export async function runCoordinatorDrivenNoPlanScenario({ event: actionEventBody(artifactEvent), commitSha: implementationSha, }; + const copyShaTool = (base, staticArguments) => Object.freeze({ + ...base, + description: `${base.description} Copy the exact commitSha returned by the preceding commit tool.`, + inputSchema: Object.freeze({ + type: "object", + additionalProperties: false, + properties: Object.freeze({ + ...Object.fromEntries(Object.entries(staticArguments).map( + ([key, value]) => [key, Object.freeze({ const: value })], + )), + commitSha: Object.freeze({ type: "string", pattern: "^[a-f0-9]{40}$" }), + }), + required: Object.freeze([...Object.keys(staticArguments), "commitSha"]), + }), + }); + const realReviewReproduce = Object.freeze({ + ...TOOLS.reviewReproduce, + description: `${TOOLS.reviewReproduce.description} Use only the content returned by the preceding read tool; do not infer a finding from this schema.`, + inputSchema: Object.freeze({ + type: "object", additionalProperties: false, + properties: Object.freeze({ + sourceEventId: Object.freeze({ const: artifactEvent.messageId }), + resourcePath: Object.freeze({ type: "string", minLength: 1, maxLength: 200 }), + counterexample: Object.freeze({ type: "string", minLength: 1, maxLength: 256 }), + reason: Object.freeze({ type: "string", minLength: 1, maxLength: 1000 }), + }), + required: Object.freeze([ + "sourceEventId", "resourcePath", "counterexample", "reason", + ]), + }), + }); + const realReviewPublish = Object.freeze({ + ...TOOLS.review, + description: `${TOOLS.review.description} Copy the exact findingDigest returned by the preceding reproduction tool.`, + inputSchema: Object.freeze({ + type: "object", additionalProperties: false, + properties: Object.freeze({ + sourceEventId: Object.freeze({ const: artifactEvent.messageId }), + event: Object.freeze({ const: actionEventBody(reviewEvent) }), + findingDigest: Object.freeze({ type: "string", pattern: "^sha256:[a-f0-9]{64}$" }), + }), + required: Object.freeze(["sourceEventId", "event", "findingDigest"]), + }), + }); + const realCommitCandidate = Object.freeze({ + ...TOOLS.implementationCommit, + inputSchema: Object.freeze({ + type: "object", additionalProperties: false, + properties: Object.freeze({ + phase: Object.freeze({ enum: Object.freeze(["implementation", "fix"]) }), + content: Object.freeze({ + enum: Object.freeze([REAL_EFFECT_IMPLEMENTATION, REAL_EFFECT_FIX]), + }), + sourceEventId: Object.freeze({ type: "string", minLength: 1, maxLength: 512 }), + }), + required: Object.freeze(["phase", "content", "sourceEventId"]), + }), + }); const scenarioTools = Object.freeze({ - implementation: exactArgumentsTool(TOOLS.implementation, kickoffArgs), + implementationCommit: realEffects ? realCommitCandidate : null, + implementation: realEffects + ? copyShaTool(TOOLS.implementation, { + sourceEventId: artifactEvent.messageId, + event: actionEventBody(artifactEvent), + }) + : exactArgumentsTool(TOOLS.implementation, kickoffArgs), rDecision: exactDecisionTool(artifactEvent.messageId), reviewRead: exactArgumentsTool(TOOLS.reviewRead, { sourceEventId: artifactEvent.messageId, }), - review: exactArgumentsTool(TOOLS.review, { - sourceEventId: artifactEvent.messageId, - event: actionEventBody(reviewEvent), - findingDigest, + reviewReproduce: realEffects ? realReviewReproduce : null, + review: realEffects ? realReviewPublish : exactArgumentsTool(TOOLS.review, { + sourceEventId: artifactEvent.messageId, event: actionEventBody(reviewEvent), findingDigest, }), aDecision: exactDecisionTool(reviewEvent.messageId), - fixApply: exactArgumentsTool(TOOLS.fixApply, { - sourceEventId: reviewEvent.messageId, - }), - fix: exactArgumentsTool(TOOLS.fix, { - sourceEventId: reviewEvent.messageId, - event: actionEventBody(fixEvent), - commitSha: fixSha, - }), + fixApply: realEffects + ? realCommitCandidate + : exactArgumentsTool(TOOLS.fixApply, { sourceEventId: reviewEvent.messageId }), + fix: realEffects + ? copyShaTool(TOOLS.fix, { + sourceEventId: reviewEvent.messageId, + event: actionEventBody(fixEvent), + }) + : exactArgumentsTool(TOOLS.fix, { + sourceEventId: reviewEvent.messageId, event: actionEventBody(fixEvent), commitSha: fixSha, + }), vDecision: exactDecisionTool(fixEvent.messageId), verifyRead: exactArgumentsTool(TOOLS.verifyRead, { sourceEventId: fixEvent.messageId, @@ -689,7 +844,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); const routeHandlerConfigs = Object.freeze([ Object.freeze({ ...ROUTE_HANDLER_CONFIGS[0], businessTools: Object.freeze([ - scenarioTools.reviewRead, scenarioTools.review, + scenarioTools.reviewRead, + ...(realEffects ? [scenarioTools.reviewReproduce] : []), + scenarioTools.review, ]) }), Object.freeze({ ...ROUTE_HANDLER_CONFIGS[1], businessTools: Object.freeze([ scenarioTools.fixApply, scenarioTools.fix, @@ -716,6 +873,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ let tamperedReopenRejectionCode = null; const verifiedActivationOrder = []; const cleanupRoles = []; + const roleCwds = {}; try { throwIfShutdownRequested(signal); adapter = providedRuntime?.adapter ?? new DeterministicNoPlanCodexAdapter({ @@ -797,14 +955,19 @@ export async function runCoordinatorDrivenNoPlanScenario({ }, }); runtime = providedRuntime ?? new CodexLiveAgentRuntime({ command: "/fake/codex", adapter }); + roleCwds.a = realEffects ? gitFixture.implementerWorktree : artifactsDirectory; refs.a = await runtime.createRole({ - role: "a", cwd: artifactsDirectory, + role: "a", cwd: roleCwds.a, tools: [ + ...(realEffects ? [scenarioTools.implementationCommit] : []), scenarioTools.implementation, scenarioTools.aDecision, - scenarioTools.fixApply, scenarioTools.fix, + ...(!realEffects ? [scenarioTools.fixApply] : []), scenarioTools.fix, ], phaseTools: { - "user-kickoff": [scenarioTools.implementation], + "user-kickoff": [ + ...(realEffects ? [scenarioTools.implementationCommit] : []), + scenarioTools.implementation, + ], "receiver-decision": [scenarioTools.aDecision], "same-a-fix": [scenarioTools.fixApply, scenarioTools.fix], }, @@ -815,12 +978,19 @@ export async function runCoordinatorDrivenNoPlanScenario({ scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); + roleCwds.r = realEffects ? gitFixture.root : artifactsDirectory; refs.r = await runtime.createRole({ - role: "r", cwd: artifactsDirectory, - tools: [scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review], + role: "r", cwd: roleCwds.r, + tools: [ + scenarioTools.rDecision, scenarioTools.reviewRead, + ...(realEffects ? [scenarioTools.reviewReproduce] : []), scenarioTools.review, + ], phaseTools: { "receiver-decision": [scenarioTools.rDecision], - "r-review": [scenarioTools.reviewRead, scenarioTools.review], + "r-review": [ + scenarioTools.reviewRead, + ...(realEffects ? [scenarioTools.reviewReproduce] : []), scenarioTools.review, + ], }, protectedPhases: { "receiver-decision": "receiver-decision", "r-review": "admitted-tool", @@ -829,8 +999,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); + roleCwds.v = realEffects ? gitFixture.root : artifactsDirectory; refs.v = await runtime.createRole({ - role: "v", cwd: artifactsDirectory, + role: "v", cwd: roleCwds.v, tools: [scenarioTools.vDecision, scenarioTools.verifyRead, scenarioTools.verify], phaseTools: { "receiver-decision": [scenarioTools.vDecision], @@ -843,8 +1014,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); + roleCwds.dependent = realEffects ? gitFixture.root : artifactsDirectory; refs.dependent = await runtime.createRole({ - role: "dependent", cwd: artifactsDirectory, + role: "dependent", cwd: roleCwds.dependent, tools: [ scenarioTools.dependentDecision, scenarioTools.dependentCheck, scenarioTools.dependent, ], @@ -860,8 +1032,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); + roleCwds.irrelevant = realEffects ? gitFixture.root : artifactsDirectory; refs.irrelevant = await runtime.createRole({ - role: "irrelevant", cwd: artifactsDirectory, + role: "irrelevant", cwd: roleCwds.irrelevant, tools: [scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review], instructions: "Remain idle unless coordinator attention is relevant.", scenarioId: "coordinator_driven_no_plan", @@ -904,10 +1077,16 @@ export async function runCoordinatorDrivenNoPlanScenario({ }, owner); const requirement = coordinator.createGitEvidenceRequirement({ chainId: "chain_coordinator_driven_no_plan", - validatedBaseSha: sha("1"), - fixtureSeedSha: sha("2"), - fixtureDefinitionDigest: digest("fixture-definition"), - trustedTestBlobDigest: digest("trusted-test"), + validatedBaseSha: realEffects ? validatedBaseSha : sha("1"), + fixtureSeedSha: realEffects ? gitFixture.seedSha : sha("2"), + fixtureDefinitionDigest: realEffects + ? gitFixture.fixtureDefinitionDigest : digest("fixture-definition"), + trustedTestBlobDigest: realEffects + ? sha256Digest(fs.readFileSync( + path.join(sourceRoot, ...INDEPENDENT_GIT_VERIFIER_TEST.resourcePath.split("/")), + "utf8", + )) + : digest("trusted-test"), implementer: actors.a, reviewer: actors.r, verifier: actors.v, @@ -919,20 +1098,24 @@ export async function runCoordinatorDrivenNoPlanScenario({ const payloads = { implementation: { actor: actors.a, turnId: null, toolCallDigest: null, - commitSha: implementationSha, parentSha: sha("2"), treeSha: sha("4"), - diffDigest: digest("implementation-diff"), - testEvidenceDigest: digest("implementation-test"), + commitSha: implementationSha, + parentSha: realEffects ? null : sha("2"), + treeSha: realEffects ? null : sha("4"), + diffDigest: realEffects ? null : digest("implementation-diff"), + testEvidenceDigest: realEffects ? null : digest("implementation-test"), }, "review-failed": { actor: actors.r, turnId: null, toolCallDigest: null, implementationSha, findingDigest, - reproductionEvidenceDigest: digest("reproduction"), + reproductionEvidenceDigest: realEffects ? null : digest("reproduction"), }, fix: { actor: actors.a, turnId: null, toolCallDigest: null, - commitSha: fixSha, parentSha: implementationSha, treeSha: sha("6"), - diffDigest: digest("fix-diff"), resolvesFindingDigest: findingDigest, - testEvidenceDigest: digest("fix-test"), + commitSha: fixSha, parentSha: implementationSha, + treeSha: realEffects ? null : sha("6"), + diffDigest: realEffects ? null : digest("fix-diff"), + resolvesFindingDigest: findingDigest, + testEvidenceDigest: realEffects ? null : digest("fix-test"), }, }; if (injectPriorRelevant) { @@ -978,7 +1161,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.r, principal: principal(actors.r), role: "r", - cwd: artifactsDirectory, + cwd: realEffects ? path.join(gitFixture.root, "reviewer") : artifactsDirectory, ref: refs.r, routes: [{ handlerId: routeHandlerConfigs[0].handlerId, @@ -990,9 +1173,45 @@ export async function runCoordinatorDrivenNoPlanScenario({ now: NOW, businessPhase: "r-review", businessTools: routeHandlerConfigs[0].businessTools, - async onBusinessToolCall({ tool: selectedTool }) { + async onBusinessToolCall({ tool: selectedTool, arguments: value }) { if (selectedTool === TOOLS.reviewRead.name) { - return { artifactDigest: digest("admitted-review-artifact") }; + if (!realEffects) return { artifactDigest: digest("admitted-review-artifact") }; + const checkout = gitFixture.verifyReviewerCheckout({ implementationSha }); + return { + resourcePath: REAL_EFFECT_RESOURCE, + content: fs.readFileSync( + path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", + ), + commitSha: checkout.subjectSha, + }; + } + if (realEffects && selectedTool === TOOLS.reviewReproduce.name) { + const candidate = { + resourcePath: value?.resourcePath, + counterexample: value?.counterexample, + }; + const content = fs.readFileSync( + path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", + ); + if ( + candidate.resourcePath !== REAL_EFFECT_RESOURCE || + candidate.counterexample !== REAL_EFFECT_IMPLEMENTATION.trim() || + !content.includes(candidate.counterexample) || + typeof value?.reason !== "string" || value.reason.length < 1 + ) throw scenarioError("threadmesh_real_effect_review_finding_not_reproduced"); + finding = Object.freeze(candidate); + findingDigest = independentGitFindingDigest(finding); + payloads["review-failed"].findingDigest = findingDigest; + payloads["review-failed"].reproductionEvidenceDigest = sha256Digest({ + commitSha: implementationSha, + resourcePath: candidate.resourcePath, + contentDigest: sha256Digest(content), + reasonDigest: sha256Digest(value.reason), + }); + return { findingDigest, reproducible: true }; + } + if (realEffects && value?.findingDigest !== findingDigest) { + throw scenarioError("threadmesh_real_effect_review_publication_invalid"); } return { findingDigest, blocking: true, implementationSha }; }, @@ -1001,8 +1220,10 @@ export async function runCoordinatorDrivenNoPlanScenario({ const execution = coordinator.getTurnExecution( activation.businessExecutionId, principal(actors.r), ); - payloads["review-failed"].turnId = execution.actions[1].turnId; - payloads["review-failed"].toolCallDigest = execution.actions[1].actionDigest; + const publicationOrdinal = realEffects ? 2 : 1; + payloads["review-failed"].turnId = execution.actions[publicationOrdinal].turnId; + payloads["review-failed"].toolCallDigest = + execution.actions[publicationOrdinal].actionDigest; const promoted = promoteStage( coordinator, activation.businessExecutionId, "review-failed", payloads["review-failed"], evidenceRevision, evidenceHead, actors.r, @@ -1010,7 +1231,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ evidenceRevision = promoted.evidenceState.recordCount; evidenceHead = promoted.evidenceState.headDigest; coordinator.publishLifecycleFromCompletedAction(promoted.executionId, { - actionOrdinal: 1, + actionOrdinal: publicationOrdinal, expectedTool: TOOLS.review.name, event: reviewEvent, expectedMaterial: { findingDigest }, @@ -1023,7 +1244,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.a, principal: principal(actors.a), role: "a", - cwd: artifactsDirectory, + cwd: realEffects ? gitFixture.implementerWorktree : artifactsDirectory, ref: refs.a, routes: [{ handlerId: routeHandlerConfigs[1].handlerId, @@ -1035,10 +1256,29 @@ export async function runCoordinatorDrivenNoPlanScenario({ now: NOW, businessPhase: "same-a-fix", businessTools: routeHandlerConfigs[1].businessTools, - async onBusinessToolCall({ tool: selectedTool }) { - if (selectedTool === TOOLS.fixApply.name) { + async onBusinessToolCall({ tool: selectedTool, arguments: value }) { + if (selectedTool === (realEffects + ? TOOLS.implementationCommit.name : TOOLS.fixApply.name)) { + if (realEffects) { + if ( + value?.phase !== "fix" || value?.content !== REAL_EFFECT_FIX || + value?.sourceEventId !== reviewEvent.messageId || !implementationSha + ) { + throw scenarioError("threadmesh_real_effect_fix_invalid"); + } + gitFixture.writeImplementerFile( + REAL_EFFECT_RESOURCE, value.content, { expectedHead: implementationSha }, + ); + fixEvidence = gitFixture.commitFix({ expectedParent: implementationSha }); + fixSha = fixEvidence.subjectSha; + verifierCheckout = gitFixture.createVerifierCheckout({ fixSha }); + return fixEvidence; + } return { appliedFindingDigest: findingDigest }; } + if (realEffects && value?.commitSha !== fixSha) { + throw scenarioError("threadmesh_real_effect_fix_publication_invalid"); + } return { commitSha: fixSha, parentSha: implementationSha }; }, async onLifecyclePublication({ activation }) { @@ -1046,6 +1286,18 @@ export async function runCoordinatorDrivenNoPlanScenario({ const execution = coordinator.getTurnExecution( activation.businessExecutionId, principal(actors.a), ); + if (realEffects) { + Object.assign(payloads.fix, { + commitSha: fixEvidence.subjectSha, + parentSha: fixEvidence.parentSha, + treeSha: fixEvidence.treeSha, + diffDigest: fixEvidence.diffDigest, + resolvesFindingDigest: findingDigest, + testEvidenceDigest: sha256Digest({ + fixedResourceDigest: sha256Digest(REAL_EFFECT_FIX), + }), + }); + } payloads.fix.turnId = execution.actions[1].turnId; payloads.fix.toolCallDigest = execution.actions[1].actionDigest; const promoted = promoteStage( @@ -1068,7 +1320,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.v, principal: principal(actors.v), role: "v", - cwd: artifactsDirectory, + cwd: realEffects ? path.join(gitFixture.root, "verifier") : artifactsDirectory, ref: refs.v, routes: [{ handlerId: routeHandlerConfigs[2].handlerId, @@ -1085,10 +1337,46 @@ export async function runCoordinatorDrivenNoPlanScenario({ return { evidenceHead, evidenceRevision }; } verifiedActivationOrder.push("v-verification-tool-selected"); - verification = createVerification({ - requirement, payloads, verifier: actors.v, dependent: actors.dependent, - trustAnchor, privateKey, - }); + if (realEffects) { + gitFixture.verifyVerifierCheckout({ fixSha }); + const request = { + repoPath: gitFixture.bareRepository, + chain: { + chainId: requirement.chainId, + requirementDigest: requirement.requirementDigest, + validatedBaseSha: requirement.validatedBaseSha, + fixtureSeedSha: requirement.fixtureSeedSha, + fixtureDefinitionDigest: requirement.fixtureDefinitionDigest, + }, + implementation: { + sha: implementationEvidence.subjectSha, + treeSha: implementationEvidence.treeSha, + diffDigest: implementationEvidence.diffDigest, + }, + fix: { + sha: fixEvidence.subjectSha, + treeSha: fixEvidence.treeSha, + diffDigest: fixEvidence.diffDigest, + }, + finding: { ...finding, digest: findingDigest }, + trustedTest: { + resourcePath: INDEPENDENT_GIT_VERIFIER_TEST.resourcePath, + blobDigest: requirement.trustedTestBlobDigest, + }, + subject: { + messageId: verifiedEvent.messageId, + senderIncarnationId: actors.v.incarnationId, + receiver: taskRef(actors.dependent), + }, + }; + const response = await verifierService.verify(request); + verification = { request, response, expectedTrustAnchor: trustAnchor }; + } else { + verification = createVerification({ + requirement, payloads, verifier: actors.v, dependent: actors.dependent, + trustAnchor, privateKey, + }); + } return verification; }, async onLifecyclePublication({ activation }) { @@ -1279,12 +1567,65 @@ export async function runCoordinatorDrivenNoPlanScenario({ const kickoff = await runKickoff({ coordinator, runtime, actor: actors.a, ref: refs.a, event: artifactEvent, args: kickoffArgs, - cwd: artifactsDirectory, recoveryDirectory: journalDirectory, + cwd: realEffects ? gitFixture.implementerWorktree : artifactsDirectory, + recoveryDirectory: journalDirectory, ownedJournalPaths, + businessTools: realEffects + ? [scenarioTools.implementationCommit, scenarioTools.implementation] + : [scenarioTools.implementation], + publicationOrdinal: realEffects ? 1 : 0, + prompt: realEffects ? [ + `Implement and publish source ${artifactEvent.messageId}.`, + `First call ${TOOLS.implementationCommit.name} with phase=implementation,`, + `sourceEventId=${artifactEvent.messageId}, and the exact candidate content`, + JSON.stringify(REAL_EFFECT_IMPLEMENTATION), + `Then call ${TOOLS.implementation.name} and copy the returned subjectSha as commitSha.`, + ].join(" ") : null, + async onBusinessToolCall({ tool: selectedTool, arguments: value }) { + if (!realEffects) return { commitSha: implementationSha, published: true }; + if (selectedTool === TOOLS.implementationCommit.name) { + if ( + value?.phase !== "implementation" || + value?.content !== REAL_EFFECT_IMPLEMENTATION || + value?.sourceEventId !== artifactEvent.messageId + ) { + throw scenarioError("threadmesh_real_effect_implementation_invalid"); + } + gitFixture.writeImplementerFile( + REAL_EFFECT_RESOURCE, value.content, { expectedHead: gitFixture.seedSha }, + ); + implementationEvidence = gitFixture.commitImplementation({ + expectedParent: gitFixture.seedSha, + }); + implementationSha = implementationEvidence.subjectSha; + reviewerCheckout = gitFixture.createReviewerCheckout({ implementationSha }); + return implementationEvidence; + } + if (selectedTool === TOOLS.implementation.name && + value?.commitSha === implementationSha) { + return { commitSha: implementationSha, published: true }; + } + throw scenarioError("threadmesh_real_effect_implementation_publication_invalid"); + }, }); throwIfShutdownRequested(signal); - payloads.implementation.turnId = kickoff.execution.actions[0].turnId; - payloads.implementation.toolCallDigest = kickoff.execution.actions[0].actionDigest; + const kickoffPublicationOrdinal = realEffects ? 1 : 0; + if (realEffects) { + Object.assign(payloads.implementation, { + commitSha: implementationEvidence.subjectSha, + parentSha: implementationEvidence.parentSha, + treeSha: implementationEvidence.treeSha, + diffDigest: implementationEvidence.diffDigest, + testEvidenceDigest: sha256Digest({ + implementationResourceDigest: sha256Digest(REAL_EFFECT_IMPLEMENTATION), + }), + }); + payloads["review-failed"].implementationSha = implementationSha; + payloads.fix.parentSha = implementationSha; + } + payloads.implementation.turnId = kickoff.execution.actions[kickoffPublicationOrdinal].turnId; + payloads.implementation.toolCallDigest = + kickoff.execution.actions[kickoffPublicationOrdinal].actionDigest; const promotedKickoff = promoteStage( coordinator, kickoff.execution.executionId, "implementation", payloads.implementation, evidenceRevision, evidenceHead, actors.a, @@ -1596,7 +1937,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ const sessionRecords = Object.entries(refs).map(([role, ref]) => ({ role, refDigest: sha256Digest(ref), - worktreeDigest: sha256Digest({ cwd: artifactsDirectory }), + worktreeDigest: sha256Digest({ cwd: roleCwds[role] ?? artifactsDirectory }), })); const sessionManifest = { recordCount: sessionRecords.length, @@ -1671,9 +2012,13 @@ export async function runCoordinatorDrivenNoPlanScenario({ ]).size === 3, bindings: counts, verification: { - mode: "deterministic-in-process-trusted-signing", - externalIndependentVerifier: false, - signer: "fixture-owned-ephemeral-key", + mode: realEffects + ? "process-isolated-child-service-signed" + : "deterministic-in-process-trusted-signing", + externalIndependentVerifier: realEffects, + signer: realEffects + ? "process-isolated-child-owned-ephemeral-key" + : "fixture-owned-ephemeral-key", nativeVerifierSessionIndependent: refs.v.threadId !== refs.a.threadId && refs.v.threadId !== refs.r.threadId, nativeVerifierTurnIdDigest: sha256Digest( @@ -1688,6 +2033,15 @@ export async function runCoordinatorDrivenNoPlanScenario({ verifierActivation.businessExecutionId, principal(actors.v), ).actions[1].resultDigest === gitEvidenceVerificationResultDigest(verification), }, + gitEffects: { + realBoundedWorktrees: realEffects, + implementationSha: payloads.implementation.commitSha, + fixSha: payloads.fix.commitSha, + directDescendant: payloads.fix.parentSha === payloads.implementation.commitSha, + reviewerDetached: realEffects ? reviewerCheckout?.evidence.detached === true : false, + verifierDetached: realEffects ? verifierCheckout?.evidence.detached === true : false, + fixtureDefinitionDigest: requirement.fixtureDefinitionDigest, + }, evidenceChain: { recordCount: chain.state.recordCount, trustedComplete: chain.state.trustedComplete, @@ -1772,7 +2126,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ for (const [role, ref] of Object.entries(refs).reverse()) { try { cleanupRoles.push({ role, ...(await runtime.deleteRole({ - role, ref, cwd: artifactsDirectory, + role, ref, cwd: roleCwds[role] ?? artifactsDirectory, })) }); } catch (error) { cleanupRoles.push({ role, deleted: false, absenceVerified: false, error: error.code }); @@ -1792,6 +2146,20 @@ export async function runCoordinatorDrivenNoPlanScenario({ failure ??= error; } coordinator.close(); + if (verifierService) { + try { + const closed = await verifierService.close(); + verifierServiceClosed = closed?.closed === true && closed?.childExited === true; + } catch (error) { + failure ??= error; + } + } + if (gitFixture) { + gitFixtureCleanup = gitFixture.cleanup(); + if (gitFixtureCleanup.complete !== true && failure === undefined) { + failure = scenarioError("threadmesh_real_effect_git_cleanup_incomplete"); + } + } } const journalRemovalFailures = []; let ownedJournalRemovedCount = 0; @@ -1859,10 +2227,13 @@ export async function runCoordinatorDrivenNoPlanScenario({ const cleanup = { complete: cleanupRoles.length === Object.keys(refs).length && cleanupRoles.every(({ deleted, absenceVerified }) => deleted && absenceVerified) && + verifierServiceClosed && gitFixtureCleanup.complete === true && remainingOwnedJournals.length === 0 && unknownJournalPaths.length === 0 && journalRemovalFailures.length === 0 && databaseRemovalFailures.length === 0 && journalDirectoryRemoved && runRootRemoved && !fs.existsSync(databasePath), roles: cleanupRoles, + verifierServiceClosed, + gitFixture: gitFixtureCleanup, ownedJournalRemovedCount, remainingJournalCount: remainingOwnedJournals.length, unknownJournalCount: unknownJournalPaths.length, diff --git a/src/validation/m5-2-event-pump-codex-gate.mjs b/src/validation/m5-2-event-pump-codex-gate.mjs index 3c3cf9e..04731bd 100644 --- a/src/validation/m5-2-event-pump-codex-gate.mjs +++ b/src/validation/m5-2-event-pump-codex-gate.mjs @@ -12,6 +12,7 @@ const DIGEST = /^sha256:[a-f0-9]{64}$/u; const ADAPTER_OWNED_CODEX_USER_AGENT = /^threadmesh-codex-app-server-adapter\/[0-9]+\.[0-9]+\.[0-9]+ \(Mac OS [0-9]+(?:\.[0-9]+){1,3}; (?:arm64|x86_64)\) dumb \(threadmesh-codex-app-server-adapter; 0\.0\.0\)$/u; const BLOCKED_CODE = "threadmesh_m52_independent_verifier_service_pending"; +const REAL_EFFECTS_BLOCKED_CODE = "threadmesh_m52_trusted_codex_binary_provenance_pending"; const EXPECTED_PHASES = Object.freeze([ ["a", "user-kickoff", "kickoff", 1], ["r", "receiver-decision", "decision", 1], @@ -48,6 +49,31 @@ const EXPECTED_ACTION_SEQUENCES = Object.freeze([ Object.freeze(["threadmesh_decide_offer"]), EXPECTED_BUSINESS_SEQUENCES.dependent, ]); +const REAL_EFFECT_EXPECTED_PHASES = Object.freeze([ + ["a", "user-kickoff", "kickoff", 2], + ["r", "receiver-decision", "decision", 1], + ["r", "r-review", "admission", 3], + ["a", "receiver-decision", "decision", 1], + ["a", "same-a-fix", "admission", 2], + ["v", "receiver-decision", "decision", 1], + ["v", "v-verify", "admission", 2], + ["dependent", "receiver-decision", "decision", 1], + ["dependent", "dependent-gated-activation", "admission", 2], +]); +const REAL_EFFECT_EXPECTED_ACTION_SEQUENCES = Object.freeze([ + Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_artifact"]), + Object.freeze(["threadmesh_decide_offer"]), + Object.freeze([ + "threadmesh_review_read_artifact", "threadmesh_reproduce_review_finding", + "threadmesh_report_review_finding", + ]), + Object.freeze(["threadmesh_decide_offer"]), + Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_dependency"]), + Object.freeze(["threadmesh_decide_offer"]), + EXPECTED_BUSINESS_SEQUENCES.v, + Object.freeze(["threadmesh_decide_offer"]), + EXPECTED_BUSINESS_SEQUENCES.dependent, +]); const EXPECTED_HANDLERS = Object.freeze([ "handler.no-plan.review.v1", "handler.no-plan.same-a-fix.v1", @@ -84,7 +110,7 @@ const CORE_RESULT_KEYS = Object.freeze([ "completedRoles", "pendingRoles", "pendingReason", "pendingGates", "routeHandlerConfigs", "executedHandlerIds", "selectionBindings", "durableDispatchManifest", "nativeTurnManifest", "runnerTraceManifest", - "sessionManifest", "attention", "sameARef", "bindings", "verification", + "sessionManifest", "attention", "sameARef", "bindings", "verification", "gitEffects", "evidenceChain", "dependent", "ordering", "irrelevant", "runtime", "cleanup", ]); @@ -104,7 +130,7 @@ export function projectM52EventPumpFailureCleanup(value) { "complete", "roles", "ownedJournalRemovedCount", "remainingJournalCount", "unknownJournalCount", "unknownJournalPathDigests", "journalRemovalFailures", "databaseRemovalFailures", "journalDirectoryRemoved", "runRootRemoved", - "coordinatorRemoved", + "coordinatorRemoved", "verifierServiceClosed", "gitFixture", ]; const exactSchema = canonicalJson(Object.keys(source).sort()) === canonicalJson(expectedKeys.sort()); @@ -126,7 +152,8 @@ export function projectM52EventPumpFailureCleanup(value) { Array.isArray(source.databaseRemovalFailures) && source.databaseRemovalFailures.length === 0 && source.journalDirectoryRemoved === true && source.runRootRemoved === true && - source.coordinatorRemoved === true; + source.coordinatorRemoved === true && source.verifierServiceClosed === true && + source.gitFixture?.complete === true; return Object.freeze({ complete: closureComplete, rolesDeleted: roles.filter((role) => role?.deleted === true).length, @@ -167,7 +194,7 @@ function integer(value, label) { return value; } -function validateNativeTurnManifest(manifest) { +function validateNativeTurnManifest(manifest, { realEffects = false } = {}) { exactObject(manifest, ["scope", "recordCount", "records", "manifestDigest"], "nativeTurnManifest"); if (manifest.scope !== "sqlite-turn-receipt-and-binding-records" || @@ -183,7 +210,10 @@ function validateNativeTurnManifest(manifest) { "receiptDigest", "actionCount", "actionHeadDigest", "actions", "actionSequenceDigest", "executionState", "bindingDigest", "recordDigest", ], `nativeTurnManifest.records[${index}]`); - const [role, phase, bindingKind, actionCount] = EXPECTED_PHASES[index]; + const phases = realEffects ? REAL_EFFECT_EXPECTED_PHASES : EXPECTED_PHASES; + const sequences = realEffects + ? REAL_EFFECT_EXPECTED_ACTION_SEQUENCES : EXPECTED_ACTION_SEQUENCES; + const [role, phase, bindingKind, actionCount] = phases[index]; const body = { ...record }; delete body.recordDigest; if ( @@ -195,7 +225,7 @@ function validateNativeTurnManifest(manifest) { if (!Array.isArray(record.actions) || record.actions.length !== actionCount || sha256Digest(record.actions) !== record.actionSequenceDigest || canonicalJson(record.actions.map(({ tool }) => tool)) !== - canonicalJson(EXPECTED_ACTION_SEQUENCES[index])) { + canonicalJson(sequences[index])) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "nativeTurnActions"); } record.actions.forEach((action, ordinal) => { @@ -345,12 +375,19 @@ function projectGateResult(coreResult, { operatorSuppliedCodexShapedRuntime = false, } = {}) { exactObject(coreResult, CORE_RESULT_KEYS, "result"); + const gitEffects = exactObject(coreResult.gitEffects, [ + "realBoundedWorktrees", "implementationSha", "fixSha", "directDescendant", + "reviewerDetached", "verifierDetached", "fixtureDefinitionDigest", + ], "gitEffects"); + const realEffects = gitEffects.realBoundedWorktrees === true; if (coreResult.state !== "passed-full-functional-in-process-fixture" || coreResult.autonomousEventPump !== true || coreResult.autonomousEventPumpScope !== "in-process-functional-fixture") { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "coreState"); } - const nativeRecords = validateNativeTurnManifest(coreResult.nativeTurnManifest); + const nativeRecords = validateNativeTurnManifest( + coreResult.nativeTurnManifest, { realEffects }, + ); const dispatchRecords = validateDispatchManifest( coreResult.durableDispatchManifest, coreResult.selectionBindings, ); @@ -420,7 +457,7 @@ function projectGateResult(coreResult, { "complete", "roles", "ownedJournalRemovedCount", "remainingJournalCount", "unknownJournalCount", "unknownJournalPathDigests", "journalRemovalFailures", "databaseRemovalFailures", "journalDirectoryRemoved", "runRootRemoved", - "coordinatorRemoved", + "coordinatorRemoved", "verifierServiceClosed", "gitFixture", ], "cleanup"); if (!Array.isArray(coreResult.cleanup.roles)) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "cleanup.roles"); @@ -447,6 +484,8 @@ function projectGateResult(coreResult, { coreResult.cleanup.journalDirectoryRemoved !== true || coreResult.cleanup.runRootRemoved !== true || coreResult.cleanup.coordinatorRemoved !== true || + coreResult.cleanup.verifierServiceClosed !== true || + coreResult.cleanup.gitFixture?.complete !== true || coreResult.cleanup.ownedJournalRemovedCount !== 0 || coreResult.cleanup.remainingJournalCount !== 0) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "cleanupClosure"); @@ -493,7 +532,7 @@ function projectGateResult(coreResult, { coreResult.bindings?.lifecycleActionPublications !== 4 || coreResult.bindings?.receiverDecisions !== 4 || coreResult.bindings?.contextAdmissions !== 4 || - coreResult.runtime?.modelSelectedToolCalls !== 13 + coreResult.runtime?.modelSelectedToolCalls !== (realEffects ? 15 : 13) ) throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "exactBindings"); const expectedOrder = [ "v-verification-tool-selected", "verified-event-durable", @@ -506,9 +545,13 @@ function projectGateResult(coreResult, { coreResult.dependent?.effectCommittedAfterFinalization !== true) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "finalizationOrder"); } - if (coreResult.verification?.externalIndependentVerifier !== false || - coreResult.verification?.mode !== "deterministic-in-process-trusted-signing" || - coreResult.verification?.signer !== "fixture-owned-ephemeral-key" || + if (coreResult.verification?.externalIndependentVerifier !== realEffects || + coreResult.verification?.mode !== (realEffects + ? "process-isolated-child-service-signed" + : "deterministic-in-process-trusted-signing") || + coreResult.verification?.signer !== (realEffects + ? "process-isolated-child-owned-ephemeral-key" + : "fixture-owned-ephemeral-key") || coreResult.verification?.nativeVerifierSessionIndependent !== true || coreResult.verification?.signatureVerified !== true || coreResult.verification?.resultDigestBound !== true || @@ -529,6 +572,16 @@ function projectGateResult(coreResult, { ) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "honestyBoundary"); } + if ( + gitEffects.directDescendant !== true || + (realEffects && ( + !/^[a-f0-9]{40}$/u.test(gitEffects.implementationSha ?? "") || + !/^[a-f0-9]{40}$/u.test(gitEffects.fixSha ?? "") || + gitEffects.implementationSha === gitEffects.fixSha || + gitEffects.reviewerDetached !== true || gitEffects.verifierDetached !== true + )) + ) throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "gitEffects"); + digest(gitEffects.fixtureDefinitionDigest, "gitEffects.fixtureDefinitionDigest"); digest(coreResult.verification.nativeVerifierTurnIdDigest, "verification.nativeVerifierTurnIdDigest"); digest(coreResult.verification.trustAnchorDigest, "verification.trustAnchorDigest"); @@ -560,8 +613,9 @@ function projectGateResult(coreResult, { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "productProbe"); } const remainingGates = [ - "independent-verifier-service", - "real-bounded-git-worktree-effects", + ...(realEffects ? [] : [ + "independent-verifier-service", "real-bounded-git-worktree-effects", + ]), ...(operatorSuppliedCodexShapedRuntime ? ["trusted-codex-binary-provenance"] : ["real-codex-product-run"]), @@ -573,7 +627,7 @@ function projectGateResult(coreResult, { return Object.freeze({ schemaVersion: 1, state: "blocked", - code: BLOCKED_CODE, + code: realEffects ? REAL_EFFECTS_BLOCKED_CODE : BLOCKED_CODE, product, evidenceClass: deterministic ? "deterministic-event-pump-codex-gate" @@ -600,7 +654,9 @@ function projectGateResult(coreResult, { distinctReceiverRoles: true, dependentStartedAfterFinalization: true, irrelevantNativeTurns: 0, - verificationMode: "fixture-owned-ephemeral-key-not-independent", + verificationMode: realEffects + ? "process-isolated-child-service-signed" + : "fixture-owned-ephemeral-key-not-independent", remainingGates, evidence: Object.freeze({ nativeTurnManifest: coreResult.nativeTurnManifest, @@ -616,6 +672,8 @@ function projectGateResult(coreResult, { ).length, coordinatorRemoved: coreResult.cleanup.coordinatorRemoved, remainingJournalCount: coreResult.cleanup.remainingJournalCount, + verifierServiceClosed: coreResult.cleanup.verifierServiceClosed, + gitResourcesRemoved: coreResult.cleanup.gitFixture.complete === true, }), }); } @@ -698,6 +756,9 @@ export async function runM52EventPumpCodexGate({ export async function runM52OperatorSuppliedCodexEventPumpGate({ artifactsDirectory, + sourceRoot, + validatedBaseSha, + temporaryParent, command, args, env, @@ -705,6 +766,8 @@ export async function runM52OperatorSuppliedCodexEventPumpGate({ signal = null, } = {}) { if (!path.isAbsolute(artifactsDirectory ?? "") || !path.isAbsolute(command ?? "") || + !path.isAbsolute(sourceRoot ?? "") || !path.isAbsolute(temporaryParent ?? "") || + !/^[a-f0-9]{40}$/u.test(validatedBaseSha ?? "") || (signal !== null && ( typeof signal !== "object" || typeof signal.aborted !== "boolean" ))) { @@ -725,6 +788,10 @@ export async function runM52OperatorSuppliedCodexEventPumpGate({ artifactsDirectory, runtime, signal, + realEffects: true, + sourceRoot, + validatedBaseSha, + temporaryParent, }); try { return projectM52OperatorSuppliedCodexEventPumpGateResult(coreResult, { probe }); diff --git a/test/m5-2-event-pump-codex-gate.test.mjs b/test/m5-2-event-pump-codex-gate.test.mjs index b701445..d80ce35 100644 --- a/test/m5-2-event-pump-codex-gate.test.mjs +++ b/test/m5-2-event-pump-codex-gate.test.mjs @@ -362,6 +362,8 @@ test("failure cleanup projection is bounded and omits raw role and path data", ( journalDirectoryRemoved: true, runRootRemoved: true, coordinatorRemoved: true, + verifierServiceClosed: true, + gitFixture: { complete: true }, }; assert.equal(projectM52EventPumpFailureCleanup(fullCleanup).complete, true); assert.equal(projectM52EventPumpFailureCleanup({ complete: true }).complete, false); From 0c546163372eddd37e2d5b105c9e1e28f282e9d5 Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:58:08 +0800 Subject: [PATCH 2/5] fix: make real reviewer turn deterministic --- .../coordinator-driven-no-plan-scenario.mjs | 4 +++- src/validation/live-agent-scenario.mjs | 8 ++++++- test/product-turn-primitives.test.mjs | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 543986b..90a192a 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -995,7 +995,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ protectedPhases: { "receiver-decision": "receiver-decision", "r-review": "admitted-tool", }, - instructions: "Review only coordinator-admitted context.", + instructions: realEffects + ? "Review only coordinator-admitted context. In the admitted review turn, call every offered tool exactly once and in order: read the artifact, reproduce a finding using only the returned content, then publish by copying the returned findingDigest. Do not stop after an intermediate tool result." + : "Review only coordinator-admitted context.", scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); diff --git a/src/validation/live-agent-scenario.mjs b/src/validation/live-agent-scenario.mjs index 749e6df..1b52c3b 100644 --- a/src/validation/live-agent-scenario.mjs +++ b/src/validation/live-agent-scenario.mjs @@ -1527,7 +1527,9 @@ export class CodexLiveAgentRuntime { }, }], }; - const reconcile = async ({ baseline, journalProjection, startedTurnId = null }) => { + const reconcile = async ({ + baseline, journalProjection, startedTurnId = null, originCode = null, + }) => { await turnRecovery.onOutcomeUnknown({ prepared, adapterIdempotencyKey, @@ -1547,6 +1549,9 @@ export class CodexLiveAgentRuntime { reasonCode, ); ambiguous.recovery = { state: "ambiguous", reasonCode, journal: journalProjection }; + if (typeof originCode === "string" && originCode.length > 0) { + ambiguous.originCode = originCode; + } throw ambiguous; } await turnRecovery.onTerminalReconciliation({ @@ -1725,6 +1730,7 @@ export class CodexLiveAgentRuntime { baseline, journalProjection, startedTurnId: started?.turnId ?? null, + originCode: typeof error?.code === "string" ? error.code : error?.name ?? null, }); } } diff --git a/test/product-turn-primitives.test.mjs b/test/product-turn-primitives.test.mjs index 5dd54ff..497f2cd 100644 --- a/test/product-turn-primitives.test.mjs +++ b/test/product-turn-primitives.test.mjs @@ -885,3 +885,24 @@ test("admitted terminal failure leaves journal and an existing journal reconcile assert.equal(fake.state.nativeStarts, 1); assert.equal(fs.existsSync(value.filename), true); }); + +test("admitted ambiguous reconciliation retains only the bounded origin code", async (t) => { + const fake = adapter({ mode: "terminal", recoveryStatus: "completed" }); + const value = await fixture(t, fake, "admitted-ambiguous-origin"); + const admission = prepared(); + await assert.rejects( + () => value.runtime.runAdmittedToolTurn({ + role: "r", phase: "review", cwd: "/private/reviewer", ref: value.ref, + prepared: admission, admissionBinding: createAdmittedTurnBinding(admission), + scenarioId: value.scenarioId, allowedToolNames: [BUSINESS_TOOL.name], + turnRecovery: recovery(value.filename, "admitted-ambiguous-origin"), + async onToolCall() { return {}; }, async onAdmissionReceipt() { return {}; }, + }), + (error) => error?.code === "threadmesh_codex_live_context_reconciliation_ambiguous" && + error?.originCode === "codex_app_server_exited" && + error?.recovery?.reasonCode === "codex-native-turn-completed-observation-only" && + !Object.hasOwn(error, "cause"), + ); + assert.equal(fake.state.nativeStarts, 1); + assert.equal(fs.existsSync(value.filename), true); +}); From 1845d86dcb2322a642fa88f713b04386c3e5852f Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:17:31 +0800 Subject: [PATCH 3/5] fix: bound reviewer effect to two actions --- .../coordinator-driven-no-plan-scenario.mjs | 41 ++++++++----------- src/validation/m5-2-event-pump-codex-gate.mjs | 7 ++-- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 90a192a..1816d47 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -744,18 +744,13 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); const realReviewReproduce = Object.freeze({ ...TOOLS.reviewReproduce, - description: `${TOOLS.reviewReproduce.description} Use only the content returned by the preceding read tool; do not infer a finding from this schema.`, + description: `${TOOLS.reviewReproduce.description} Inspect the exact detached reviewer checkout and return independently reproduced finding evidence.`, inputSchema: Object.freeze({ type: "object", additionalProperties: false, properties: Object.freeze({ sourceEventId: Object.freeze({ const: artifactEvent.messageId }), - resourcePath: Object.freeze({ type: "string", minLength: 1, maxLength: 200 }), - counterexample: Object.freeze({ type: "string", minLength: 1, maxLength: 256 }), - reason: Object.freeze({ type: "string", minLength: 1, maxLength: 1000 }), }), - required: Object.freeze([ - "sourceEventId", "resourcePath", "counterexample", "reason", - ]), + required: Object.freeze(["sourceEventId"]), }), }); const realReviewPublish = Object.freeze({ @@ -844,8 +839,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); const routeHandlerConfigs = Object.freeze([ Object.freeze({ ...ROUTE_HANDLER_CONFIGS[0], businessTools: Object.freeze([ - scenarioTools.reviewRead, - ...(realEffects ? [scenarioTools.reviewReproduce] : []), + ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), scenarioTools.review, ]) }), Object.freeze({ ...ROUTE_HANDLER_CONFIGS[1], businessTools: Object.freeze([ @@ -982,21 +976,22 @@ export async function runCoordinatorDrivenNoPlanScenario({ refs.r = await runtime.createRole({ role: "r", cwd: roleCwds.r, tools: [ - scenarioTools.rDecision, scenarioTools.reviewRead, - ...(realEffects ? [scenarioTools.reviewReproduce] : []), scenarioTools.review, + scenarioTools.rDecision, + ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), + scenarioTools.review, ], phaseTools: { "receiver-decision": [scenarioTools.rDecision], "r-review": [ - scenarioTools.reviewRead, - ...(realEffects ? [scenarioTools.reviewReproduce] : []), scenarioTools.review, + ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), + scenarioTools.review, ], }, protectedPhases: { "receiver-decision": "receiver-decision", "r-review": "admitted-tool", }, instructions: realEffects - ? "Review only coordinator-admitted context. In the admitted review turn, call every offered tool exactly once and in order: read the artifact, reproduce a finding using only the returned content, then publish by copying the returned findingDigest. Do not stop after an intermediate tool result." + ? "Review only coordinator-admitted context. In the admitted review turn, call every offered tool exactly once and in order: reproduce the finding from the detached checkout, then publish by copying the returned findingDigest. Do not stop after the first tool result." : "Review only coordinator-admitted context.", scenarioId: "coordinator_driven_no_plan", }); @@ -1188,18 +1183,18 @@ export async function runCoordinatorDrivenNoPlanScenario({ }; } if (realEffects && selectedTool === TOOLS.reviewReproduce.name) { - const candidate = { - resourcePath: value?.resourcePath, - counterexample: value?.counterexample, - }; + const checkout = gitFixture.verifyReviewerCheckout({ implementationSha }); const content = fs.readFileSync( path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", ); + const candidate = { + resourcePath: REAL_EFFECT_RESOURCE, + counterexample: REAL_EFFECT_IMPLEMENTATION.trim(), + }; if ( - candidate.resourcePath !== REAL_EFFECT_RESOURCE || - candidate.counterexample !== REAL_EFFECT_IMPLEMENTATION.trim() || + value?.sourceEventId !== artifactEvent.messageId || !content.includes(candidate.counterexample) || - typeof value?.reason !== "string" || value.reason.length < 1 + checkout.subjectSha !== implementationSha ) throw scenarioError("threadmesh_real_effect_review_finding_not_reproduced"); finding = Object.freeze(candidate); findingDigest = independentGitFindingDigest(finding); @@ -1208,7 +1203,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ commitSha: implementationSha, resourcePath: candidate.resourcePath, contentDigest: sha256Digest(content), - reasonDigest: sha256Digest(value.reason), + findingDigest, }); return { findingDigest, reproducible: true }; } @@ -1222,7 +1217,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ const execution = coordinator.getTurnExecution( activation.businessExecutionId, principal(actors.r), ); - const publicationOrdinal = realEffects ? 2 : 1; + const publicationOrdinal = 1; payloads["review-failed"].turnId = execution.actions[publicationOrdinal].turnId; payloads["review-failed"].toolCallDigest = execution.actions[publicationOrdinal].actionDigest; diff --git a/src/validation/m5-2-event-pump-codex-gate.mjs b/src/validation/m5-2-event-pump-codex-gate.mjs index 04731bd..b2db36e 100644 --- a/src/validation/m5-2-event-pump-codex-gate.mjs +++ b/src/validation/m5-2-event-pump-codex-gate.mjs @@ -52,7 +52,7 @@ const EXPECTED_ACTION_SEQUENCES = Object.freeze([ const REAL_EFFECT_EXPECTED_PHASES = Object.freeze([ ["a", "user-kickoff", "kickoff", 2], ["r", "receiver-decision", "decision", 1], - ["r", "r-review", "admission", 3], + ["r", "r-review", "admission", 2], ["a", "receiver-decision", "decision", 1], ["a", "same-a-fix", "admission", 2], ["v", "receiver-decision", "decision", 1], @@ -64,8 +64,7 @@ const REAL_EFFECT_EXPECTED_ACTION_SEQUENCES = Object.freeze([ Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_artifact"]), Object.freeze(["threadmesh_decide_offer"]), Object.freeze([ - "threadmesh_review_read_artifact", "threadmesh_reproduce_review_finding", - "threadmesh_report_review_finding", + "threadmesh_reproduce_review_finding", "threadmesh_report_review_finding", ]), Object.freeze(["threadmesh_decide_offer"]), Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_dependency"]), @@ -532,7 +531,7 @@ function projectGateResult(coreResult, { coreResult.bindings?.lifecycleActionPublications !== 4 || coreResult.bindings?.receiverDecisions !== 4 || coreResult.bindings?.contextAdmissions !== 4 || - coreResult.runtime?.modelSelectedToolCalls !== (realEffects ? 15 : 13) + coreResult.runtime?.modelSelectedToolCalls !== (realEffects ? 14 : 13) ) throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "exactBindings"); const expectedOrder = [ "v-verification-tool-selected", "verified-event-durable", From c3df4bec07808cb291c8b4d892f5d0b42c28088e Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:18:18 +0800 Subject: [PATCH 4/5] test: close real-effects integration review gaps --- README.md | 13 +- ROADMAP.md | 24 ++- docs/06-guides/m5-2-live-agent-scenario.md | 18 +- ...5-2-real-codex-event-pump-attempt-audit.md | 39 ++-- ...-01-m5-2-real-codex-event-pump-behavior.md | 15 ++ ...026-09-01-m5-2-real-effects-integration.md | 98 ++++++++++ docs/10-planning/mainline-plan.md | 23 +-- docs/10-planning/project-status.md | 14 +- scripts/run-m5-2-event-pump-gate.mjs | 7 +- src/coordinator/sqlite-coordinator.mjs | 17 +- .../coordinator-driven-no-plan-scenario.mjs | 185 +++++++++++++----- .../deterministic-no-plan-codex-adapter.mjs | 26 ++- src/validation/live-agent-scenario.mjs | 12 +- src/validation/m5-2-event-pump-codex-gate.mjs | 17 +- ...ordinator-driven-no-plan-scenario.test.mjs | 140 +++++++++++++ test/m5-2-event-pump-codex-gate.test.mjs | 2 + test/product-turn-primitives.test.mjs | 24 +++ 17 files changed, 556 insertions(+), 118 deletions(-) create mode 100644 docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md diff --git a/README.md b/README.md index d4a7a29..9d68c1a 100644 --- a/README.md +++ b/README.md @@ -109,14 +109,17 @@ phase prompts or direct activations; the irrelevant session ran zero turns; five of five temporary sessions and all coordinator artifacts were removed. The completed result is deliberately classified `state=blocked` and -`liveProductEvidence=false`: verifier custody and Git effects are still -fixture-owned or simulated. This proves bounded real session initiative, not -M5.2 or production closure. The next checkpoint reuses the existing Git and -verifier foundations in this correlated path, then measures against a manual -relay/polling baseline. Non-mainline expansion remains frozen. +`liveProductEvidence=false`: that retained run used fixture-owned or simulated +Git and verification effects. The next branch now wires the existing bounded +Git worktrees and process-isolated child verifier into the same correlated +path. Its live rerun is pending after a reproducible local DNS/TLS failure; it +has not been upgraded into product evidence. After that rerun, the remaining +checkpoint is the manual relay/polling baseline and minimum critical +negative/restart closure. Non-mainline expansion remains frozen. [Read the exact fixture evidence](docs/09-reviews/2026-09-01-m5-2-autonomous-fixture.md) · [Read the real Codex behavior](docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md) · +[Read the real-effects checkpoint](docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md) · [Read the M5.2 scenario guide](docs/06-guides/m5-2-live-agent-scenario.md) ## Quickstart diff --git a/ROADMAP.md b/ROADMAP.md index c02dc26..1d8472d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -150,13 +150,13 @@ nine bound native turns, zero later runner prompts or direct activations, an irrelevant zero-turn control, and exact cleanup. This exposed an execution-order imbalance rather than a change in product -direction. The behavioral checkpoint is now passed. The immediate checkpoint -is to reuse the existing bounded Git-worktree and verifier foundations inside -that correlated live path, then add the manual baseline and minimum critical -negative/restart evidence. New substrate, generalized recovery, cross-harness, -or presentation work remains frozen. The completed run correctly remains -`blocked` and `liveProductEvidence=false` while verifier custody and Git effects -are simulated; it demonstrates behavior, not M5.2 closure. +direction. The behavioral checkpoint is passed. The existing bounded +Git-worktree and child-verifier foundations are now wired into that correlated +path, but a successful live rerun is pending after a reproducible local DNS/TLS +failure. The manual baseline and minimum critical negative/restart evidence +remain next. New substrate, generalized recovery, cross-harness, or +presentation work remains frozen. No partial integration attempt is promoted +to M5.2 evidence. - [x] Ship a one-command local demo with generated identities, grants, example sessions, and an inspector @@ -181,9 +181,15 @@ are simulated; it demonstrates behavior, not M5.2 closure. prompts or direct activations, exact real session/turn/dispatch bindings, dependent ordering, an irrelevant zero-turn control, and exact cleanup. Simulated verifier and Git effects remain explicitly labeled. + - [x] Reuse the existing bounded Git topology and process-isolated child + verifier in the correlated event-pump implementation, with exact cleanup + and no new coordinator or verifier subsystem. + - [ ] Retain one successful live Codex traversal of that real-effects path; + current reruns are blocked by a reproducible local DNS/TLS endpoint error. - [ ] M5.2 closure: reuse the existing bounded Git and verifier foundations - in the same correlated run, add the manual baseline and minimum critical - negative/restart evidence, and keep raw product data out of public output. + in one successful correlated run, add the manual baseline and minimum + critical negative/restart evidence, and keep raw product data out of public + output. - [ ] M5.3: pass three fresh relevant runs plus the manual baseline, irrelevant, stale/unverified, restart, and cleanup matrix. - [ ] Repeat the loop across Codex and one ACP-compatible harness diff --git a/docs/06-guides/m5-2-live-agent-scenario.md b/docs/06-guides/m5-2-live-agent-scenario.md index ba84d4b..fa61efe 100644 --- a/docs/06-guides/m5-2-live-agent-scenario.md +++ b/docs/06-guides/m5-2-live-agent-scenario.md @@ -184,7 +184,8 @@ export THREADMESH_CODEX_COMMAND=/absolute/path/to/codex node scripts/run-m5-2-event-pump-gate.mjs --mode live --artifacts-dir /fresh/owned/directory ``` -Six bounded attempts are retained: +Ten bounded attempts are retained. Attempts 1–6 cover the behavioral path; +attempts 7–10 exercise the real Git/child-verifier integration: | Attempt | Stop | Chain evidence | Cleanup evidence | |---|---|---|---| @@ -194,6 +195,10 @@ Six bounded attempts are retained: | 4 | Exact lifecycle publication action mismatch after kickoff | Five tasks and one kickoff intent; no pump dispatch | Normal five-of-five session and temporary-resource cleanup | | 5 | Ambiguous reconciliation during the admitted same-A fix turn | Real chain through reviewer review and same-A acceptance | Normal five-of-five session and temporary-resource cleanup | | 6 | Completed with expected `threadmesh_m52_independent_verifier_service_pending` product-gate classification | Full real `A -> R -> same-A -> V -> dependent`; one kickoff, nine bound turns, zero later runner prompts/direct activations, irrelevant zero turns | Normal five-of-five session cleanup, coordinator removal, zero journals, and removal of the exact empty artifacts directory | +| 7 | R admitted turn ended before a tool selection | Real A implementation commit and autonomous R route | Complete role/verifier/Git/coordinator cleanup | +| 8 | R admitted turn ended after its detached-checkout read | Real A implementation plus one R read action | Complete role/verifier/Git/coordinator cleanup | +| 9 | R decision turn was terminally reconciled | Real A implementation and R route selection | Complete role/verifier/Git/coordinator cleanup | +| 10 | R admitted turn became ambiguous during a reproduced DNS/TLS endpoint failure | Real A implementation, R acceptance, and admission start | Complete role/verifier/Git/coordinator cleanup | [#126](https://github.com/fyaic/threadmesh/pull/126) and [#127](https://github.com/fyaic/threadmesh/pull/127) fixed the first two observed @@ -203,11 +208,12 @@ extended only protected admitted business turns to 300 seconds. Attempt 6 is a completed `state=blocked` gate result and the first real autonomous behavioral chain. -The next checkpoint is no longer another behavioral rerun. Reuse the existing -bounded Git-worktree and verifier foundations inside this correlated event-pump -path, then add the manual relay/polling baseline and minimum critical -negative/restart evidence. Until those gates pass, the correct public result -remains `state=blocked` and `liveProductEvidence=false`. +The bounded Git-worktree and process-isolated child-verifier foundations are +now wired into this correlated path. The next checkpoint is one successful +live rerun after `codex doctor` no longer reports the current WebSocket +certificate failure, then the manual relay/polling baseline and minimum +critical negative/restart evidence. Until those gates pass, the correct public +result remains `state=blocked` and `liveProductEvidence=false`. See the [bounded attempt audit](../09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md) and [real behavior record](../09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md). diff --git a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md index 2dc45c5..8897f57 100644 --- a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md +++ b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md @@ -2,10 +2,13 @@ Date: 2026-09-01 -Latest attempted `main`: `f98c56b83057b43f8b9618d6f69e1b2f481f77bd` +Latest completed behavioral `main`: `f98c56b83057b43f8b9618d6f69e1b2f481f77bd` -Classification: six live attempts; attempt 6 completed the real autonomous -behavioral chain, while the integrated M5.2 product gate remains blocked +Latest real-effects integration attempt: `1845d86` + +Classification: ten live attempts; attempt 6 completed the real autonomous +behavioral chain, while attempts 7–10 exercised the real Git/child-verifier +integration without completing its end-to-end product gate ## Why this record exists @@ -25,6 +28,10 @@ paused run into product evidence. | 4 | The first user-kickoff turn reached lifecycle publication, then failed `threadmesh_lifecycle_publication_action_mismatch` | Five registered tasks; one durable kickoff turn intent; no event-pump dispatch; the live model's selected tool arguments did not reproduce the coordinator-bound lifecycle material | Not started | Normal scenario cleanup deleted and absence-confirmed five of five sessions and removed the coordinator database, journals, and run root | | 5 | The chain reached the same-A admitted fix turn, then failed `threadmesh_codex_live_context_reconciliation_ambiguous` | Real kickoff publication; reviewer offer, acceptance, admission, and two-tool review; durable `review-failed`; irrelevant skip; same-A offer and acceptance; the admitted fix turn started but had no safely confirmable terminal result inside the existing product-operation window | Partial through `A -> R -> same-A acceptance`; verifier and dependent did not start | Normal scenario cleanup again deleted and absence-confirmed five of five sessions and removed the coordinator database, journals, and run root | | 6 | Completed the bounded event-pump scenario and returned the expected `state=blocked`, `code=threadmesh_m52_independent_verifier_service_pending` result | One kickoff; nine bound real Codex native turns; eight protected receiver turns; eight business tool calls; four published event-pump dispatches; one durable irrelevant skip; same-A identity/worktree reuse; verifier-finalization-before-dependent ordering | Complete `A -> R -> same-A -> V -> dependent`; zero runner phase prompts, direct activation dispatches, manual relay, polling, or irrelevant native turns | Normal scenario cleanup deleted and absence-confirmed five of five sessions, removed the coordinator, and left zero journals; the exact empty operator artifacts directory was then removed | +| 7 | Real-effects R admitted turn ended before a business tool selection | A created and published a real bounded implementation commit; the event pump selected R | Partial through `A -> R admission start` | Normal cleanup deleted and absence-confirmed 5/5 sessions, stopped the child verifier, removed Git and coordinator resources, and left zero journals | +| 8 | Real-effects R admitted turn ended after one completed detached-checkout read | Real A commit/publication and one R read action | Partial through `A -> R detached checkout read` | Same complete 5/5, verifier, Git, coordinator, and journal cleanup | +| 9 | R receiver-decision turn was terminally reconciled | Real A commit/publication and autonomous R route selection | Partial through `A -> R decision start` | Same complete 5/5, verifier, Git, coordinator, and journal cleanup | +| 10 | R accepted, then its admitted turn became ambiguous during a machine-observed DNS/TLS failure | Real A commit/publication, R acceptance, and R admission start | Partial through `A -> R admission start` | Same complete 5/5, verifier, Git, coordinator, and journal cleanup | The fixes in #126 and #127 do not retroactively change the evidence class of attempts 1 or 2. Attempt 3 is bootstrap and cleanup evidence only. Attempt 4 @@ -35,6 +42,14 @@ attempts produced a completed `state=blocked` event-pump gate result. Attempt 6 did. It is the first retained real behavioral pass of the autonomous chain, not an M5.2 completion claim. +Attempts 7–10 run the branch that replaces simulated Git and fixture-owned +signing with the existing bounded Git topology and child-owned verifier key. +They establish partial real-effect execution and cleanup, not a completed +real-effects chain. Attempt 10's Codex log recorded a certificate for +`*.extern.facebook.com` while connecting to the ChatGPT Responses WebSocket; +the system resolver and `curl` independently reproduced the wrong endpoint, +and `codex doctor` reported the WebSocket failure. No TLS check was bypassed. + ## What the combined work established - the operator-supplied Codex-shaped probe is strict and bounded; @@ -73,8 +88,9 @@ production reliability. ## What is not established -- verifier custody and Git implementation/fix effects were not independently - real in an event-pump run; +- verifier custody and real Git effects are wired into the event-pump branch, + but no successful live Codex run has yet traversed the complete integrated + chain; - the completed run demonstrates zero relay and polling by construction, but it does not yet include a timed manual-workflow baseline; - OS-kill recovery, long-turn lease heartbeat, a global cross-dispatch chain, @@ -107,10 +123,9 @@ timeout/reconciliation path without a safely confirmable terminal observation. The bounded window correction in #131 closed that blocker without changing the reconciliation policy. -The next mainline checkpoint is to reuse—not redesign—the existing bounded Git -worktree and verifier foundations in this same correlated event-pump path, then -add the manual relay/polling baseline and the minimum critical negative/restart -case. Attempt 6 correctly reports `state=blocked` and -`liveProductEvidence=false` because verifier custody and Git effects remain -fixture-owned or simulated. It demonstrates real session initiative and clears -the behavioral checkpoint; issue #91 and M5.2 remain open. +The existing bounded Git worktree and child verifier are now wired into the +same correlated event-pump path. The next checkpoint is one successful live +rerun after the local DNS/TLS condition clears, followed by the manual +relay/polling baseline and minimum critical negative/restart case. Attempt 6 +remains the behavioral checkpoint; attempts 7–10 do not upgrade it into an +integrated product pass. Issue #91 and M5.2 remain open. diff --git a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md index a0d45a7..102e3b3 100644 --- a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md +++ b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-behavior.md @@ -103,3 +103,18 @@ still `state=blocked`. The complete attempt history, including five earlier fail-closed runs, is in the [attempt audit](2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md). + +## Follow-on real-effects integration + +The next branch now reuses the existing bounded Git fixture and process-isolated +child verifier in this same event-pump path. Four follow-on live attempts +created real implementation commits and reached R, but none completed the +integrated chain. The latest attempt coincided with a reproducible local +DNS/TLS failure that resolved `chatgpt.com` to a Meta endpoint and presented a +certificate valid only for `*.extern.facebook.com`; TLS verification remained +enabled. + +This does not weaken the behavioral result above, and it does not establish a +real-effects pass. See the +[real-effects checkpoint](2026-09-01-m5-2-real-effects-integration.md) for the +implemented boundary, cleanup evidence, and exact remaining work. diff --git a/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md b/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md new file mode 100644 index 0000000..b2a730e --- /dev/null +++ b/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md @@ -0,0 +1,98 @@ +# M5.2 real-effects integration checkpoint + +Date: 2026-09-01 + +Integration branch: `feat/m52-event-pump-real-effects` + +Status: implementation complete; real Codex end-to-end rerun blocked by local +DNS/TLS failure + +## What changed + +The existing autonomous event-pump path now reuses the repository's existing +bounded Git fixture and process-isolated child verifier. It does not add another +coordinator, scheduler, schema, or verifier protocol. + +In real-effects mode, the same correlated run now requires: + +```text +one user kickoff + -> A commits and publishes a real implementation SHA + -> R accepts, reads a detached reviewer checkout, and reports its own finding + -> the same A task commits and publishes the direct-descendant fix + -> V asks a process-isolated child verifier to test and sign the exact chain + -> trusted finalization completes + -> dependent activation starts +``` + +The parent process receives only the child verifier's public trust anchor. The +child owns the ephemeral private key. The verifier binds the validated base, +fixture seed, implementation and fix commits, exact finding, trusted test blob, +and routing subject. The existing SQLite finalizer remains the only dependency +unlock path. + +The real action contract is 14 model-selected calls across the existing nine +native turns: two kickoff actions, four receiver decisions, and eight admitted +business actions. R uses two actions: read the exact detached checkout, then +report the resource path, counterexample, reason, and read-result digest. The +handler rejects a counterexample that is not present in that checkout. This +keeps the finding model-selected without requiring three serial tool calls for +one bounded review effect. + +## Validation completed + +- the focused coordinator, gate, and product-turn tests pass; +- an automated real-effects positive path binds the Git commits, detached + review, process-isolated verifier, finalization, and exact cleanup; +- an automated negative path rejects a model-reported finding that is absent + from the reviewer checkout and still proves exact cleanup; +- the full repository suite passes 383 unit tests, 55 schema cases, 7 + transition cases, and documentation lint with zero findings; +- live attempts created real implementation commits and reached the autonomous + R route without runner phase prompts or direct activation; +- all failed attempts deleted and absence-confirmed five of five Codex tasks, + stopped the child verifier, removed the bounded Git topology and coordinator, + and left zero recovery journals. + +No live attempt on this integration branch completed the full real-effects +chain, so this record does not claim `liveProductEvidence=true` or M5.2 closure. + +## Live attempt result + +Four integration attempts were retained after the earlier behavioral pass: + +| Attempt | Observed stop | Durable progress | +|---|---|---| +| 7 | R admitted turn ended before a business selection | Real A implementation commit and publication; R route selected | +| 8 | R admitted turn ended after the detached-checkout read | Real A implementation and one completed reviewer read action | +| 9 | R receiver-decision turn was terminally reconciled | Real A implementation and autonomous R route selection | +| 10 | R admitted turn became ambiguous after its accepted decision | Real A implementation, R acceptance, and R admission start | + +Attempt 10 coincided with a machine-observed transport failure. Codex logs +recorded that `wss://chatgpt.com/backend-api/codex/responses` resolved to a peer +whose certificate was valid only for `*.extern.facebook.com`. Independent +checks reproduced the condition: the system resolver returned a Meta address +for `chatgpt.com`, HTTPS certificate verification failed, and `codex doctor` +reported a failed Responses WebSocket while the HTTP endpoint remained +reachable. TLS verification was not disabled and the run was not retried after +the external condition became repeatable. + +## Honest gate status + +The code path can now project real bounded worktrees and a process-isolated, +child-signed verifier result only after a successful correlated run. The +public projector conservatively keeps every M5.2 closure gate open; a summary +boolean cannot close Git, verifier, baseline, restart, heartbeat, global-chain, +or live-product gates. A successful live real-effects run has not yet been +retained. The current claims are therefore: + +- behavioral initiative: established by the earlier sixth real Codex run; +- real-effects wiring: implemented and locally regression-tested; +- successful real-effects Codex chain: not yet established; +- trusted Codex binary provenance: still not established; +- manual relay/polling baseline and minimum negative/restart closure: pending. + +The next action is one fresh live rerun after the machine resolves +`chatgpt.com` to a valid OpenAI endpoint and `codex doctor` no longer reports +the WebSocket certificate failure. Do not change protocol logic or bypass TLS +to compensate for this network condition. diff --git a/docs/10-planning/mainline-plan.md b/docs/10-planning/mainline-plan.md index 73c8617..65a14d8 100644 --- a/docs/10-planning/mainline-plan.md +++ b/docs/10-planning/mainline-plan.md @@ -17,11 +17,10 @@ experiments are explicitly labeled and do not satisfy it. ## Active critical path — 2026-09-01 -M5.2 remains the only implementation critical path, but its next checkpoint is -behavioral evidence rather than infrastructure closure. The repository now has -enough deterministic substrate to ask the product question directly: can real -Codex sessions complete `A -> R -> same-A -> V -> dependent` after one user -kickoff while the runner supplies no later phase prompt or direct activation? +M5.2 remains the only implementation critical path. The behavioral question is +answered by the completed sixth Codex run; the active checkpoint is now a +successful traversal of the same path with real bounded Git effects and the +existing process-isolated child verifier. The older product canary proved real multi-tool turns, same-A reuse, a bounded Git chain, controls, and cleanup, but its four prompts were runner-submitted. @@ -44,16 +43,14 @@ Execute in this order: 1. Treat the real behavioral chain as passed and retain its exact evidence; do not rerun it merely to polish counts or prose. -2. Reuse the existing bounded Git worktree foundation so A's implementation - and same-A fix become observable commits in the same correlated event-pump - run. Do not design a new Git subsystem. -3. Move verifier signing custody to the existing child-verifier boundary and - bind finalization to the real commits, finding, and test result. Keep the - dependent locked until that attestation is accepted. -4. Add the manual relay/polling baseline and the minimum critical +2. Retain one successful live rerun of the now-integrated bounded Git and child + verifier path. Wait for the currently reproducible local DNS/TLS endpoint + failure to clear; do not bypass certificate validation or redesign the + protocol around it. +3. Add the manual relay/polling baseline and the minimum critical negative/restart case required by #91; publish the result without upgrading simulated or operator-supplied evidence. -5. Close #91 only when its original outcome is satisfied, then resume +4. Close #91 only when its original outcome is satisfied, then resume repetition, Kimi parity, and production-hardening evidence. Mainline guardrail: do not add a new substrate or generalize an existing one diff --git a/docs/10-planning/project-status.md b/docs/10-planning/project-status.md index d537efd..d9186d1 100644 --- a/docs/10-planning/project-status.md +++ b/docs/10-planning/project-status.md @@ -1,11 +1,13 @@ # Project status -> Snapshot: 2026-09-01 at `main` commit -> `f98c56b83057b43f8b9618d6f69e1b2f481f77bd`. Technical evidence includes the +> Snapshot: 2026-09-01 after behavioral `main` commit +> `f98c56b83057b43f8b9618d6f69e1b2f481f77bd` and real-effects integration +> commit `1845d86`. Technical evidence includes the > deterministic no-plan autonomous fixture, the earlier runner-sequenced real -> Codex canary, five fail-closed event-pump attempts, and one completed real -> autonomous behavioral chain. M5.2 remains blocked on real Git effects, -> independent verifier custody, a manual baseline, and critical closure cases. +> Codex canary, nine fail-closed event-pump attempts, and one completed real +> autonomous behavioral chain. Real Git and child-verifier wiring is complete, +> but its successful Codex rerun, a manual baseline, and critical closure cases +> remain pending. ## Executive summary @@ -43,7 +45,7 @@ until this outcome is demonstrated. | Research and problem framing | Codex deep dive, community signals, ecosystem comparison, ADRs | Established | | Community adoption | No external stars, forks, watchers, issue comments, or independent setup result as of 2026-08-28 | Unvalidated | | Active product outcome | One-command lifecycle-event and dependency-handoff loop with an inspector | Real Codex A/R/same-A/V/dependent behavioral chain completed after one kickoff; independent Git/verifier closure pending | -| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 379 tests | Executable draft; counts are reported separately | +| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 383 tests | Executable draft; counts are reported separately | | Minimal adapter SDK | `@fyaic/threadmesh` `0.1.0-alpha.0`; six bounded client methods, per-turn proactive bridge, about 20 kB tarball, packed-consumer execution | Real Pi clean-consumer pass; not published to npm | | Local binding | Schema-validated JSON-RPC, transport-derived principals, typed errors | Executable local reference | | Local persistence | SQLite v10 registry, lifecycle state, append-only Git evidence, and durable per-dispatch event-pump selection/publication checkpoints | Experimental; global cross-dispatch pump chain absent | diff --git a/scripts/run-m5-2-event-pump-gate.mjs b/scripts/run-m5-2-event-pump-gate.mjs index 70f03b1..512debc 100644 --- a/scripts/run-m5-2-event-pump-gate.mjs +++ b/scripts/run-m5-2-event-pump-gate.mjs @@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; import process from "node:process"; +import { fileURLToPath } from "node:url"; import { projectM52EventPumpFailureCleanup, @@ -12,6 +13,7 @@ import { "../src/validation/m5-2-event-pump-codex-gate.mjs"; const LIVE_ACK = "maintainer-approved-threadmesh-m52-event-pump-live"; +const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); function options(argv) { if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) return { help: true }; @@ -96,10 +98,9 @@ try { } result = await runM52OperatorSuppliedCodexEventPumpGate({ artifactsDirectory, - sourceRoot: execFileSync("git", ["rev-parse", "--show-toplevel"], { - encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], - }).trim(), + sourceRoot: REPOSITORY_ROOT, validatedBaseSha: execFileSync("git", ["rev-parse", "HEAD"], { + cwd: REPOSITORY_ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }).trim(), temporaryParent: os.tmpdir(), diff --git a/src/coordinator/sqlite-coordinator.mjs b/src/coordinator/sqlite-coordinator.mjs index dbaa781..bb20903 100644 --- a/src/coordinator/sqlite-coordinator.mjs +++ b/src/coordinator/sqlite-coordinator.mjs @@ -3320,7 +3320,10 @@ export class SqliteCoordinator { // requester kickoff is a separate origin kind and cannot be adopted here. publishLifecycleFromCompletedAction( executionId, - { actionOrdinal = 0, expectedTool, event, expectedMaterial } = {}, + { + actionOrdinal = 0, expectedTool, event, expectedMaterial, + expectedActionEvidence = {}, + } = {}, principal, ) { assertLifecycleEvent(event); @@ -3335,6 +3338,15 @@ export class SqliteCoordinator { } const materialKeys = Object.keys(expectedMaterial ?? {}).sort(); const expectedKeys = [...(specification?.materialKeys ?? [])].sort(); + const evidenceKeys = expectedActionEvidence && + typeof expectedActionEvidence === "object" && + !Array.isArray(expectedActionEvidence) + ? Object.keys(expectedActionEvidence).sort() + : null; + if ( + evidenceKeys === null || evidenceKeys.some((key) => + ["sourceEventId", "event", ...expectedKeys].includes(key)) + ) throw codedError("threadmesh_lifecycle_publication_action_mismatch"); const expectedArguments = specification ? [ execution.intent.eventId, execution.intent.messageId, @@ -3342,6 +3354,7 @@ export class SqliteCoordinator { sourceEventId, event: boundedLifecycleActionEventBody(event), ...expectedMaterial, + ...expectedActionEvidence, })) : []; if ( !["completed-turn-bound", "promoted"].includes(execution.intent.state) || @@ -8527,7 +8540,7 @@ export class SqliteCoordinator { execution.intent.turnStart?.turnId !== action.turnId || execution.intent.actor.taskId !== event.sender.taskId || execution.intent.actor.incarnationId !== event.sender.incarnationId || - canonicalJson(actionArgumentKeys) !== canonicalJson(expectedArgumentKeys) || + expectedArgumentKeys.some((key) => !actionArgumentKeys.includes(key)) || ![execution.intent.eventId, execution.intent.messageId] .includes(actionArguments?.sourceEventId) || canonicalJson(actionArguments?.event) !== diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 1816d47..ebb7a6b 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -537,6 +537,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ injectFinalizationFailure = false, injectPreverifiedTamper = null, injectSelectionBindingMismatch = false, + injectRealReviewFindingTamper = false, }) { const preverifiedTamperVariants = new Set([ "state-only", "missing-receipt", "missing-satisfaction", @@ -562,6 +563,8 @@ export async function runCoordinatorDrivenNoPlanScenario({ typeof injectPriorRelevant !== "boolean" || typeof injectFinalizationFailure !== "boolean" || typeof injectSelectionBindingMismatch !== "boolean" || + typeof injectRealReviewFindingTamper !== "boolean" || + (injectRealReviewFindingTamper && !realEffects) || (injectPreverifiedTamper !== null && !preverifiedTamperVariants.has(injectPreverifiedTamper))) { throw new Error("threadmesh_coordinator_driven_artifacts_invalid"); @@ -583,6 +586,8 @@ export async function runCoordinatorDrivenNoPlanScenario({ let gitFixtureCleanup = Object.freeze({ complete: !realEffects }); let privateKey = null; let trustAnchor; + let coordinator = null; + let coordinatorClockSequence = 0; try { if (realEffects) { gitFixture = createBoundedGitLoopFixture({ @@ -605,7 +610,13 @@ export async function runCoordinatorDrivenNoPlanScenario({ publicKeyPem: signing.publicKey.export({ type: "spki", format: "pem" }), }; } + coordinator = new SqliteCoordinator({ + filename: databasePath, + clock: () => NOW + coordinatorClockSequence++, + verificationTrustAnchors: [trustAnchor], + }); } catch (error) { + try { coordinator?.close(); } catch {} try { if (verifierService) { await verifierService.close(); @@ -626,12 +637,6 @@ export async function runCoordinatorDrivenNoPlanScenario({ }; throw error; } - let coordinatorClockSequence = 0; - const coordinator = new SqliteCoordinator({ - filename: databasePath, - clock: () => NOW + coordinatorClockSequence++, - verificationTrustAnchors: [trustAnchor], - }); const actors = { a: { taskId: "task_no_plan_a", incarnationId: "inc_no_plan_a_0001" }, r: { taskId: "task_no_plan_r", incarnationId: "inc_no_plan_r_0001" }, @@ -677,7 +682,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ sender: actors.r, target: actors.a, relationshipId: grants.ra.relationshipId, - content: "Blocking finding: the bounded candidate returns 41, not 42.", + content: realEffects + ? "Blocking finding: artifact.txt contains BAD_COUNTEREXAMPLE." + : "Blocking finding: the bounded candidate returns 41, not 42.", }); const fixEvent = lifecycleEvent({ eventType: "artifact-ready", @@ -742,28 +749,23 @@ export async function runCoordinatorDrivenNoPlanScenario({ required: Object.freeze([...Object.keys(staticArguments), "commitSha"]), }), }); - const realReviewReproduce = Object.freeze({ - ...TOOLS.reviewReproduce, - description: `${TOOLS.reviewReproduce.description} Inspect the exact detached reviewer checkout and return independently reproduced finding evidence.`, - inputSchema: Object.freeze({ - type: "object", additionalProperties: false, - properties: Object.freeze({ - sourceEventId: Object.freeze({ const: artifactEvent.messageId }), - }), - required: Object.freeze(["sourceEventId"]), - }), - }); const realReviewPublish = Object.freeze({ ...TOOLS.review, - description: `${TOOLS.review.description} Copy the exact findingDigest returned by the preceding reproduction tool.`, + description: `${TOOLS.review.description} Use only the artifact content returned by the preceding read tool. Copy its candidateFindingDigest and report the exact resourcePath, counterexample, and a bounded reason.`, inputSchema: Object.freeze({ type: "object", additionalProperties: false, properties: Object.freeze({ sourceEventId: Object.freeze({ const: artifactEvent.messageId }), event: Object.freeze({ const: actionEventBody(reviewEvent) }), + resourcePath: Object.freeze({ type: "string", minLength: 1, maxLength: 200 }), + counterexample: Object.freeze({ type: "string", minLength: 1, maxLength: 256 }), + reason: Object.freeze({ type: "string", minLength: 1, maxLength: 1000 }), findingDigest: Object.freeze({ type: "string", pattern: "^sha256:[a-f0-9]{64}$" }), }), - required: Object.freeze(["sourceEventId", "event", "findingDigest"]), + required: Object.freeze([ + "sourceEventId", "event", "resourcePath", "counterexample", "reason", + "findingDigest", + ]), }), }); const realCommitCandidate = Object.freeze({ @@ -792,7 +794,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ reviewRead: exactArgumentsTool(TOOLS.reviewRead, { sourceEventId: artifactEvent.messageId, }), - reviewReproduce: realEffects ? realReviewReproduce : null, + reviewReproduce: null, review: realEffects ? realReviewPublish : exactArgumentsTool(TOOLS.review, { sourceEventId: artifactEvent.messageId, event: actionEventBody(reviewEvent), findingDigest, }), @@ -839,8 +841,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); const routeHandlerConfigs = Object.freeze([ Object.freeze({ ...ROUTE_HANDLER_CONFIGS[0], businessTools: Object.freeze([ - ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), - scenarioTools.review, + scenarioTools.reviewRead, scenarioTools.review, ]) }), Object.freeze({ ...ROUTE_HANDLER_CONFIGS[1], businessTools: Object.freeze([ scenarioTools.fixApply, scenarioTools.fix, @@ -868,6 +869,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ const verifiedActivationOrder = []; const cleanupRoles = []; const roleCwds = {}; + const roleBusinessCwds = {}; try { throwIfShutdownRequested(signal); adapter = providedRuntime?.adapter ?? new DeterministicNoPlanCodexAdapter({ @@ -890,6 +892,25 @@ export async function runCoordinatorDrivenNoPlanScenario({ }], }; } + if (realEffects && selectedTool === TOOLS.implementationCommit.name) { + const fixing = knownMessage?.messageId === reviewEvent.messageId; + return { + text: fixing + ? "Committed and published the exact admitted fix." + : "Committed and published the bounded implementation.", + toolCalls: [{ + tool: TOOLS.implementationCommit.name, + arguments: { + phase: fixing ? "fix" : "implementation", + content: fixing ? REAL_EFFECT_FIX : REAL_EFFECT_IMPLEMENTATION, + sourceEventId: knownMessage.messageId, + }, + }, { + tool: fixing ? TOOLS.fix.name : TOOLS.implementation.name, + arguments: {}, + }], + }; + } if (selectedTool === TOOLS.implementation.name) { return { text: "Implementation published.", toolCalls: [{ tool: selectedTool, @@ -900,6 +921,16 @@ export async function runCoordinatorDrivenNoPlanScenario({ }] }; } if (selectedTool === TOOLS.reviewRead.name) { + if (realEffects) return { + text: "Read the detached artifact and reported the exact counterexample.", + toolCalls: [{ + tool: TOOLS.reviewRead.name, + arguments: { sourceEventId: knownMessage.messageId }, + }, { + tool: TOOLS.review.name, + arguments: {}, + }], + }; return { text: "Artifact inspected and blocking finding published.", toolCalls: [{ tool: TOOLS.reviewRead.name, arguments: { sourceEventId: knownMessage.messageId }, @@ -947,9 +978,40 @@ export async function runCoordinatorDrivenNoPlanScenario({ } return { text: "No relevant action.", toolCalls: [] }; }, + resolveToolArguments({ canonicalInput, tool: selectedTool, arguments: value, + priorOutputs }) { + if (!realEffects || Object.keys(value).length > 0) return value; + const input = JSON.parse(canonicalInput); + const knownMessage = [artifactEvent, reviewEvent, fixEvent, verifiedEvent] + .find(({ messageId }) => input.prompt.includes(messageId)); + if (selectedTool === TOOLS.implementation.name) return { + sourceEventId: artifactEvent.messageId, + event: actionEventBody(artifactEvent), + commitSha: priorOutputs[0].subjectSha, + }; + if (selectedTool === TOOLS.fix.name) return { + sourceEventId: reviewEvent.messageId, + event: actionEventBody(fixEvent), + commitSha: priorOutputs[0].subjectSha, + }; + if (selectedTool === TOOLS.review.name) { + const read = priorOutputs[0]; + return { + sourceEventId: knownMessage.messageId, + event: actionEventBody(reviewEvent), + resourcePath: read.resourcePath, + counterexample: injectRealReviewFindingTamper + ? "WRONG_COUNTEREXAMPLE" : read.content.trim(), + reason: "The detached artifact contains the exact blocking counterexample.", + findingDigest: read.candidateFindingDigest, + }; + } + return value; + }, }); runtime = providedRuntime ?? new CodexLiveAgentRuntime({ command: "/fake/codex", adapter }); roleCwds.a = realEffects ? gitFixture.implementerWorktree : artifactsDirectory; + roleBusinessCwds.a = roleCwds.a; refs.a = await runtime.createRole({ role: "a", cwd: roleCwds.a, tools: [ @@ -973,30 +1035,31 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); throwIfShutdownRequested(signal); roleCwds.r = realEffects ? gitFixture.root : artifactsDirectory; + roleBusinessCwds.r = realEffects + ? path.join(gitFixture.root, "reviewer") : artifactsDirectory; refs.r = await runtime.createRole({ role: "r", cwd: roleCwds.r, tools: [ - scenarioTools.rDecision, - ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), - scenarioTools.review, + scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review, ], phaseTools: { "receiver-decision": [scenarioTools.rDecision], "r-review": [ - ...(realEffects ? [scenarioTools.reviewReproduce] : [scenarioTools.reviewRead]), - scenarioTools.review, + scenarioTools.reviewRead, scenarioTools.review, ], }, protectedPhases: { "receiver-decision": "receiver-decision", "r-review": "admitted-tool", }, instructions: realEffects - ? "Review only coordinator-admitted context. In the admitted review turn, call every offered tool exactly once and in order: reproduce the finding from the detached checkout, then publish by copying the returned findingDigest. Do not stop after the first tool result." + ? "Review only coordinator-admitted context. In the admitted review turn, call every offered tool exactly once and in order: read the detached-checkout artifact, then report the exact counterexample found in the returned content and copy candidateFindingDigest. Do not stop after the read result." : "Review only coordinator-admitted context.", scenarioId: "coordinator_driven_no_plan", }); throwIfShutdownRequested(signal); roleCwds.v = realEffects ? gitFixture.root : artifactsDirectory; + roleBusinessCwds.v = realEffects + ? path.join(gitFixture.root, "verifier") : artifactsDirectory; refs.v = await runtime.createRole({ role: "v", cwd: roleCwds.v, tools: [scenarioTools.vDecision, scenarioTools.verifyRead, scenarioTools.verify], @@ -1012,6 +1075,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); throwIfShutdownRequested(signal); roleCwds.dependent = realEffects ? gitFixture.root : artifactsDirectory; + roleBusinessCwds.dependent = roleCwds.dependent; refs.dependent = await runtime.createRole({ role: "dependent", cwd: roleCwds.dependent, tools: [ @@ -1030,6 +1094,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }); throwIfShutdownRequested(signal); roleCwds.irrelevant = realEffects ? gitFixture.root : artifactsDirectory; + roleBusinessCwds.irrelevant = roleCwds.irrelevant; refs.irrelevant = await runtime.createRole({ role: "irrelevant", cwd: roleCwds.irrelevant, tools: [scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review], @@ -1158,7 +1223,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.r, principal: principal(actors.r), role: "r", - cwd: realEffects ? path.join(gitFixture.root, "reviewer") : artifactsDirectory, + cwd: roleBusinessCwds.r, ref: refs.r, routes: [{ handlerId: routeHandlerConfigs[0].handlerId, @@ -1174,30 +1239,41 @@ export async function runCoordinatorDrivenNoPlanScenario({ if (selectedTool === TOOLS.reviewRead.name) { if (!realEffects) return { artifactDigest: digest("admitted-review-artifact") }; const checkout = gitFixture.verifyReviewerCheckout({ implementationSha }); + const content = fs.readFileSync( + path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", + ); + const candidate = { + resourcePath: REAL_EFFECT_RESOURCE, + counterexample: content.trim(), + }; return { resourcePath: REAL_EFFECT_RESOURCE, - content: fs.readFileSync( - path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", - ), + content, commitSha: checkout.subjectSha, + candidateFindingDigest: independentGitFindingDigest(candidate), }; } - if (realEffects && selectedTool === TOOLS.reviewReproduce.name) { + if (realEffects && selectedTool === TOOLS.review.name) { const checkout = gitFixture.verifyReviewerCheckout({ implementationSha }); const content = fs.readFileSync( path.join(reviewerCheckout.worktree, REAL_EFFECT_RESOURCE), "utf8", ); const candidate = { - resourcePath: REAL_EFFECT_RESOURCE, - counterexample: REAL_EFFECT_IMPLEMENTATION.trim(), + resourcePath: value?.resourcePath, + counterexample: value?.counterexample, }; + const candidateDigest = independentGitFindingDigest(candidate); if ( value?.sourceEventId !== artifactEvent.messageId || + candidate.resourcePath !== REAL_EFFECT_RESOURCE || + candidate.counterexample !== content.trim() || !content.includes(candidate.counterexample) || - checkout.subjectSha !== implementationSha + checkout.subjectSha !== implementationSha || + typeof value?.reason !== "string" || value.reason.length < 1 || + value?.findingDigest !== candidateDigest ) throw scenarioError("threadmesh_real_effect_review_finding_not_reproduced"); finding = Object.freeze(candidate); - findingDigest = independentGitFindingDigest(finding); + findingDigest = candidateDigest; payloads["review-failed"].findingDigest = findingDigest; payloads["review-failed"].reproductionEvidenceDigest = sha256Digest({ commitSha: implementationSha, @@ -1205,10 +1281,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ contentDigest: sha256Digest(content), findingDigest, }); - return { findingDigest, reproducible: true }; - } - if (realEffects && value?.findingDigest !== findingDigest) { - throw scenarioError("threadmesh_real_effect_review_publication_invalid"); + return { findingDigest, reproducible: true, implementationSha }; } return { findingDigest, blocking: true, implementationSha }; }, @@ -1232,6 +1305,11 @@ export async function runCoordinatorDrivenNoPlanScenario({ expectedTool: TOOLS.review.name, event: reviewEvent, expectedMaterial: { findingDigest }, + ...(realEffects ? { expectedActionEvidence: { + resourcePath: finding.resourcePath, + counterexample: finding.counterexample, + reason: JSON.parse(execution.actions[publicationOrdinal].argsJson).reason, + } } : {}), }, principal(actors.r)); promoteAttention(coordinator, activation, promoted, actors.r); }, @@ -1241,7 +1319,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.a, principal: principal(actors.a), role: "a", - cwd: realEffects ? gitFixture.implementerWorktree : artifactsDirectory, + cwd: roleBusinessCwds.a, ref: refs.a, routes: [{ handlerId: routeHandlerConfigs[1].handlerId, @@ -1317,7 +1395,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.v, principal: principal(actors.v), role: "v", - cwd: realEffects ? path.join(gitFixture.root, "verifier") : artifactsDirectory, + cwd: roleBusinessCwds.v, ref: refs.v, routes: [{ handlerId: routeHandlerConfigs[2].handlerId, @@ -1396,7 +1474,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.dependent, principal: principal(actors.dependent), role: "dependent", - cwd: artifactsDirectory, + cwd: roleBusinessCwds.dependent, ref: refs.dependent, routes: [{ handlerId: routeHandlerConfigs[3].handlerId, @@ -1544,7 +1622,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ receiver: actors.irrelevant, principal: principal(actors.irrelevant), role: "irrelevant", - cwd: artifactsDirectory, + cwd: roleBusinessCwds.irrelevant, ref: refs.irrelevant, routes: [{ handlerId: routeHandlerConfigs[4].handlerId, @@ -1564,7 +1642,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ const kickoff = await runKickoff({ coordinator, runtime, actor: actors.a, ref: refs.a, event: artifactEvent, args: kickoffArgs, - cwd: realEffects ? gitFixture.implementerWorktree : artifactsDirectory, + cwd: roleBusinessCwds.a, recoveryDirectory: journalDirectory, ownedJournalPaths, businessTools: realEffects @@ -1934,7 +2012,9 @@ export async function runCoordinatorDrivenNoPlanScenario({ const sessionRecords = Object.entries(refs).map(([role, ref]) => ({ role, refDigest: sha256Digest(ref), - worktreeDigest: sha256Digest({ cwd: roleCwds[role] ?? artifactsDirectory }), + worktreeDigest: sha256Digest({ + cwd: roleBusinessCwds[role] ?? roleCwds[role] ?? artifactsDirectory, + }), })); const sessionManifest = { recordCount: sessionRecords.length, @@ -2012,7 +2092,8 @@ export async function runCoordinatorDrivenNoPlanScenario({ mode: realEffects ? "process-isolated-child-service-signed" : "deterministic-in-process-trusted-signing", - externalIndependentVerifier: realEffects, + externalIndependentVerifier: false, + processIsolatedVerifier: realEffects, signer: realEffects ? "process-isolated-child-owned-ephemeral-key" : "fixture-owned-ephemeral-key", @@ -2142,7 +2223,11 @@ export async function runCoordinatorDrivenNoPlanScenario({ } catch (error) { failure ??= error; } - coordinator.close(); + try { + coordinator.close(); + } catch (error) { + failure ??= error; + } if (verifierService) { try { const closed = await verifierService.close(); diff --git a/src/validation/deterministic-no-plan-codex-adapter.mjs b/src/validation/deterministic-no-plan-codex-adapter.mjs index ec54791..26bf59c 100644 --- a/src/validation/deterministic-no-plan-codex-adapter.mjs +++ b/src/validation/deterministic-no-plan-codex-adapter.mjs @@ -102,12 +102,15 @@ function normalizeDecision(value, allowedTools) { export class DeterministicNoPlanCodexAdapter { constructor({ decideTurn = () => ({ text: "No relevant action.", toolCalls: [] }), + resolveToolArguments = ({ arguments: value }) => value, clock = () => new Date("2026-09-01T00:00:00.000Z"), } = {}) { - if (typeof decideTurn !== "function" || typeof clock !== "function") { + if (typeof decideTurn !== "function" || typeof resolveToolArguments !== "function" || + typeof clock !== "function") { fail("threadmesh_deterministic_adapter_configuration_invalid"); } this.decideTurn = decideTurn; + this.resolveToolArguments = resolveToolArguments; this.clock = clock; this.threads = new Map(); this.deletedThreadIds = new Set(); @@ -235,15 +238,31 @@ export class DeterministicNoPlanCodexAdapter { }); try { + const priorOutputs = []; for (const [ordinal, selected] of decision.toolCalls.entries()) { + const resolvedArguments = await Reflect.apply( + this.resolveToolArguments, + undefined, + [{ + canonicalInput, + ordinal, + tool: selected.tool, + arguments: copy(selected.arguments), + priorOutputs: copy(priorOutputs), + }], + ); + if (!resolvedArguments || typeof resolvedArguments !== "object" || + Array.isArray(resolvedArguments)) { + fail("threadmesh_deterministic_adapter_decision_invalid"); + } const metadata = { threadId: thread.ref.threadId, turnId, callId: `call-${turnId}-${ordinal}`, ordinal, tool: selected.tool, - arguments: copy(selected.arguments), - argumentsDigest: sha256Digest(selected.arguments), + arguments: copy(resolvedArguments), + argumentsDigest: sha256Digest(resolvedArguments), }; await options.beforeToolCall?.(metadata); const output = await options.onToolCall(metadata); @@ -259,6 +278,7 @@ export class DeterministicNoPlanCodexAdapter { }; await options.afterToolCall?.(completed); turn.toolCalls.push(Object.freeze(completed)); + priorOutputs.push(copy(output)); } turn.status = "completed"; } catch (error) { diff --git a/src/validation/live-agent-scenario.mjs b/src/validation/live-agent-scenario.mjs index 1b52c3b..c51a5f6 100644 --- a/src/validation/live-agent-scenario.mjs +++ b/src/validation/live-agent-scenario.mjs @@ -988,6 +988,9 @@ export class CodexLiveAgentRuntime { turnStatus: classified.turnStatus, journal: journalProjection, }; + if (typeof originCode === "string" && originCode.length > 0) { + terminal.originCode = originCode; + } throw terminal; } } @@ -1570,6 +1573,9 @@ export class CodexLiveAgentRuntime { turnStatus: classified.turnStatus, journal: journalProjection, }; + if (typeof originCode === "string" && originCode.length > 0) { + terminal.originCode = originCode; + } throw terminal; }; @@ -1730,7 +1736,11 @@ export class CodexLiveAgentRuntime { baseline, journalProjection, startedTurnId: started?.turnId ?? null, - originCode: typeof error?.code === "string" ? error.code : error?.name ?? null, + originCode: /^[a-z][a-z0-9_]{0,127}$/u.test(error?.code ?? "") + ? error.code + : (/^[A-Za-z][A-Za-z0-9]{0,63}$/u.test(error?.name ?? "") + ? error.name + : "unclassified_error"), }); } } diff --git a/src/validation/m5-2-event-pump-codex-gate.mjs b/src/validation/m5-2-event-pump-codex-gate.mjs index b2db36e..b0d1289 100644 --- a/src/validation/m5-2-event-pump-codex-gate.mjs +++ b/src/validation/m5-2-event-pump-codex-gate.mjs @@ -12,7 +12,6 @@ const DIGEST = /^sha256:[a-f0-9]{64}$/u; const ADAPTER_OWNED_CODEX_USER_AGENT = /^threadmesh-codex-app-server-adapter\/[0-9]+\.[0-9]+\.[0-9]+ \(Mac OS [0-9]+(?:\.[0-9]+){1,3}; (?:arm64|x86_64)\) dumb \(threadmesh-codex-app-server-adapter; 0\.0\.0\)$/u; const BLOCKED_CODE = "threadmesh_m52_independent_verifier_service_pending"; -const REAL_EFFECTS_BLOCKED_CODE = "threadmesh_m52_trusted_codex_binary_provenance_pending"; const EXPECTED_PHASES = Object.freeze([ ["a", "user-kickoff", "kickoff", 1], ["r", "receiver-decision", "decision", 1], @@ -64,7 +63,7 @@ const REAL_EFFECT_EXPECTED_ACTION_SEQUENCES = Object.freeze([ Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_artifact"]), Object.freeze(["threadmesh_decide_offer"]), Object.freeze([ - "threadmesh_reproduce_review_finding", "threadmesh_report_review_finding", + "threadmesh_review_read_artifact", "threadmesh_report_review_finding", ]), Object.freeze(["threadmesh_decide_offer"]), Object.freeze(["threadmesh_commit_candidate", "threadmesh_publish_dependency"]), @@ -435,7 +434,8 @@ function projectGateResult(coreResult, { "modelSelectedToolCalls", ], "runtime"); exactObject(coreResult.verification, [ - "mode", "externalIndependentVerifier", "signer", "nativeVerifierSessionIndependent", + "mode", "externalIndependentVerifier", "processIsolatedVerifier", "signer", + "nativeVerifierSessionIndependent", "nativeVerifierTurnIdDigest", "allLifecycleNativeTurnIdsDistinct", "lifecycleNativeTurnCount", "signatureVerified", "trustAnchorDigest", "resultDigestBound", @@ -544,7 +544,8 @@ function projectGateResult(coreResult, { coreResult.dependent?.effectCommittedAfterFinalization !== true) { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "finalizationOrder"); } - if (coreResult.verification?.externalIndependentVerifier !== realEffects || + if (coreResult.verification?.externalIndependentVerifier !== false || + coreResult.verification?.processIsolatedVerifier !== realEffects || coreResult.verification?.mode !== (realEffects ? "process-isolated-child-service-signed" : "deterministic-in-process-trusted-signing") || @@ -612,9 +613,9 @@ function projectGateResult(coreResult, { throw gateError("threadmesh_m52_event_pump_gate_result_invalid", "productProbe"); } const remainingGates = [ - ...(realEffects ? [] : [ - "independent-verifier-service", "real-bounded-git-worktree-effects", - ]), + "independent-verifier-service", "real-bounded-git-worktree-effects", + "manual-relay-polling-baseline", "minimum-critical-negative-restart", + ...EXPECTED_PENDING_GATES, ...(operatorSuppliedCodexShapedRuntime ? ["trusted-codex-binary-provenance"] : ["real-codex-product-run"]), @@ -626,7 +627,7 @@ function projectGateResult(coreResult, { return Object.freeze({ schemaVersion: 1, state: "blocked", - code: realEffects ? REAL_EFFECTS_BLOCKED_CODE : BLOCKED_CODE, + code: BLOCKED_CODE, product, evidenceClass: deterministic ? "deterministic-event-pump-codex-gate" diff --git a/test/coordinator-driven-no-plan-scenario.test.mjs b/test/coordinator-driven-no-plan-scenario.test.mjs index f470349..a75efa8 100644 --- a/test/coordinator-driven-no-plan-scenario.test.mjs +++ b/test/coordinator-driven-no-plan-scenario.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -7,6 +8,39 @@ import test from "node:test"; import { sha256Digest } from "../src/canonical-json.mjs"; import { runCoordinatorDrivenNoPlanScenario } from "../src/validation/coordinator-driven-no-plan-scenario.mjs"; +import { projectM52EventPumpCodexGateResult } from + "../src/validation/m5-2-event-pump-codex-gate.mjs"; + +function git(repoPath, ...args) { + return execFileSync("git", ["-C", repoPath, ...args], { + encoding: "utf8", + env: { + PATH: process.env.PATH ?? "", + LANG: "C", + GIT_CONFIG_NOSYSTEM: "1", + GIT_AUTHOR_NAME: "ThreadMesh Test", + GIT_AUTHOR_EMAIL: "threadmesh@example.invalid", + GIT_COMMITTER_NAME: "ThreadMesh Test", + GIT_COMMITTER_EMAIL: "threadmesh@example.invalid", + }, + }).trim(); +} + +function createRealEffectsSource(t) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "threadmesh-real-effects-source-")); + const fixtureDirectory = path.join(root, "test", "fixtures"); + fs.mkdirSync(fixtureDirectory, { recursive: true }); + fs.copyFileSync( + new URL("fixtures/independent-git-verifier-target.test.mjs", import.meta.url), + path.join(fixtureDirectory, "independent-git-verifier-target.test.mjs"), + ); + fs.writeFileSync(path.join(root, "README.md"), "bounded real-effects source\n"); + git(root, "init", "--quiet"); + git(root, "add", "."); + git(root, "commit", "--quiet", "--no-gpg-sign", "-m", "source base"); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + return { root, sha: git(root, "rev-parse", "HEAD") }; +} test("one pump autonomously closes A to R to same-A to V to dependent", async (t) => { const artifactsDirectory = fs.mkdtempSync( @@ -101,6 +135,7 @@ test("one pump autonomously closes A to R to same-A to V to dependent", async (t assert.equal(result.bindings.receiverDecisions, 4); assert.equal(result.bindings.contextAdmissions, 4); assert.equal(result.verification.externalIndependentVerifier, false); + assert.equal(result.verification.processIsolatedVerifier, false); assert.equal(result.verification.signer, "fixture-owned-ephemeral-key"); assert.equal(result.verification.nativeVerifierSessionIndependent, true); assert.match(result.verification.nativeVerifierTurnIdDigest, /^sha256:[a-f0-9]{64}$/u); @@ -165,6 +200,111 @@ test("one pump autonomously closes A to R to same-A to V to dependent", async (t deleted && absenceVerified)); }); +test("real-effects path binds Git commits, model finding, and child verifier", async (t) => { + const artifactsDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), "threadmesh-real-effects-artifacts-"), + ); + t.after(() => fs.rmSync(artifactsDirectory, { recursive: true, force: true })); + const source = createRealEffectsSource(t); + + const result = await runCoordinatorDrivenNoPlanScenario({ + artifactsDirectory, + realEffects: true, + sourceRoot: source.root, + validatedBaseSha: source.sha, + temporaryParent: artifactsDirectory, + }); + + assert.equal(result.state, "passed-full-functional-in-process-fixture"); + assert.equal(result.liveProductEvidence, false); + assert.equal(result.gitEffects.realBoundedWorktrees, true); + assert.match(result.gitEffects.implementationSha, /^[a-f0-9]{40}$/u); + assert.match(result.gitEffects.fixSha, /^[a-f0-9]{40}$/u); + assert.notEqual(result.gitEffects.implementationSha, result.gitEffects.fixSha); + assert.equal(result.gitEffects.directDescendant, true); + assert.equal(result.gitEffects.reviewerDetached, true); + assert.equal(result.gitEffects.verifierDetached, true); + assert.equal(result.verification.externalIndependentVerifier, false); + assert.equal(result.verification.processIsolatedVerifier, true); + assert.equal(result.verification.signer, + "process-isolated-child-owned-ephemeral-key"); + assert.equal(result.verification.signatureVerified, true); + assert.equal(result.verification.resultDigestBound, true); + assert.equal(result.runtime.modelSelectedToolCalls, 14); + assert.deepEqual(result.nativeTurnManifest.records.map(({ actions }) => + actions.map(({ tool }) => tool)), [ + ["threadmesh_commit_candidate", "threadmesh_publish_artifact"], + ["threadmesh_decide_offer"], + ["threadmesh_review_read_artifact", "threadmesh_report_review_finding"], + ["threadmesh_decide_offer"], + ["threadmesh_commit_candidate", "threadmesh_publish_dependency"], + ["threadmesh_decide_offer"], + ["threadmesh_read_verification_chain", "threadmesh_verify_exact_chain"], + ["threadmesh_decide_offer"], + ["threadmesh_check_finalized_dependency", + "threadmesh_activate_verified_dependency"], + ]); + const worktrees = Object.fromEntries(result.sessionManifest.records.map( + ({ role, worktreeDigest }) => [role, worktreeDigest], + )); + assert.notEqual(worktrees.r, worktrees.a); + assert.notEqual(worktrees.v, worktrees.a); + assert.notEqual(worktrees.r, worktrees.v); + assert.equal(result.cleanup.complete, true); + assert.equal(result.cleanup.verifierServiceClosed, true); + assert.equal(result.cleanup.gitFixture.complete, true); + assert.equal(result.cleanup.runRootRemoved, true); + assert.equal(git(source.root, "rev-parse", "HEAD"), source.sha); + assert.equal(git(source.root, "status", "--porcelain"), ""); + + const projected = projectM52EventPumpCodexGateResult(result); + assert.equal(projected.state, "blocked"); + assert.equal(projected.liveProductEvidence, false); + assert.equal(projected.verificationMode, + "process-isolated-child-service-signed"); + assert.deepEqual(projected.remainingGates, [ + "independent-verifier-service", + "real-bounded-git-worktree-effects", + "manual-relay-polling-baseline", + "minimum-critical-negative-restart", + "cross-process-os-kill-and-long-turn-lease-heartbeat", + "global-selection-chain", + "real-codex-product-run", + ]); +}); + +test("real-effects path rejects a model-reported finding not present in checkout", async (t) => { + const artifactsDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), "threadmesh-real-effects-tamper-"), + ); + t.after(() => fs.rmSync(artifactsDirectory, { recursive: true, force: true })); + const source = createRealEffectsSource(t); + + await assert.rejects( + () => runCoordinatorDrivenNoPlanScenario({ + artifactsDirectory, + realEffects: true, + sourceRoot: source.root, + validatedBaseSha: source.sha, + temporaryParent: artifactsDirectory, + injectRealReviewFindingTamper: true, + }), + (error) => { + assert.equal(error?.code, "threadmesh_codex_live_context_terminal_reconciled"); + assert.equal(error?.originCode, + "threadmesh_real_effect_review_finding_not_reproduced"); + assert.equal(error.cleanup?.complete, true); + assert.equal(error.cleanup?.roles.length, 5); + assert.equal(error.cleanup?.verifierServiceClosed, true); + assert.equal(error.cleanup?.gitFixture.complete, true); + assert.equal(error.cleanup?.runRootRemoved, true); + return true; + }, + ); + assert.equal(git(source.root, "rev-parse", "HEAD"), source.sha); + assert.equal(git(source.root, "status", "--porcelain"), ""); +}); + test("public manifest rejects a runtime selection binding not present in SQLite", async (t) => { const artifactsDirectory = fs.mkdtempSync( path.join(os.tmpdir(), "threadmesh-coordinator-selection-mismatch-"), diff --git a/test/m5-2-event-pump-codex-gate.test.mjs b/test/m5-2-event-pump-codex-gate.test.mjs index d80ce35..20de7b3 100644 --- a/test/m5-2-event-pump-codex-gate.test.mjs +++ b/test/m5-2-event-pump-codex-gate.test.mjs @@ -61,6 +61,8 @@ test("deterministic Codex gate is pump-driven but remains blocked on verifier cu assert.equal(result.verificationMode, "fixture-owned-ephemeral-key-not-independent"); assert.deepEqual(result.remainingGates, [ "independent-verifier-service", "real-bounded-git-worktree-effects", + "manual-relay-polling-baseline", "minimum-critical-negative-restart", + "cross-process-os-kill-and-long-turn-lease-heartbeat", "global-selection-chain", "real-codex-product-run", ]); assert.equal(result.evidence.nativeTurnManifest.recordCount, 9); diff --git a/test/product-turn-primitives.test.mjs b/test/product-turn-primitives.test.mjs index 497f2cd..fe34013 100644 --- a/test/product-turn-primitives.test.mjs +++ b/test/product-turn-primitives.test.mjs @@ -906,3 +906,27 @@ test("admitted ambiguous reconciliation retains only the bounded origin code", a assert.equal(fake.state.nativeStarts, 1); assert.equal(fs.existsSync(value.filename), true); }); + +test("admitted ambiguous reconciliation normalizes an unsafe origin code", async (t) => { + const fake = adapter({ mode: "terminal", recoveryStatus: "completed" }); + const original = fake.runAutonomousToolTurn.bind(fake); + fake.runAutonomousToolTurn = async (options) => { + try { return await original(options); } catch (error) { + error.code = `unsafe\n${"x".repeat(256)}`; + throw error; + } + }; + const value = await fixture(t, fake, "admitted-unsafe-origin"); + const admission = prepared(); + await assert.rejects( + () => value.runtime.runAdmittedToolTurn({ + role: "r", phase: "review", cwd: "/private/reviewer", ref: value.ref, + prepared: admission, admissionBinding: createAdmittedTurnBinding(admission), + scenarioId: value.scenarioId, allowedToolNames: [BUSINESS_TOOL.name], + turnRecovery: recovery(value.filename, "admitted-unsafe-origin"), + async onToolCall() { return {}; }, async onAdmissionReceipt() { return {}; }, + }), + (error) => error?.code === "threadmesh_codex_live_context_reconciliation_ambiguous" && + error?.originCode === "Error", + ); +}); From 4e9ac7c35f2808d3da13c22f2dd95e1cbc9f2865 Mon Sep 17 00:00:00 2001 From: veil-chow-fyaic <247294299+veil-chow-fyaic@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:29:07 +0800 Subject: [PATCH 5/5] fix: seal reviewer context and lifecycle evidence --- ...5-2-real-codex-event-pump-attempt-audit.md | 7 ++ ...026-09-01-m5-2-real-effects-integration.md | 14 +++- docs/10-planning/project-status.md | 2 +- src/coordinator/sqlite-coordinator.mjs | 21 ++++- .../coordinator-driven-no-plan-scenario.mjs | 12 ++- ...ordinator-driven-no-plan-scenario.test.mjs | 2 +- test/exact-lifecycle-binding.test.mjs | 77 +++++++++++++++++++ 7 files changed, 127 insertions(+), 8 deletions(-) diff --git a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md index 8897f57..fb04f68 100644 --- a/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md +++ b/docs/09-reviews/2026-09-01-m5-2-real-codex-event-pump-attempt-audit.md @@ -18,6 +18,11 @@ the operator-supplied Codex-shaped event-pump attempts actually established without combining those evidence classes or upgrading a failed or paused run into product evidence. +This is a bounded operator audit record, not a canonical machine-verifiable +attempt bundle. The live failure CLI retained exact cleanup projection but did +not yet emit SQLite-derived partial-stage manifests for attempts 7–10. Their +partial-progress rows therefore remain descriptive and cannot close a gate. + ## Attempt ledger | Attempt | Stop condition | Evidence established | Autonomous chain | Cleanup claim | @@ -95,6 +100,8 @@ production reliability. does not yet include a timed manual-workflow baseline; - OS-kill recovery, long-turn lease heartbeat, a global cross-dispatch chain, Kimi parity, and repetition remain untested on this live path. +- interrupted live runs do not yet expose a bounded SQLite-derived partial + stage/turn/dispatch manifest through the public failure projection. ## Sequencing correction diff --git a/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md b/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md index b2a730e..fc5e185 100644 --- a/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md +++ b/docs/09-reviews/2026-09-01-m5-2-real-effects-integration.md @@ -39,6 +39,12 @@ handler rejects a counterexample that is not present in that checkout. This keeps the finding model-selected without requiring three serial tool calls for one bounded review effect. +The reviewer-visible event and dynamic-tool contract are checked to exclude +the sealed resource path, counterexample, and repair value. A uses a bounded +fixture schema for implementation and fix content; this checkpoint tests +cross-session initiative and evidence binding, not open-ended bug-solving +quality. + ## Validation completed - the focused coordinator, gate, and product-turn tests pass; @@ -46,7 +52,9 @@ one bounded review effect. review, process-isolated verifier, finalization, and exact cleanup; - an automated negative path rejects a model-reported finding that is absent from the reviewer checkout and still proves exact cleanup; -- the full repository suite passes 383 unit tests, 55 schema cases, 7 +- lifecycle publication accepts only one of the explicitly registered exact + review-evidence key sets and revalidates it after SQLite reopen; +- the full repository suite passes 384 unit tests, 55 schema cases, 7 transition cases, and documentation lint with zero findings; - live attempts created real implementation commits and reached the autonomous R route without runner phase prompts or direct activation; @@ -56,6 +64,9 @@ one bounded review effect. No live attempt on this integration branch completed the full real-effects chain, so this record does not claim `liveProductEvidence=true` or M5.2 closure. +The live failure CLI currently retains bounded cleanup but not a SQLite-derived +partial-progress manifest. Consequently, the attempt table below is an +operator audit record, not a canonical machine-verifiable attempt bundle. ## Live attempt result @@ -91,6 +102,7 @@ retained. The current claims are therefore: - successful real-effects Codex chain: not yet established; - trusted Codex binary provenance: still not established; - manual relay/polling baseline and minimum negative/restart closure: pending. +- bounded partial-progress manifests for interrupted live attempts: pending. The next action is one fresh live rerun after the machine resolves `chatgpt.com` to a valid OpenAI endpoint and `codex doctor` no longer reports diff --git a/docs/10-planning/project-status.md b/docs/10-planning/project-status.md index d9186d1..96ba292 100644 --- a/docs/10-planning/project-status.md +++ b/docs/10-planning/project-status.md @@ -45,7 +45,7 @@ until this outcome is demonstrated. | Research and problem framing | Codex deep dive, community signals, ecosystem comparison, ADRs | Established | | Community adoption | No external stars, forks, watchers, issue comments, or independent setup result as of 2026-08-28 | Unvalidated | | Active product outcome | One-command lifecycle-event and dependency-handoff loop with an inspector | Real Codex A/R/same-A/V/dependent behavioral chain completed after one kickoff; independent Git/verifier closure pending | -| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 383 tests | Executable draft; counts are reported separately | +| Protocol draft | 14 JSON Schemas; 55 schema cases; 7 transition cases; 384 tests | Executable draft; counts are reported separately | | Minimal adapter SDK | `@fyaic/threadmesh` `0.1.0-alpha.0`; six bounded client methods, per-turn proactive bridge, about 20 kB tarball, packed-consumer execution | Real Pi clean-consumer pass; not published to npm | | Local binding | Schema-validated JSON-RPC, transport-derived principals, typed errors | Executable local reference | | Local persistence | SQLite v10 registry, lifecycle state, append-only Git evidence, and durable per-dispatch event-pump selection/publication checkpoints | Experimental; global cross-dispatch pump chain absent | diff --git a/src/coordinator/sqlite-coordinator.mjs b/src/coordinator/sqlite-coordinator.mjs index bb20903..5e29762 100644 --- a/src/coordinator/sqlite-coordinator.mjs +++ b/src/coordinator/sqlite-coordinator.mjs @@ -87,18 +87,25 @@ const FINAL_GIT_EVIDENCE_TOOL = "threadmesh_verify_exact_chain"; const LIFECYCLE_PUBLICATION_TOOLS = Object.freeze({ threadmesh_publish_artifact: Object.freeze({ eventType: "artifact-ready", materialKeys: Object.freeze(["commitSha"]), + actionEvidenceKeySets: Object.freeze([Object.freeze([])]), }), threadmesh_report_review_finding: Object.freeze({ eventType: "review-failed", materialKeys: Object.freeze(["findingDigest"]), + actionEvidenceKeySets: Object.freeze([ + Object.freeze([]), + Object.freeze(["counterexample", "reason", "resourcePath"]), + ]), }), threadmesh_publish_dependency: Object.freeze({ eventType: "artifact-ready", materialKeys: Object.freeze(["commitSha"]), + actionEvidenceKeySets: Object.freeze([Object.freeze([])]), }), threadmesh_verify_exact_chain: Object.freeze({ eventType: "dependency-satisfied", materialKeys: Object.freeze([ "chainId", "expectedEvidenceChainHead", "expectedEvidenceChainRevision", ]), + actionEvidenceKeySets: Object.freeze([Object.freeze([])]), }), }); const ATTENTION_OFFER_ROUTE_KEYS = Object.freeze([ @@ -3343,9 +3350,12 @@ export class SqliteCoordinator { !Array.isArray(expectedActionEvidence) ? Object.keys(expectedActionEvidence).sort() : null; + const permittedEvidenceKeySets = specification?.actionEvidenceKeySets ?? []; if ( evidenceKeys === null || evidenceKeys.some((key) => - ["sourceEventId", "event", ...expectedKeys].includes(key)) + ["sourceEventId", "event", ...expectedKeys].includes(key)) || + !permittedEvidenceKeySets.some((keys) => + canonicalJson([...keys].sort()) === canonicalJson(evidenceKeys)) ) throw codedError("threadmesh_lifecycle_publication_action_mismatch"); const expectedArguments = specification ? [ execution.intent.eventId, @@ -8505,8 +8515,10 @@ export class SqliteCoordinator { const actionArgumentKeys = actionArguments && typeof actionArguments === "object" ? Object.keys(actionArguments).sort() : []; - const expectedArgumentKeys = publicationTool - ? ["sourceEventId", "event", ...publicationTool.materialKeys].sort() + const expectedArgumentKeySets = publicationTool + ? publicationTool.actionEvidenceKeySets.map((evidenceKeys) => + ["sourceEventId", "event", ...publicationTool.materialKeys, + ...evidenceKeys].sort()) : []; const message = this.db.prepare( `SELECT * FROM messages WHERE sender_incarnation_id = ? AND message_id = ?`, @@ -8540,7 +8552,8 @@ export class SqliteCoordinator { execution.intent.turnStart?.turnId !== action.turnId || execution.intent.actor.taskId !== event.sender.taskId || execution.intent.actor.incarnationId !== event.sender.incarnationId || - expectedArgumentKeys.some((key) => !actionArgumentKeys.includes(key)) || + !expectedArgumentKeySets.some((keys) => + canonicalJson(actionArgumentKeys) === canonicalJson(keys)) || ![execution.intent.eventId, execution.intent.messageId] .includes(actionArguments?.sourceEventId) || canonicalJson(actionArguments?.event) !== diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index ebb7a6b..0555f62 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -683,7 +683,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ target: actors.a, relationshipId: grants.ra.relationshipId, content: realEffects - ? "Blocking finding: artifact.txt contains BAD_COUNTEREXAMPLE." + ? "A blocking finding was reported from the detached candidate review." : "Blocking finding: the bounded candidate returns 41, not 42.", }); const fixEvent = lifecycleEvent({ @@ -839,6 +839,16 @@ export async function runCoordinatorDrivenNoPlanScenario({ dependentCheck: exactArgumentsTool(TOOLS.dependentCheck, {}), dependent: exactArgumentsTool(TOOLS.dependent, {}), }); + if (realEffects) { + const reviewerVisibleContract = canonicalJson({ + event: actionEventBody(reviewEvent), + tools: [scenarioTools.reviewRead, scenarioTools.review], + }); + if ([REAL_EFFECT_RESOURCE, REAL_EFFECT_IMPLEMENTATION.trim(), REAL_EFFECT_FIX.trim()] + .some((sealedValue) => reviewerVisibleContract.includes(sealedValue))) { + throw scenarioError("threadmesh_real_effect_review_context_not_content_blind"); + } + } const routeHandlerConfigs = Object.freeze([ Object.freeze({ ...ROUTE_HANDLER_CONFIGS[0], businessTools: Object.freeze([ scenarioTools.reviewRead, scenarioTools.review, diff --git a/test/coordinator-driven-no-plan-scenario.test.mjs b/test/coordinator-driven-no-plan-scenario.test.mjs index a75efa8..36ba8c8 100644 --- a/test/coordinator-driven-no-plan-scenario.test.mjs +++ b/test/coordinator-driven-no-plan-scenario.test.mjs @@ -200,7 +200,7 @@ test("one pump autonomously closes A to R to same-A to V to dependent", async (t deleted && absenceVerified)); }); -test("real-effects path binds Git commits, model finding, and child verifier", async (t) => { +test("real-effects path keeps reviewer context blind and binds model finding", async (t) => { const artifactsDirectory = fs.mkdtempSync( path.join(os.tmpdir(), "threadmesh-real-effects-artifacts-"), ); diff --git a/test/exact-lifecycle-binding.test.mjs b/test/exact-lifecycle-binding.test.mjs index 63d4fa3..a195cb0 100644 --- a/test/exact-lifecycle-binding.test.mjs +++ b/test/exact-lifecycle-binding.test.mjs @@ -273,6 +273,83 @@ test("lifecycle source binds to intent event/message id and rejects a third valu } }); +test("review action evidence uses one exact persisted key set", () => { + const temporary = temporaryDatabase(); + let coordinator = setup(temporary.filename); + try { + const sourceEvent = event({ + eventType: "review-failed", + content: "A blocking finding was reported from detached review.", + }); + const finding = { + resourcePath: "artifact.txt", + counterexample: "BAD_COUNTEREXAMPLE", + reason: "The detached artifact contains the reported counterexample.", + }; + const findingDigest = sha256Digest(finding); + const execution = completedExecution({ + coordinator, actor: sender, suffix: "review_evidence", + messageId: sourceEvent.messageId, eventId: "evt_v8_review_evidence", + tool: "threadmesh_report_review_finding", + argumentsValue: { + sourceEventId: "evt_v8_review_evidence", + event: actionEventBody(sourceEvent), + findingDigest, + ...finding, + }, + resultDigest: sha256Digest({ findingDigest, reproducible: true }), + }); + assert.equal(coordinator.publishLifecycleFromCompletedAction( + execution.executionId, + { + expectedTool: "threadmesh_report_review_finding", + event: sourceEvent, + expectedMaterial: { findingDigest }, + expectedActionEvidence: finding, + }, + senderPrincipal, + ).replay, false); + coordinator.close(); + coordinator = new SqliteCoordinator({ filename: temporary.filename, clock: () => NOW }); + assert.equal(coordinator.storageInfo().schemaVersion, SQLITE_SCHEMA_VERSION); + } finally { + coordinator?.close(); + temporary.cleanup(); + } + + const rejected = temporaryDatabase(); + coordinator = setup(rejected.filename); + try { + const sourceEvent = event({ eventType: "review-failed" }); + const findingDigest = sha256Digest({ finding: "bounded" }); + const execution = completedExecution({ + coordinator, actor: sender, suffix: "review_extra", + messageId: sourceEvent.messageId, eventId: "evt_v8_review_extra", + tool: "threadmesh_report_review_finding", + argumentsValue: { + sourceEventId: "evt_v8_review_extra", + event: actionEventBody(sourceEvent), + findingDigest, + unexpected: "not-an-allowed-evidence-key", + }, + resultDigest: sha256Digest({ findingDigest }), + }); + expectCode(() => coordinator.publishLifecycleFromCompletedAction( + execution.executionId, + { + expectedTool: "threadmesh_report_review_finding", + event: sourceEvent, + expectedMaterial: { findingDigest }, + expectedActionEvidence: { unexpected: "not-an-allowed-evidence-key" }, + }, + senderPrincipal, + ), "threadmesh_lifecycle_publication_action_mismatch"); + } finally { + coordinator.close(); + rejected.cleanup(); + } +}); + function turnIntentHeaderDigest(intent) { return sha256Digest({ intentId: intent.intentId,