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 c9b1e9a..2f90342 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,9 +2,9 @@ Date: 2026-09-01 -Current reviewed `main`: `c17c83755c32647eae1a4ed70a075ff56bfc84a5` +Latest attempted `main`: `e052299c3c0ff4f07f9a27dd23da421fbd95170a` -Classification: three non-completing live attempts; no real autonomous pass +Classification: four non-completing live attempts; no real autonomous pass ## Why this record exists @@ -21,10 +21,13 @@ paused run into product evidence. | 1 | Product-probe validation rejected the observed adapter-owned product shape | Preflight rejection only; the mismatch was later addressed by [#126](https://github.com/fyaic/threadmesh/pull/126) | Not started | Not asserted by this record | | 2 | `threadmesh_durable_turn_intent_evidence_invalid` rejected a numeric Codex `completedAt` value | A real adapter boundary mismatch; Unix-second normalization was later addressed by [#127](https://github.com/fyaic/threadmesh/pull/127) | Not established | Not asserted by this record | | 3 | Operator paused the run after the five role sessions had bootstrapped | Five session bootstraps; coordinator counts remained task `0`, turn intent `0`, pump dispatch `0`, and audit event `0` | Not started | The signal path did not run normal cleanup; a one-off exact operator cleanup deleted and absence-confirmed five of five owned sessions and removed the temporary SQLite, WAL, journal, and run-root resources | +| 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 | 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. None of the -three attempts produced a completed `state=blocked` event-pump gate result. +attempts 1 or 2. Attempt 3 is bootstrap and cleanup evidence only. Attempt 4 +is the first retained real kickoff/action-binding failure after all five tasks +registered. None of the four attempts produced a completed `state=blocked` +event-pump gate result. ## What the combined work established @@ -36,7 +39,12 @@ three attempts produced a completed `state=blocked` event-pump gate result. - zero-to-five partially created role sessions can be represented honestly in cleanup evidence; - the interrupted attempt's exact ownership could be reconstructed for a - one-off cleanup without deleting unrelated user tasks. + one-off cleanup without deleting unrelated user tasks; +- merged [#129](https://github.com/fyaic/threadmesh/pull/129) converts + `SIGINT`/`SIGTERM` into a cooperative shutdown checked after each role + bootstrap, after kickoff, and between event-pump dispatches; +- attempt 4 exercised the normal five-role cleanup path successfully after a + post-bootstrap live failure, without one-off operator cleanup. These are valuable fail-closed and cleanup results. They are not evidence that real Codex sessions completed the proactive lifecycle chain. @@ -59,14 +67,22 @@ cleanup behavior. The execution order was imbalanced: generalized durability, verification, Git evidence, and recovery work advanced before one uninterrupted real proactive chain was retained. -Non-mainline expansion is now frozen. The next checkpoint is one fresh run on -the existing `c17c837` surface with one user kickoff, zero runner phase or -business prompts, zero runner direct activation dispatches, exact real -session/turn/dispatch bindings, an irrelevant zero-turn control, and exact -cleanup. Only a blocker observed by that run may interrupt this checkpoint. -The missing bounded SIGINT/SIGTERM cleanup path is already observed and may be -fixed before the rerun; it must not expand into a general process-supervision -workstream. +Non-mainline expansion is now frozen. The bounded signal cleanup fix is merged, +and attempt 4 proved the normal post-bootstrap cleanup path. The next checkpoint +is one fresh run after the exact tool-contract correction, with one user +kickoff, zero runner phase or business prompts, zero runner direct activation +dispatches, exact real session/turn/dispatch bindings, an irrelevant zero-turn +control, and exact cleanup. Only a blocker observed by that run may interrupt +this checkpoint. + +Attempt 4 then exposed the next direct blocker: the live model was asked to +select `threadmesh_publish_artifact`, but the registered dynamic-tool schema +did not tell it the exact coordinator-owned event and material required by the +durable lifecycle binding. The bounded correction is to expose those already +authorized arguments through phase-specific JSON Schemas. It does not relax +the exact binding, add a new protocol field, or let final prose authorize an +effect. A fresh run, not deterministic fixtures, must show whether this closes +the blocker. A completed chain would still report `state=blocked` and `liveProductEvidence=false` while verifier custody and Git effects remain diff --git a/docs/10-planning/mainline-plan.md b/docs/10-planning/mainline-plan.md index b8d08e0..51a109a 100644 --- a/docs/10-planning/mainline-plan.md +++ b/docs/10-planning/mainline-plan.md @@ -25,17 +25,21 @@ kickoff while the runner supplies no later phase prompt or direct activation? 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. -The newer event-pump gate has not completed a live chain. Its three attempts -stopped at product-probe validation, timestamp evidence validation, and an -operator pause after five-session bootstrap respectively. The paused attempt -had zero coordinator tasks, turn intents, and pump dispatches before five of -five sessions and exact temporary resources were cleaned. +The newer event-pump gate has not completed a live chain. Its first three +attempts stopped at product-probe validation, timestamp evidence validation, +and an operator pause after five-session bootstrap. A fourth fresh attempt on +`e052299` registered all five tasks and started the sole kickoff, then failed +the exact lifecycle-action binding before any event-pump dispatch because the +dynamic-tool schema did not expose the coordinator-bound arguments. Normal +cleanup deleted and absence-confirmed all five sessions and removed the exact +temporary resources. Execute in this order: -1. Close the already observed bounded SIGINT/SIGTERM cleanup gap, then run the - current `c17c837` event-pump surface fresh. Do not add another prerequisite - that was not exposed by a live run. +1. The bounded SIGINT/SIGTERM cleanup gap is closed by #129. Expose the exact + already-authorized lifecycle arguments through phase-specific tool schemas, + then run the event-pump surface fresh. Do not add another prerequisite that + was not exposed by a live run. 2. Retain exact real session, native-turn, model-action, durable-dispatch, dependent-ordering, irrelevant-control, and cleanup evidence. The required behavioral counts are one kickoff, zero runner phase/business prompts or diff --git a/docs/10-planning/project-status.md b/docs/10-planning/project-status.md index 106e283..b6161f5 100644 --- a/docs/10-planning/project-status.md +++ b/docs/10-planning/project-status.md @@ -62,7 +62,7 @@ until this outcome is demonstrated. | M5.2 Git evidence foundation | Independent temporary bare repo and role worktrees, same-worktree implementer fix, SQLite v5 evidence chains, SQLite v6 turn/cursor recovery, SQLite v7 bound verifier-only atomic unlock, private signed-result recovery journal, five controlled coordinator-reopen checkpoints, and a preconfigured-key child verifier bound to exact commits/finding/test | Deterministic safety, persistence, model-action receipt, authority, and coordinator-reopen recovery seams passed; integrated process-crash recovery and coordinator-driven real loop pending | | M5.2 real Codex model/tool canary | Five persistent roles; four real A/R/same-A/V turns; seven model-selected tool calls; exact two-commit chain; same-A identity/worktree; dependent and irrelevant zero-turn controls; five-of-five cleanup on base `1155fc8` | Canary completed; intentionally `blocked` with `liveProductEvidence=false` because the runner submitted four phase prompts and ThreadMesh performed no lifecycle handoff | | M5.2 autonomous no-plan fixture | One user kickoff; durable SQLite attention drives A→R→same-A→V→dependent; zero fixture-runner activation dispatches or phase/business prompts; zero manual relay and polling; pump starts protected receiver turns; trusted finalization precedes dependent turn; exact cleanup | Deterministic in-process fixture at `711da66`; per-dispatch recovery durable, global chain absent; OS kill/heartbeat/live products/external verifier pending | -| M5.2 real event-pump attempts | Three attempts: product-probe rejection; timestamp-evidence rejection; operator pause after five-session bootstrap with task/turn/dispatch counts `0/0/0` | No proactive-chain evidence; attempt 3 established five-of-five session absence and exact temporary-resource cleanup only; M5.2 remains open | +| M5.2 real event-pump attempts | Four attempts: product-probe rejection; timestamp-evidence rejection; operator pause after five-session bootstrap; exact lifecycle-action mismatch after five tasks and the sole kickoff registered | No event-pump dispatch or proactive-chain evidence; attempt 4 used normal five-of-five deletion, absence verification, and exact temporary-resource cleanup; M5.2 remains open | | Gemini CLI headless | Official package 0.56.0 integrity, required flags, isolated-home cleanup | Real no-model preflight passed | | Gemini live model behavior | Exact marker script requires explicit provider key | Not authorized, not run | | Multi-product admission | One mailbox/acceptance/claim/evidence path across ACP, Codex, and Gemini fakes | Merged experimental implementation | diff --git a/src/validation/coordinator-driven-no-plan-scenario.mjs b/src/validation/coordinator-driven-no-plan-scenario.mjs index 5958181..31b5ff2 100644 --- a/src/validation/coordinator-driven-no-plan-scenario.mjs +++ b/src/validation/coordinator-driven-no-plan-scenario.mjs @@ -65,6 +65,39 @@ function tool(name, description) { }); } +function exactArgumentsTool(base, argumentsValue) { + const properties = Object.fromEntries(Object.entries(argumentsValue).map( + ([key, value]) => [key, { const: value }], + )); + return Object.freeze({ + ...base, + description: `${base.description} Use the exact coordinator-bound arguments in the schema.`, + inputSchema: Object.freeze({ + type: "object", + additionalProperties: false, + properties: Object.freeze(properties), + required: Object.freeze(Object.keys(argumentsValue)), + }), + }); +} + +function exactDecisionTool(messageId) { + return Object.freeze({ + ...REGISTERED_PEER_DECISION_TOOL, + inputSchema: Object.freeze({ + type: "object", + additionalProperties: false, + properties: Object.freeze({ + messageId: Object.freeze({ const: messageId }), + decision: Object.freeze({ + type: "string", enum: Object.freeze(["accepted", "deferred", "rejected"]), + }), + }), + required: Object.freeze(["messageId", "decision"]), + }), + }); +} + const TOOLS = Object.freeze({ implementation: tool("threadmesh_publish_artifact", "Publish the bounded implementation."), reviewRead: tool( @@ -600,6 +633,77 @@ export async function runCoordinatorDrivenNoPlanScenario({ relationshipId: grants.ar.relationshipId, content: "A prior relevant event must never be skipped for a later expected message.", }); + const kickoffArgs = { + sourceEventId: artifactEvent.messageId, + event: actionEventBody(artifactEvent), + commitSha: implementationSha, + }; + const scenarioTools = Object.freeze({ + implementation: 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, + }), + aDecision: exactDecisionTool(reviewEvent.messageId), + fixApply: exactArgumentsTool(TOOLS.fixApply, { + sourceEventId: reviewEvent.messageId, + }), + fix: exactArgumentsTool(TOOLS.fix, { + sourceEventId: reviewEvent.messageId, + event: actionEventBody(fixEvent), + commitSha: fixSha, + }), + vDecision: exactDecisionTool(fixEvent.messageId), + verifyRead: exactArgumentsTool(TOOLS.verifyRead, { + sourceEventId: fixEvent.messageId, + }), + verify: Object.freeze({ + ...TOOLS.verify, + description: `${TOOLS.verify.description} Use the exact event and chain fields in the schema; copy the revision and head returned by the preceding read tool.`, + inputSchema: Object.freeze({ + type: "object", + additionalProperties: false, + properties: Object.freeze({ + sourceEventId: Object.freeze({ const: fixEvent.messageId }), + event: Object.freeze({ const: actionEventBody(verifiedEvent) }), + chainId: Object.freeze({ const: "chain_coordinator_driven_no_plan" }), + expectedEvidenceChainRevision: Object.freeze({ type: "integer", minimum: 0 }), + expectedEvidenceChainHead: Object.freeze({ + type: "string", pattern: "^sha256:[a-f0-9]{64}$", + }), + }), + required: Object.freeze([ + "sourceEventId", "event", "chainId", "expectedEvidenceChainRevision", + "expectedEvidenceChainHead", + ]), + }), + }), + dependentDecision: exactDecisionTool(verifiedEvent.messageId), + dependentCheck: exactArgumentsTool(TOOLS.dependentCheck, {}), + dependent: exactArgumentsTool(TOOLS.dependent, {}), + }); + const routeHandlerConfigs = Object.freeze([ + Object.freeze({ ...ROUTE_HANDLER_CONFIGS[0], businessTools: Object.freeze([ + scenarioTools.reviewRead, scenarioTools.review, + ]) }), + Object.freeze({ ...ROUTE_HANDLER_CONFIGS[1], businessTools: Object.freeze([ + scenarioTools.fixApply, scenarioTools.fix, + ]) }), + Object.freeze({ ...ROUTE_HANDLER_CONFIGS[2], businessTools: Object.freeze([ + scenarioTools.verifyRead, scenarioTools.verify, + ]) }), + Object.freeze({ ...ROUTE_HANDLER_CONFIGS[3], businessTools: Object.freeze([ + scenarioTools.dependentCheck, scenarioTools.dependent, + ]) }), + Object.freeze({ ...ROUTE_HANDLER_CONFIGS[4], businessTools: Object.freeze([ + scenarioTools.reviewRead, scenarioTools.review, + ]) }), + ]); let refs = {}; let runtime; @@ -696,13 +800,13 @@ export async function runCoordinatorDrivenNoPlanScenario({ refs.a = await runtime.createRole({ role: "a", cwd: artifactsDirectory, tools: [ - TOOLS.implementation, REGISTERED_PEER_DECISION_TOOL, - TOOLS.fixApply, TOOLS.fix, + scenarioTools.implementation, scenarioTools.aDecision, + scenarioTools.fixApply, scenarioTools.fix, ], phaseTools: { - "user-kickoff": [TOOLS.implementation], - "receiver-decision": [REGISTERED_PEER_DECISION_TOOL], - "same-a-fix": [TOOLS.fixApply, TOOLS.fix], + "user-kickoff": [scenarioTools.implementation], + "receiver-decision": [scenarioTools.aDecision], + "same-a-fix": [scenarioTools.fixApply, scenarioTools.fix], }, protectedPhases: { "receiver-decision": "receiver-decision", "same-a-fix": "admitted-tool", @@ -713,10 +817,10 @@ export async function runCoordinatorDrivenNoPlanScenario({ throwIfShutdownRequested(signal); refs.r = await runtime.createRole({ role: "r", cwd: artifactsDirectory, - tools: [REGISTERED_PEER_DECISION_TOOL, TOOLS.reviewRead, TOOLS.review], + tools: [scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review], phaseTools: { - "receiver-decision": [REGISTERED_PEER_DECISION_TOOL], - "r-review": [TOOLS.reviewRead, TOOLS.review], + "receiver-decision": [scenarioTools.rDecision], + "r-review": [scenarioTools.reviewRead, scenarioTools.review], }, protectedPhases: { "receiver-decision": "receiver-decision", "r-review": "admitted-tool", @@ -727,10 +831,10 @@ export async function runCoordinatorDrivenNoPlanScenario({ throwIfShutdownRequested(signal); refs.v = await runtime.createRole({ role: "v", cwd: artifactsDirectory, - tools: [REGISTERED_PEER_DECISION_TOOL, TOOLS.verifyRead, TOOLS.verify], + tools: [scenarioTools.vDecision, scenarioTools.verifyRead, scenarioTools.verify], phaseTools: { - "receiver-decision": [REGISTERED_PEER_DECISION_TOOL], - "v-verify": [TOOLS.verifyRead, TOOLS.verify], + "receiver-decision": [scenarioTools.vDecision], + "v-verify": [scenarioTools.verifyRead, scenarioTools.verify], }, protectedPhases: { "receiver-decision": "receiver-decision", "v-verify": "admitted-tool", @@ -742,11 +846,11 @@ export async function runCoordinatorDrivenNoPlanScenario({ refs.dependent = await runtime.createRole({ role: "dependent", cwd: artifactsDirectory, tools: [ - REGISTERED_PEER_DECISION_TOOL, TOOLS.dependentCheck, TOOLS.dependent, + scenarioTools.dependentDecision, scenarioTools.dependentCheck, scenarioTools.dependent, ], phaseTools: { - "receiver-decision": [REGISTERED_PEER_DECISION_TOOL], - "dependent-gated-activation": [TOOLS.dependentCheck, TOOLS.dependent], + "receiver-decision": [scenarioTools.dependentDecision], + "dependent-gated-activation": [scenarioTools.dependentCheck, scenarioTools.dependent], }, protectedPhases: { "receiver-decision": "receiver-decision", @@ -758,7 +862,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ throwIfShutdownRequested(signal); refs.irrelevant = await runtime.createRole({ role: "irrelevant", cwd: artifactsDirectory, - tools: [REGISTERED_PEER_DECISION_TOOL, TOOLS.reviewRead, TOOLS.review], + tools: [scenarioTools.rDecision, scenarioTools.reviewRead, scenarioTools.review], instructions: "Remain idle unless coordinator attention is relevant.", scenarioId: "coordinator_driven_no_plan", }); @@ -877,7 +981,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ cwd: artifactsDirectory, ref: refs.r, routes: [{ - handlerId: ROUTE_HANDLER_CONFIGS[0].handlerId, + handlerId: routeHandlerConfigs[0].handlerId, eventType: "artifact-ready", subscribedEventTypes: ["artifact-ready"], grant: grants.ar, @@ -885,7 +989,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ targetTask: { ...actors.r, objectiveVersion: 1 }, now: NOW, businessPhase: "r-review", - businessTools: [TOOLS.reviewRead, TOOLS.review], + businessTools: routeHandlerConfigs[0].businessTools, async onBusinessToolCall({ tool: selectedTool }) { if (selectedTool === TOOLS.reviewRead.name) { return { artifactDigest: digest("admitted-review-artifact") }; @@ -922,7 +1026,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ cwd: artifactsDirectory, ref: refs.a, routes: [{ - handlerId: ROUTE_HANDLER_CONFIGS[1].handlerId, + handlerId: routeHandlerConfigs[1].handlerId, eventType: "review-failed", subscribedEventTypes: ["review-failed"], grant: grants.ra, @@ -930,7 +1034,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ targetTask: { ...actors.a, objectiveVersion: 1 }, now: NOW, businessPhase: "same-a-fix", - businessTools: [TOOLS.fixApply, TOOLS.fix], + businessTools: routeHandlerConfigs[1].businessTools, async onBusinessToolCall({ tool: selectedTool }) { if (selectedTool === TOOLS.fixApply.name) { return { appliedFindingDigest: findingDigest }; @@ -967,7 +1071,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ cwd: artifactsDirectory, ref: refs.v, routes: [{ - handlerId: ROUTE_HANDLER_CONFIGS[2].handlerId, + handlerId: routeHandlerConfigs[2].handlerId, eventType: "artifact-ready", subscribedEventTypes: ["artifact-ready"], grant: grants.av, @@ -975,7 +1079,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ targetTask: { ...actors.v, objectiveVersion: 1 }, now: NOW, businessPhase: "v-verify", - businessTools: [TOOLS.verifyRead, TOOLS.verify], + businessTools: routeHandlerConfigs[2].businessTools, async onBusinessToolCall({ tool: selectedTool }) { if (selectedTool === TOOLS.verifyRead.name) { return { evidenceHead, evidenceRevision }; @@ -1010,7 +1114,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ cwd: artifactsDirectory, ref: refs.dependent, routes: [{ - handlerId: ROUTE_HANDLER_CONFIGS[3].handlerId, + handlerId: routeHandlerConfigs[3].handlerId, eventType: "dependency-satisfied", subscribedEventTypes: ["dependency-satisfied"], grant: grants.vd, @@ -1023,7 +1127,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ }, now: NOW, businessPhase: "dependent-gated-activation", - businessTools: [TOOLS.dependentCheck, TOOLS.dependent], + businessTools: routeHandlerConfigs[3].businessTools, async afterAdmissionPrepared() { verifiedActivationOrder.push("dependent-admission-prepared"); if (injectPreverifiedTamper === "state-only") { @@ -1158,7 +1262,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ cwd: artifactsDirectory, ref: refs.irrelevant, routes: [{ - handlerId: ROUTE_HANDLER_CONFIGS[4].handlerId, + handlerId: routeHandlerConfigs[4].handlerId, eventType: "artifact-ready", subscribedEventTypes: ["review-failed"], grant: grants.ai, @@ -1166,17 +1270,12 @@ export async function runCoordinatorDrivenNoPlanScenario({ targetTask: { ...actors.irrelevant, objectiveVersion: 1 }, now: NOW, businessPhase: "irrelevant-never-runs", - businessTools: [TOOLS.reviewRead, TOOLS.review], + businessTools: routeHandlerConfigs[4].businessTools, async onBusinessToolCall() { throw new Error("irrelevant business turn ran"); }, async onLifecyclePublication() { throw new Error("irrelevant publication ran"); }, }], }); - const kickoffArgs = { - sourceEventId: artifactEvent.messageId, - event: actionEventBody(artifactEvent), - commitSha: implementationSha, - }; const kickoff = await runKickoff({ coordinator, runtime, actor: actors.a, ref: refs.a, event: artifactEvent, args: kickoffArgs, @@ -1545,7 +1644,7 @@ export async function runCoordinatorDrivenNoPlanScenario({ "cross-process-os-kill-and-long-turn-lease-heartbeat", "global-selection-chain", ], - routeHandlerConfigs: ROUTE_HANDLER_CONFIGS, + routeHandlerConfigs, executedHandlerIds: selectionBindings .filter(({ kind }) => kind === "coordinator-activation") .map(({ handlerId }) => handlerId), diff --git a/test/coordinator-driven-no-plan-scenario.test.mjs b/test/coordinator-driven-no-plan-scenario.test.mjs index 06d763e..f470349 100644 --- a/test/coordinator-driven-no-plan-scenario.test.mjs +++ b/test/coordinator-driven-no-plan-scenario.test.mjs @@ -327,13 +327,14 @@ test("a bounded shutdown after role bootstrap cleans every created role and run const created = []; const deleted = []; const runtime = { - async createRole({ role }) { + async createRole(options) { + const { role } = options; const ref = { kind: "codex-app-server", threadId: `shutdown-thread-${role}`, snapshotDigest: sha256Digest({ role, boundary: "shutdown" }), }; - created.push({ role, ref }); + created.push({ role, ref, options }); if (created.length === 5) controller.abort(); return ref; }, @@ -370,6 +371,18 @@ test("a bounded shutdown after role bootstrap cleans every created role and run assert.equal(failure?.code, "threadmesh_coordinator_driven_shutdown_requested"); assert.deepEqual(created.map(({ role }) => role), ["a", "r", "v", "dependent", "irrelevant"]); assert.deepEqual(deleted.map(({ role }) => role), ["irrelevant", "dependent", "v", "r", "a"]); + const aKickoffSchema = created[0].options.phaseTools["user-kickoff"][0].inputSchema; + assert.equal(aKickoffSchema.additionalProperties, false); + assert.equal(aKickoffSchema.properties.sourceEventId.const, "msg_no_plan_artifact_0001"); + assert.equal(aKickoffSchema.properties.commitSha.const, "3".repeat(40)); + const reviewSchema = created[1].options.phaseTools["r-review"][1].inputSchema; + assert.equal(reviewSchema.properties.event.const.eventType, "review-failed"); + assert.match(reviewSchema.properties.findingDigest.const, /^sha256:[a-f0-9]{64}$/u); + const verifySchema = created[2].options.phaseTools["v-verify"][1].inputSchema; + assert.equal(verifySchema.properties.chainId.const, "chain_coordinator_driven_no_plan"); + assert.equal(verifySchema.properties.expectedEvidenceChainHead.const, undefined); + assert.equal(verifySchema.properties.expectedEvidenceChainHead.pattern, + "^sha256:[a-f0-9]{64}$"); assert.equal(failure.cleanup?.complete, true); assert.equal(failure.cleanup?.roles.length, 5); assert.equal(failure.cleanup?.remainingJournalCount, 0);