From 9871d2595633309646f7b40fcc3f8e78379bc4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E8=BE=BE=E5=A5=87?= <> Date: Mon, 27 Jul 2026 07:51:47 +0800 Subject: [PATCH 1/4] feat(legal): add immutable source repair transaction --- .../legal/plugins/legal-coverage/hook.mjs | 84 ++- .../plugins/legal-coverage/hooks/hooks.json | 2 +- .../legal-coverage/scripts/legal-coverage.mjs | 20 +- .../scripts/lib/legal-coverage.mjs | 551 +++++++++++++++- .../legal-coverage-plugin-runtime.spec.ts | 368 +++++++++++ tests/products/legal-coverage.spec.ts | 592 +++++++++++------- 6 files changed, 1382 insertions(+), 235 deletions(-) diff --git a/products/legal/plugins/legal-coverage/hook.mjs b/products/legal/plugins/legal-coverage/hook.mjs index 63c55830..612f8f4b 100644 --- a/products/legal/plugins/legal-coverage/hook.mjs +++ b/products/legal/plugins/legal-coverage/hook.mjs @@ -73,8 +73,14 @@ try { const active = !isSubagent && (sessionState?.active === true || await pathExists(input.cwd, `${STATE_DIRECTORY}/config.json`) || await pathExists(input.cwd, PROOF_PATH)); - if (active && input.hookEventName === "PostToolUse" && input.toolName === "read_file") { - const preparation = await advanceRepairPreparation(input.cwd, sessionState, input.toolInput); + if (active && input.hookEventName === "PostToolUse" + && ["read_file", "write_file"].includes(input.toolName)) { + const preparation = await advanceRepairPreparation( + input.cwd, + sessionState, + input.toolName, + input.toolInput, + ); if (preparation.changed) { await writeSessionState(input.cwd, sessionPath, { ...sessionState, @@ -199,7 +205,14 @@ async function dynamicWorkItems(workspaceRoot, result) { const first = result.errors[0]; if (first?.phase === "sources" && first.code === "source_pending") { const plan = await pendingSourceReviewPlan(workspaceRoot, { expectedStateHash: result.stateHash }); - return ["delegated", "main-agent-merge", "main-agent-propose", "main-agent-apply"].includes(plan.mode) ? plan : undefined; + return [ + "delegated", + "main-agent-merge", + "main-agent-propose", + "main-agent-apply", + "main-agent-repair", + "main-agent-repair-apply", + ].includes(plan.mode) ? plan : undefined; } if (first?.phase === "coverage") { return nextCoverageBatch(workspaceRoot, { limit: 4, maxSerializedBytes: 2048, validationResult: result }); @@ -314,7 +327,19 @@ function advanceProgressState(sessionState, digest, current, checkpointDigest, r function legalProgressCheckpointDigest(workItems) { let checkpoint; - if (workItems?.group === "source-fragment-apply" + if (workItems?.appliedRepair + && validStateHash(workItems.appliedRepair.stateHash) + && validStateHash(workItems.appliedRepair.repairSha256) + && Array.isArray(workItems.appliedRepair.sourceIds)) { + const sourceIds = workItems.appliedRepair.sourceIds.filter(nonEmptyString).slice(0, 12).sort(); + if (sourceIds.length === 0 || sourceIds.length !== workItems.appliedRepair.sourceIds.length) return undefined; + checkpoint = { + kind: "source-fragment-repair-applied", + stateHash: workItems.appliedRepair.stateHash, + sourceIds, + repairSha256: workItems.appliedRepair.repairSha256, + }; + } else if (workItems?.group === "source-fragment-apply" && workItems.proposal?.validated === true && validStateHash(workItems.proposal.expectedStateHash) && Array.isArray(workItems.proposal.sourceIds)) { @@ -416,7 +441,22 @@ function advanceHandoffState(sessionState, checkpoint) { function legalRepairCheckpoint(workItems) { let checkpoint; - if (workItems?.group === "source-fragment-propose" + if (workItems?.group === "source-fragment-repair" + && hasRepairFeedback(workItems.repair) + && validStateHash(workItems.repair.expectedStateHash) + && validStateHash(workItems.repair.proposalSha256) + && validStateHash(workItems.repair.diagnosticSha256) + && Array.isArray(workItems.repair.sourceIds)) { + const sourceIds = workItems.repair.sourceIds.filter(nonEmptyString).slice(0, 12).sort(); + if (sourceIds.length === 0 || sourceIds.length !== workItems.repair.sourceIds.length) return undefined; + checkpoint = { + kind: "source-fragment-immutable-repair", + expectedStateHash: workItems.repair.expectedStateHash, + proposalSha256: workItems.repair.proposalSha256, + diagnosticSha256: workItems.repair.diagnosticSha256, + sourceIds, + }; + } else if (workItems?.group === "source-fragment-propose" && hasRepairFeedback(workItems.proposal) && validStateHash(workItems.proposal.expectedStateHash) && Array.isArray(workItems.proposal.sourceIds)) { @@ -461,7 +501,11 @@ function legalRepairCheckpoint(workItems) { function legalRepairTarget(workItems, repairDigest) { if (!validStateHash(repairDigest)) return undefined; let path; - if (workItems?.group === "source-fragment-propose" && hasRepairFeedback(workItems.proposal)) { + let preparationTool = "read_file"; + if (workItems?.group === "source-fragment-repair" && hasRepairFeedback(workItems.repair)) { + path = workItems.repair?.path; + preparationTool = "write_file"; + } else if (workItems?.group === "source-fragment-propose" && hasRepairFeedback(workItems.proposal)) { path = workItems.proposal?.path; } else if (workItems?.group === "matrix-pending-selection" && hasRepairFeedback(workItems.selection)) { path = workItems.selection?.path; @@ -471,10 +515,10 @@ function legalRepairTarget(workItems, repairDigest) { path = workItems.proposal?.path; } if (typeof path !== "string" || path.length === 0 || path.length > 2048) return undefined; - return { repairDigest, path }; + return { repairDigest, path, preparationTool }; } -async function advanceRepairPreparation(workspaceRoot, sessionState, toolInput) { +async function advanceRepairPreparation(workspaceRoot, sessionState, toolName, toolInput) { const ordinal = repairPreparationOrdinal(sessionState); const seenCheckpointDigests = Array.isArray(sessionState?.repairPreparationCheckpointDigests) ? sessionState.repairPreparationCheckpointDigests.filter(validStateHash) @@ -484,14 +528,27 @@ async function advanceRepairPreparation(workspaceRoot, sessionState, toolInput) const inputPath = toolInput && typeof toolInput === "object" && !Array.isArray(toolInput) ? toolInput.file_path : undefined; - if (!target || typeof inputPath !== "string" || inputPath.length === 0 || inputPath.length > 2048) { + if (!target || toolName !== target.preparationTool + || typeof inputPath !== "string" || inputPath.length === 0 || inputPath.length > 2048) { return { ordinal, seenCheckpointDigests, changed: false }; } const targetPath = await resolveSafeWorkspacePath(workspaceRoot, target.path, { allowMissing: true }); const readPath = await resolveSafeWorkspacePath(workspaceRoot, inputPath, { allowMissing: true }); if (targetPath !== readPath) return { ordinal, seenCheckpointDigests, changed: false }; + if (toolName === "write_file") { + let info; + try { + info = await stat(targetPath); + } catch (error) { + if (error?.code === "ENOENT") return { ordinal, seenCheckpointDigests, changed: false }; + throw error; + } + if (!info.isFile() || info.size <= 0 || info.size > 24576) { + return { ordinal, seenCheckpointDigests, changed: false }; + } + } const preparationDigest = checkpointDigest({ - kind: "repair-target-read", + kind: `repair-target-${toolName === "write_file" ? "write" : "read"}`, repairDigest: target.repairDigest, }); if (!preparationDigest || seenCheckpointDigests.includes(preparationDigest)) { @@ -516,14 +573,17 @@ function parseRepairTarget(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; if (!validStateHash(value.repairDigest)) return undefined; if (typeof value.path !== "string" || value.path.length === 0 || value.path.length > 2048) return undefined; - return { repairDigest: value.repairDigest, path: value.path }; + const preparationTool = value.preparationTool === "write_file" ? "write_file" : "read_file"; + return { repairDigest: value.repairDigest, path: value.path, preparationTool }; } function sameRepairTarget(left, right) { const parsedLeft = parseRepairTarget(left); const parsedRight = parseRepairTarget(right); if (!parsedLeft || !parsedRight) return parsedLeft === undefined && parsedRight === undefined; - return parsedLeft.repairDigest === parsedRight.repairDigest && parsedLeft.path === parsedRight.path; + return parsedLeft.repairDigest === parsedRight.repairDigest + && parsedLeft.path === parsedRight.path + && parsedLeft.preparationTool === parsedRight.preparationTool; } function hasRepairFeedback(value) { diff --git a/products/legal/plugins/legal-coverage/hooks/hooks.json b/products/legal/plugins/legal-coverage/hooks/hooks.json index 9c8f0c16..3c8dc016 100644 --- a/products/legal/plugins/legal-coverage/hooks/hooks.json +++ b/products/legal/plugins/legal-coverage/hooks/hooks.json @@ -21,7 +21,7 @@ ], "PostToolUse": [ { - "matcher": "read_file", + "matcher": "read_file|write_file", "hooks": [ { "type": "command", diff --git a/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs b/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs index 242a7f1b..4f9ec5dc 100644 --- a/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs +++ b/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs @@ -7,6 +7,7 @@ import { applyMatrixProposal, applyMatrixSelection, applySourceMergeProposal, + applySourceMergeRepair, bootstrapSourcesFromManifest, coverageBatchSchema, ensureWorkspace, @@ -180,6 +181,23 @@ if (command === "init") { })); process.exitCode = 1; } +} else if (command === "source-repair-apply") { + try { + const result = await applySourceMergeRepair(workspaceRoot, { + repairPath: readOption(args, "--input-file"), + repairSha256: readOption(args, "--repair-sha256"), + }); + console.log(JSON.stringify(result, null, 2)); + process.exitCode = 0; + } catch (error) { + console.error(JSON.stringify({ + error: { + code: typeof error?.code === "string" ? error.code : "source_repair_apply_failed", + message: error instanceof Error ? error.message : String(error), + }, + })); + process.exitCode = 1; + } } else if (command === "matrix-selection-apply") { try { const result = await applyMatrixSelection(workspaceRoot, { @@ -276,7 +294,7 @@ if (command === "init") { console.log(JSON.stringify(result, null, 2)); process.exitCode = result.passed || command === "status" ? 0 : 2; } else { - console.error("Usage: legal-coverage.mjs [--workspace PATH] [--from-manifest] [--name data-contracts|issue-rules] [--checkpoint PATH] [--expected-state-hash HASH] [--fragment PATH] [--receipt-sha256 HASH] [--source-id ID] [--phase coverage] [--input-file PATH] [--proposal-sha256 HASH] [--limit 1..12] [--max-bytes 1024..24576] [--input PATH|--input-from-manifest] [--deliverable ID=PATH] [--jurisdiction NAME] [--basis-date DATE] [--allow-no-material-facts] [--write-proof]"); + console.error("Usage: legal-coverage.mjs [--workspace PATH] [--from-manifest] [--name data-contracts|issue-rules] [--checkpoint PATH] [--expected-state-hash HASH] [--fragment PATH] [--receipt-sha256 HASH] [--source-id ID] [--phase coverage] [--input-file PATH] [--proposal-sha256 HASH] [--repair-sha256 HASH] [--limit 1..12] [--max-bytes 1024..24576] [--input PATH|--input-from-manifest] [--deliverable ID=PATH] [--jurisdiction NAME] [--basis-date DATE] [--allow-no-material-facts] [--write-proof]"); process.exitCode = 1; } diff --git a/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs b/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs index cef76cf9..77b0711f 100644 --- a/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs +++ b/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs @@ -65,14 +65,19 @@ const MATRIX_STATUSES = new Set(["pending", "complete", "not-applicable"]); const TEXT_EXTENSIONS = new Set([".md", ".txt", ".html", ".htm", ".csv"]); const SOURCE_REVIEW_FRAGMENT_PATTERN = /^source-review-[a-f0-9]{12}\.json$/u; const SOURCE_MERGE_READINESS_PATTERN = /^source-merge-ready-[a-f0-9]{12}\.json$/u; +const SOURCE_MERGE_REPAIR_PATTERN = /^source-repair-[a-f0-9]{12}\.json$/u; +const SOURCE_MERGE_REPAIR_APPLIED_PATTERN = /^source-repair-applied-[a-f0-9]{12}\.json$/u; const SOURCE_REVIEW_FRAGMENT_TYPE = "legal-evidence-source-batch-review"; const SOURCE_MERGE_READINESS_TYPE = "legal-source-merge-readiness"; +const SOURCE_MERGE_REPAIR_APPLIED_TYPE = "legal-source-repair-applied"; const SOURCE_REVIEW_FRAGMENT_MAX_BYTES = 262144; const SOURCE_MERGE_READINESS_MAX_BYTES = 4096; const SOURCE_REVIEW_MERGE_MAX_SOURCES = 4; const SOURCE_MERGE_MAX_FACTS = 32; const SOURCE_MERGE_MAX_VALIDATION_DIAGNOSTICS = SOURCE_MERGE_MAX_FACTS + SOURCE_REVIEW_MERGE_MAX_SOURCES; const SOURCE_MERGE_REPAIR_SLICE_MAX_BYTES = 196608; +const SOURCE_MERGE_REPAIR_MAX_BYTES = 24576; +const SOURCE_MERGE_REPAIR_APPLIED_MAX_BYTES = 4096; const SOURCE_REVIEW_MATERIALITIES = new Set(["non-material", "material", "critical", "uncertain"]); const MATRIX_TRANSACTION_DIRECTORY = `${STATE_DIRECTORY}/matrix-transactions`; const MATRIX_SELECTION_PATTERN = /^matrix-selection-[a-f0-9]{12}\.json$/u; @@ -205,13 +210,20 @@ export async function pendingSourceReviewPlan(workspaceRoot, options = {}) { ? options.expectedStateHash : (await validateWorkspace({ workspaceRoot, writeProof: false })).stateHash; const proposalPlan = sourceMergeProposalPlanFor(mergePlan, expectedStateHash); + const appliedRepair = await currentSourceMergeRepairAppliedReceipt( + workspaceRoot, + expectedStateHash, + sourceRows, + ); const readinessReceipt = await validSourceMergeReadinessReceipt(workspaceRoot, proposalPlan); - if (!readinessReceipt) return proposalPlan; + if (!readinessReceipt) return withAppliedRepairCheckpoint(proposalPlan, appliedRepair); const proposePlan = sourceMergeProposePlanFor(proposalPlan, readinessReceipt); const proposalReceipt = await validSourceMergeProposalReceipt(workspaceRoot, proposePlan, receipts, loaded.state); - if (proposalReceipt?.valid) return sourceMergeApplyPlanFor(proposePlan, proposalReceipt); + if (proposalReceipt?.valid) { + return withAppliedRepairCheckpoint(sourceMergeApplyPlanFor(proposePlan, proposalReceipt), appliedRepair); + } if (proposalReceipt?.error) { - return { + const rejectedPlan = { ...proposePlan, proposal: { ...proposePlan.proposal, @@ -224,8 +236,32 @@ export async function pendingSourceReviewPlan(workspaceRoot, options = {}) { : {}), }, }; + const repairPlan = sourceMergeRepairPlanFor(rejectedPlan); + if (!repairPlan) return withAppliedRepairCheckpoint(rejectedPlan, appliedRepair); + const repairReceipt = await validSourceMergeRepairReceipt( + workspaceRoot, + repairPlan, + receipts, + loaded.state, + ); + if (repairReceipt?.valid) { + return withAppliedRepairCheckpoint( + sourceMergeRepairApplyPlanFor(repairPlan, repairReceipt), + appliedRepair, + ); + } + if (repairReceipt?.error) { + return withAppliedRepairCheckpoint({ + ...repairPlan, + repair: { + ...repairPlan.repair, + validationError: repairReceipt.error, + }, + }, appliedRepair); + } + return withAppliedRepairCheckpoint(repairPlan, appliedRepair); } - return proposePlan; + return withAppliedRepairCheckpoint(proposePlan, appliedRepair); } function sourceReviewPlanFor(sourceRows, options = {}) { @@ -586,6 +622,111 @@ function sourceMergeApplyPlanFor(proposalPlan, proposalReceipt) { }; } +function sourceMergeRepairPlanFor(rejectedPlan) { + const repairSlice = rejectedPlan.proposal?.repairSlice; + const diagnostics = repairSlice?.diagnostics; + const rejectedFacts = Array.isArray(repairSlice?.rejectedFacts) ? repairSlice.rejectedFacts : []; + const diagnosticItems = Array.isArray(diagnostics?.items) ? diagnostics.items : []; + if (rejectedFacts.length === 0 || diagnosticItems.length === 0 + || diagnosticItems.some((item) => !Number.isSafeInteger(item?.factNumber))) return undefined; + const factNumbers = rejectedFacts.map((item) => item.factNumber); + if (new Set(factNumbers).size !== factNumbers.length + || diagnosticItems.some((item) => !factNumbers.includes(item.factNumber))) return undefined; + const diagnosticIdentity = diagnosticItems.map((item) => ({ + factNumber: item.factNumber, + code: item.code, + })); + const diagnosticSha256 = sha256(Buffer.from(stableStringify(diagnosticIdentity))); + const digest = sha256([ + rejectedPlan.proposal.expectedStateHash, + rejectedPlan.proposal.path, + repairSlice.proposal.sha256, + diagnosticSha256, + ].join("\0")).slice(0, 12); + const repairPath = `${STATE_DIRECTORY}/fragments/source-repair-${digest}.json`; + const appliedReceiptPath = `${STATE_DIRECTORY}/fragments/source-repair-applied-${digest}.json`; + const { preparedSlice: _preparedSlice, ...boundedPlan } = rejectedPlan; + const proposal = { ...boundedPlan.proposal }; + delete proposal.template; + delete proposal.validationError; + delete proposal.validationDiagnostics; + delete proposal.repairSlice; + return { + ...boundedPlan, + group: "source-fragment-repair", + mode: "main-agent-repair", + proposal, + repair: { + repairRequired: true, + path: repairPath, + expectedStateHash: rejectedPlan.proposal.expectedStateHash, + proposalPath: rejectedPlan.proposal.path, + proposalSha256: repairSlice.proposal.sha256, + sourceIds: [...rejectedPlan.proposal.sourceIds], + diagnosticSha256, + appliedReceiptPath, + limits: { + maxOperations: rejectedFacts.length, + maxSerializedBytes: SOURCE_MERGE_REPAIR_MAX_BYTES, + }, + template: { + schemaVersion: 1, + phase: "sources", + group: "source-fragment-repair", + expectedStateHash: rejectedPlan.proposal.expectedStateHash, + proposalPath: rejectedPlan.proposal.path, + proposalSha256: repairSlice.proposal.sha256, + diagnosticSha256, + operations: rejectedFacts.map((item) => ({ + factNumber: item.factNumber, + action: "replace", + fact: item.fact, + })), + }, + repairSlice: { + schemaVersion: 1, + diagnostics, + rejectedFacts, + sourceContext: repairSlice.sourceContext, + }, + }, + }; +} + +function sourceMergeRepairApplyPlanFor(repairPlan, repairReceipt) { + const { preparedSlice: _preparedSlice, ...boundedPlan } = repairPlan; + const proposal = { ...boundedPlan.proposal }; + delete proposal.template; + delete proposal.validationError; + delete proposal.validationDiagnostics; + delete proposal.repairSlice; + return { + ...boundedPlan, + group: "source-fragment-repair-apply", + mode: "main-agent-repair-apply", + proposal, + repair: { + path: repairPlan.repair.path, + expectedStateHash: repairPlan.repair.expectedStateHash, + proposalPath: repairPlan.repair.proposalPath, + proposalSha256: repairPlan.repair.proposalSha256, + sourceIds: [...repairPlan.repair.sourceIds], + diagnosticSha256: repairPlan.repair.diagnosticSha256, + appliedReceiptPath: repairPlan.repair.appliedReceiptPath, + repairSha256: repairReceipt.sha256, + validated: true, + operationCount: repairReceipt.operationCount, + factCount: repairReceipt.factCount, + noMaterialFactCount: repairReceipt.noMaterialFactCount, + transactionBytes: repairReceipt.transactionBytes, + }, + }; +} + +function withAppliedRepairCheckpoint(plan, appliedRepair) { + return appliedRepair ? { ...plan, appliedRepair } : plan; +} + function boundedReceiptSourceIds(receipt, maxRecords, maxSerializedBytes) { const rowsById = new Map(receipt.sourceRows.map((row) => [row.sourceId, row])); const selected = []; @@ -852,6 +993,230 @@ function sourceMergeRepairSliceFor(patch, bytes, plan, receipt, validationDiagno : undefined; } +async function validSourceMergeRepairReceipt(workspaceRoot, plan, receipts, state) { + let repairBytes; + try { + const repairPath = await resolveSafeWorkspacePath(workspaceRoot, plan.repair.path); + repairBytes = await readFile(repairPath); + if (repairBytes.byteLength > plan.repair.limits.maxSerializedBytes) { + throw batchError( + "source_repair_byte_limit", + `Source repair transaction is ${repairBytes.byteLength} bytes; maximum is ${plan.repair.limits.maxSerializedBytes}.`, + ); + } + const originalPath = await resolveSafeWorkspacePath(workspaceRoot, plan.repair.proposalPath); + const proposalBytes = await readFile(originalPath); + if (sha256(proposalBytes) !== plan.repair.proposalSha256) { + throw batchError("source_repair_proposal_changed", "The rejected source proposal changed after repair preparation."); + } + const transaction = JSON.parse(repairBytes.toString("utf8")); + const proposal = JSON.parse(proposalBytes.toString("utf8")); + const receipt = receipts.find((candidate) => candidate.fragmentPath === plan.proposal.fragmentPath + && candidate.receiptSha256 === plan.proposal.receiptSha256); + if (!receipt) throw batchError("source_fragment_receipt_invalid", "The source fragment receipt is no longer valid."); + const validated = validateSourceMergeRepairTransaction( + transaction, + plan, + proposal, + receipt, + state, + ); + return { + valid: true, + sha256: sha256(repairBytes), + operationCount: validated.operationCount, + factCount: validated.normalized.facts.length, + noMaterialFactCount: validated.normalized.noMaterialFacts.length, + transactionBytes: validated.normalized.transactionBytes, + }; + } catch (error) { + if (error?.code === "ENOENT") return undefined; + return { + valid: false, + error: { + code: typeof error?.code === "string" ? error.code : "source_repair_invalid", + message: errorMessage(error), + }, + }; + } +} + +function validateSourceMergeRepairTransaction(transaction, plan, proposal, receipt, state) { + if (!isRecord(transaction)) { + throw batchError("source_repair_not_object", "Source repair transaction must be a JSON object."); + } + const expectedKeys = [ + "diagnosticSha256", + "expectedStateHash", + "group", + "operations", + "phase", + "proposalPath", + "proposalSha256", + "schemaVersion", + ]; + if (!hasOnlyKeys(transaction, expectedKeys)) { + throw batchError("source_repair_keys_invalid", `Source repair transaction must contain only: ${expectedKeys.join(", ")}.`); + } + if (transaction.schemaVersion !== 1 || transaction.phase !== "sources" + || transaction.group !== "source-fragment-repair") { + throw batchError("source_repair_identity_invalid", "Source repair requires schemaVersion 1, phase sources, and group source-fragment-repair."); + } + if (transaction.expectedStateHash !== plan.repair.expectedStateHash + || transaction.proposalPath !== plan.repair.proposalPath + || transaction.proposalSha256 !== plan.repair.proposalSha256 + || transaction.diagnosticSha256 !== plan.repair.diagnosticSha256) { + throw batchError("source_repair_scope_mismatch", "Source repair state, proposal, and diagnostic identities must match the injected transaction."); + } + const rejectedFacts = plan.repair.repairSlice.rejectedFacts; + const rejectedNumbers = rejectedFacts.map((item) => item.factNumber).sort((left, right) => left - right); + if (!Array.isArray(transaction.operations) + || transaction.operations.length !== rejectedNumbers.length + || transaction.operations.length > plan.repair.limits.maxOperations) { + throw batchError("source_repair_operation_count_invalid", "Source repair requires exactly one operation for every rejected fact."); + } + const operationNumbers = transaction.operations + .map((operation) => operation?.factNumber) + .sort((left, right) => left - right); + if (operationNumbers.some((number) => !Number.isSafeInteger(number)) + || new Set(operationNumbers).size !== operationNumbers.length + || JSON.stringify(operationNumbers) !== JSON.stringify(rejectedNumbers)) { + throw batchError("source_repair_fact_scope_invalid", "Source repair operations must cover only, and every, rejected fact number exactly once."); + } + if (!Array.isArray(proposal?.facts)) { + throw batchError("source_repair_proposal_facts_invalid", "The rejected source proposal no longer has a facts array."); + } + const repairedFacts = [...proposal.facts]; + const removals = []; + for (const operation of transaction.operations) { + if (!isRecord(operation) || !["replace", "remove"].includes(operation.action)) { + throw batchError("source_repair_operation_invalid", "Every source repair operation requires action replace or remove."); + } + if (operation.action === "replace") { + if (!hasOnlyKeys(operation, ["action", "fact", "factNumber"]) || !isRecord(operation.fact)) { + throw batchError("source_repair_replacement_invalid", `Replacement for fact ${operation.factNumber} requires one complete fact object.`); + } + repairedFacts[operation.factNumber - 1] = operation.fact; + } else { + if (!hasOnlyKeys(operation, ["action", "factNumber", "reason"]) + || !nonEmpty(operation.reason) || containsProposalPlaceholder(operation.reason)) { + throw batchError("source_repair_removal_invalid", `Removal for fact ${operation.factNumber} requires a specific non-placeholder reason.`); + } + removals.push(operation.factNumber); + } + } + for (const factNumber of removals.sort((left, right) => right - left)) { + repairedFacts.splice(factNumber - 1, 1); + } + const repairedProposal = { ...proposal, facts: repairedFacts }; + const serializedBytes = Buffer.byteLength(JSON.stringify(repairedProposal)); + const normalized = validateSourceMergeProposal( + repairedProposal, + plan, + receipt, + state, + serializedBytes, + ); + return { + repairedProposal, + normalized, + operationCount: transaction.operations.length, + }; +} + +async function currentSourceMergeRepairAppliedReceipt(workspaceRoot, expectedStateHash, sourceRows) { + const fragmentDirectory = `${STATE_DIRECTORY}/fragments`; + const directoryPath = await resolveSafeWorkspacePath(workspaceRoot, fragmentDirectory, { allowMissing: true }); + let entries; + try { + entries = await readdir(directoryPath, { withFileTypes: true }); + } catch (error) { + if (error?.code === "ENOENT") return undefined; + throw error; + } + const sourceById = new Map(sourceRows + .filter((source) => isRecord(source) && nonEmpty(source.id)) + .map((source) => [source.id, source])); + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + if (!entry.isFile() || !SOURCE_MERGE_REPAIR_APPLIED_PATTERN.test(entry.name)) continue; + try { + const receiptPath = `${fragmentDirectory}/${entry.name}`; + const path = await resolveSafeWorkspacePath(workspaceRoot, receiptPath); + const receiptInfo = await stat(path); + if (!receiptInfo.isFile() || receiptInfo.size <= 0 + || receiptInfo.size > SOURCE_MERGE_REPAIR_APPLIED_MAX_BYTES) continue; + const bytes = await readFile(path); + const receipt = JSON.parse(bytes.toString("utf8")); + const expectedKeys = [ + "checkpointType", + "previousStateHash", + "repairPath", + "repairSha256", + "schemaVersion", + "sourceIds", + "stateHash", + ]; + if (!isRecord(receipt) || !hasOnlyKeys(receipt, expectedKeys) + || receipt.schemaVersion !== 1 + || receipt.checkpointType !== SOURCE_MERGE_REPAIR_APPLIED_TYPE + || receipt.stateHash !== expectedStateHash + || !/^[a-f0-9]{64}$/u.test(String(receipt.previousStateHash ?? "")) + || !/^[a-f0-9]{64}$/u.test(String(receipt.repairSha256 ?? "")) + || !Array.isArray(receipt.sourceIds) + || receipt.sourceIds.length < 1 + || receipt.sourceIds.length > SOURCE_REVIEW_MERGE_MAX_SOURCES + || new Set(receipt.sourceIds).size !== receipt.sourceIds.length + || receipt.sourceIds.some((sourceId) => !nonEmpty(sourceId) + || !sourceById.has(sourceId) + || sourceById.get(sourceId)?.status === "pending")) continue; + const digest = entry.name.match(/^source-repair-applied-([a-f0-9]{12})\.json$/u)?.[1]; + const expectedRepairPath = `${fragmentDirectory}/source-repair-${digest}.json`; + if (!digest || receipt.repairPath !== expectedRepairPath) continue; + const repairPath = await resolveSafeWorkspacePath(workspaceRoot, expectedRepairPath); + const repairInfo = await stat(repairPath); + if (!repairInfo.isFile() || repairInfo.size <= 0 || repairInfo.size > SOURCE_MERGE_REPAIR_MAX_BYTES) continue; + const repairBytes = await readFile(repairPath); + if (sha256(repairBytes) !== receipt.repairSha256) continue; + const transaction = JSON.parse(repairBytes.toString("utf8")); + if (!isRecord(transaction) + || transaction.schemaVersion !== 1 + || transaction.phase !== "sources" + || transaction.group !== "source-fragment-repair" + || transaction.expectedStateHash !== receipt.previousStateHash + || !nonEmpty(transaction.proposalPath) + || !/^source-merge-[a-f0-9]{12}\.json$/u.test(transaction.proposalPath.split("/").at(-1) ?? "") + || !/^[a-f0-9]{64}$/u.test(String(transaction.proposalSha256 ?? "")) + || !/^[a-f0-9]{64}$/u.test(String(transaction.diagnosticSha256 ?? ""))) continue; + const expectedDigest = sha256([ + transaction.expectedStateHash, + transaction.proposalPath, + transaction.proposalSha256, + transaction.diagnosticSha256, + ].join("\0")).slice(0, 12); + if (expectedDigest !== digest) continue; + const proposalPath = await resolveSafeWorkspacePath(workspaceRoot, transaction.proposalPath); + const proposalInfo = await stat(proposalPath); + if (!proposalInfo.isFile() || proposalInfo.size <= 0 || proposalInfo.size > 24576) continue; + const proposalBytes = await readFile(proposalPath); + if (sha256(proposalBytes) !== transaction.proposalSha256) continue; + const proposal = JSON.parse(proposalBytes.toString("utf8")); + const proposalSourceIds = Array.isArray(proposal?.sourceIds) ? [...proposal.sourceIds].sort() : []; + if (proposalSourceIds.length !== receipt.sourceIds.length + || proposalSourceIds.some((sourceId) => !nonEmpty(sourceId)) + || JSON.stringify(proposalSourceIds) !== JSON.stringify([...receipt.sourceIds].sort())) continue; + return { + path: receiptPath, + stateHash: receipt.stateHash, + sourceIds: [...receipt.sourceIds], + repairSha256: receipt.repairSha256, + }; + } catch { + continue; + } + } + return undefined; +} + export async function applySourceMergeProposal(workspaceRoot, options = {}) { if (!nonEmpty(options.proposalPath) || !/^source-merge-[a-f0-9]{12}\.json$/u.test(options.proposalPath.split("/").at(-1) ?? "")) { throw batchError("source_merge_proposal_path_invalid", "source-merge-apply requires the injected deterministic proposal path."); @@ -897,6 +1262,121 @@ export async function applySourceMergeProposal(workspaceRoot, options = {}) { if (!receipt) throw batchError("source_fragment_receipt_invalid", "The source fragment receipt is no longer valid."); const normalized = validateSourceMergeProposal(patch, proposalPlan, receipt, loaded.state, proposalBytes.byteLength); + return applyNormalizedSourceMerge(workspaceRoot, { + before, + loaded, + proposalPlan, + receipt, + normalized, + group: "source-fragment-merge", + }); +} + +export async function applySourceMergeRepair(workspaceRoot, options = {}) { + if (!nonEmpty(options.repairPath) + || !SOURCE_MERGE_REPAIR_PATTERN.test(options.repairPath.split("/").at(-1) ?? "")) { + throw batchError("source_repair_path_invalid", "source-repair-apply requires the injected deterministic repair path."); + } + if (!/^[a-f0-9]{64}$/u.test(String(options.repairSha256 ?? ""))) { + throw batchError("source_repair_hash_invalid", "source-repair-apply requires the injected lowercase repair SHA-256."); + } + const before = await validateWorkspace({ workspaceRoot, writeProof: false }); + const plan = await pendingSourceReviewPlan(workspaceRoot, { expectedStateHash: before.stateHash }); + if (plan.group !== "source-fragment-repair-apply" || plan.repair?.validated !== true + || plan.repair.path !== options.repairPath) { + throw batchError("source_repair_out_of_scope", "The repair is not the current validated source transaction."); + } + if (plan.repair.repairSha256 !== options.repairSha256) { + throw batchError("source_repair_changed", "The source repair transaction changed after validation."); + } + const repairPath = await resolveSafeWorkspacePath(workspaceRoot, options.repairPath); + const repairBytes = await readFile(repairPath); + if (sha256(repairBytes) !== options.repairSha256) { + throw batchError("source_repair_changed", "The source repair transaction changed after validation."); + } + if (repairBytes.byteLength > SOURCE_MERGE_REPAIR_MAX_BYTES) { + throw batchError("source_repair_byte_limit", `Source repair transaction is ${repairBytes.byteLength} bytes; maximum is ${SOURCE_MERGE_REPAIR_MAX_BYTES}.`); + } + const loaded = await readWorkspaceState(workspaceRoot); + const sourceRows = Array.isArray(loaded.state.sources?.sources) ? loaded.state.sources.sources : []; + const receipts = await validSourceReviewReceipts(workspaceRoot, sourceRows); + const mergePlan = sourceReviewMergePlanFor(sourceRows, receipts); + if (!mergePlan) throw batchError("source_repair_out_of_scope", "No rejected source proposal is currently repairable."); + const proposalPlan = sourceMergeProposalPlanFor(mergePlan, before.stateHash); + const readinessReceipt = await validSourceMergeReadinessReceipt(workspaceRoot, proposalPlan); + if (!readinessReceipt) throw batchError("source_repair_out_of_scope", "The source merge readiness receipt is no longer valid."); + const proposePlan = sourceMergeProposePlanFor(proposalPlan, readinessReceipt); + const proposalReceipt = await validSourceMergeProposalReceipt( + workspaceRoot, + proposePlan, + receipts, + loaded.state, + ); + if (!proposalReceipt?.error) { + throw batchError("source_repair_out_of_scope", "The source proposal is no longer in the rejected repair state."); + } + const rejectedPlan = { + ...proposePlan, + proposal: { + ...proposePlan.proposal, + validationError: proposalReceipt.error, + ...(proposalReceipt.validationDiagnostics + ? { validationDiagnostics: proposalReceipt.validationDiagnostics } + : {}), + ...(proposalReceipt.repairSlice ? { repairSlice: proposalReceipt.repairSlice } : {}), + }, + }; + const repairPlan = sourceMergeRepairPlanFor(rejectedPlan); + if (!repairPlan + || repairPlan.repair.path !== plan.repair.path + || repairPlan.repair.proposalSha256 !== plan.repair.proposalSha256 + || repairPlan.repair.diagnosticSha256 !== plan.repair.diagnosticSha256) { + throw batchError("source_repair_out_of_scope", "The rejected source proposal no longer matches this repair transaction."); + } + const receipt = receipts.find((candidate) => candidate.fragmentPath === repairPlan.proposal.fragmentPath + && candidate.receiptSha256 === repairPlan.proposal.receiptSha256); + if (!receipt) throw batchError("source_fragment_receipt_invalid", "The source fragment receipt is no longer valid."); + const proposalPath = await resolveSafeWorkspacePath(workspaceRoot, repairPlan.repair.proposalPath); + const proposalBytes = await readFile(proposalPath); + if (sha256(proposalBytes) !== repairPlan.repair.proposalSha256) { + throw batchError("source_repair_proposal_changed", "The rejected source proposal changed after repair validation."); + } + const transaction = JSON.parse(repairBytes.toString("utf8")); + const proposal = JSON.parse(proposalBytes.toString("utf8")); + const validated = validateSourceMergeRepairTransaction( + transaction, + repairPlan, + proposal, + receipt, + loaded.state, + ); + return applyNormalizedSourceMerge(workspaceRoot, { + before, + loaded, + proposalPlan: repairPlan, + receipt, + normalized: validated.normalized, + group: "source-fragment-repair", + appliedReceipt: { + path: repairPlan.repair.appliedReceiptPath, + repairPath: repairPlan.repair.path, + repairSha256: options.repairSha256, + }, + }); +} + +async function applyNormalizedSourceMerge(workspaceRoot, options) { + const { + before, + loaded, + proposalPlan, + receipt, + normalized, + group, + appliedReceipt, + } = options; + + const sourceRows = Array.isArray(loaded.state.sources?.sources) ? loaded.state.sources.sources : []; const selectedIds = new Set(proposalPlan.proposal.sourceIds); const fragmentRows = new Map(receipt.sourceRows.map((row) => [row.sourceId, row])); const factIdsBySource = new Map(proposalPlan.proposal.sourceIds.map((sourceId) => [sourceId, []])); @@ -949,10 +1429,40 @@ export async function applySourceMergeProposal(workspaceRoot, options = {}) { } const after = await validateWorkspace({ workspaceRoot, writeProof: true }); + if (appliedReceipt) { + try { + if (!SOURCE_MERGE_REPAIR_APPLIED_PATTERN.test(appliedReceipt.path.split("/").at(-1) ?? "")) { + throw batchError("source_repair_receipt_path_invalid", "Applied source repair receipt path is invalid."); + } + const receiptPath = await resolveSafeWorkspacePath(workspaceRoot, appliedReceipt.path, { allowMissing: true }); + await writeJsonAtomic(receiptPath, { + schemaVersion: 1, + checkpointType: SOURCE_MERGE_REPAIR_APPLIED_TYPE, + previousStateHash: before.stateHash, + stateHash: after.stateHash, + sourceIds: [...selectedIds].sort(), + repairPath: appliedReceipt.repairPath, + repairSha256: appliedReceipt.repairSha256, + }); + } catch (error) { + try { + await writeJsonAtomic(loaded.paths.facts, loaded.state.facts); + await writeJsonAtomic(loaded.paths.sources, loaded.state.sources); + const receiptPath = await resolveSafeWorkspacePath(workspaceRoot, appliedReceipt.path, { allowMissing: true }); + await rm(receiptPath, { force: true }); + } catch (rollbackError) { + throw batchError( + "source_repair_receipt_rollback_failed", + `Applied receipt failed (${errorMessage(error)}) and canonical rollback also failed (${errorMessage(rollbackError)}).`, + ); + } + throw error; + } + } return { applied: true, phase: "sources", - group: "source-fragment-merge", + group, sourceCount: selectedIds.size, factCount: normalized.facts.length, previousStateHash: before.stateHash, @@ -2978,6 +3488,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { const sourceBootstrap = sourceBootstrapCommandFor(result, cliPath); const sourceFragment = sourceFragmentSliceCommandFor(workItems, cliPath); const sourceMergeApply = sourceMergeApplyCommandFor(workItems, cliPath); + const sourceRepairApply = sourceRepairApplyCommandFor(workItems, cliPath); const matrixSelectionApply = matrixSelectionApplyCommandFor(workItems, cliPath); const matrixProposalApply = matrixProposalApplyCommandFor(workItems, cliPath); const authorityClosureApply = authorityClosureApplyCommandFor(workItems, cliPath); @@ -3012,6 +3523,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { ...(sourceBootstrap ? { sourceBootstrapCommand: sourceBootstrap } : {}), ...(sourceFragment ? { sourceFragmentCommand: sourceFragment } : {}), ...(sourceMergeApply ? { sourceMergeApplyCommand: sourceMergeApply } : {}), + ...(sourceRepairApply ? { sourceMergeRepairApplyCommand: sourceRepairApply } : {}), ...(matrixSelectionApply ? { matrixSelectionApplyCommand: matrixSelectionApply } : {}), ...(matrixProposalApply ? { matrixProposalApplyCommand: matrixProposalApply } : {}), ...(authorityClosureApply ? { authorityClosureApplyCommand: authorityClosureApply } : {}), @@ -3027,6 +3539,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { sourceBootstrap, sourceFragment, sourceMergeApply, + sourceRepairApply, matrixSelectionApply, matrixProposalApply, authorityClosureApply, @@ -3071,6 +3584,13 @@ function sourceMergeApplyCommandFor(workItems, cliPath) { + `--limit ${workItems.limits.maxRecords} --max-bytes ${workItems.limits.maxSerializedBytes}`; } +function sourceRepairApplyCommandFor(workItems, cliPath) { + if (workItems?.group !== "source-fragment-repair-apply" || workItems?.repair?.validated !== true) return undefined; + return `node ${JSON.stringify(cliPath)} source-repair-apply --workspace "$PWD" ` + + `--input-file ${JSON.stringify(workItems.repair.path)} ` + + `--repair-sha256 ${workItems.repair.repairSha256}`; +} + function matrixSelectionApplyCommandFor(workItems, cliPath) { if (workItems?.group !== "matrix-pending-selection-apply" || workItems.selection?.validated !== true || !nonEmpty(workItems.selection?.path)) return undefined; @@ -3234,6 +3754,7 @@ function nextActionFor( sourceBootstrap, sourceFragment, sourceMergeApply, + sourceRepairApply, matrixSelectionApply, matrixProposalApply, authorityClosureApply, @@ -3298,6 +3819,26 @@ function nextActionFor( + `Set thresholdAssessment to null unless the source supports a numeric threshold comparison; when present use the exact shape {"operator":"gt","actual":120,"threshold":100,"unit":"currency units","breached":true}, with operator one of gt, gte, lt, lte, or eq; never use prose or alternate field names. ` + `Do not read the readiness checkpoint, fragment, canonical ledgers, or raw sources; do not re-dispatch workers. The Legal Plugin will validate the proposal receipt before exposing an apply command.`; } + if (first?.phase === "sources" && first.code === "source_pending" + && workItems?.group === "source-fragment-repair") { + if (workItems.repair?.validationError) { + return `The immutable source repair transaction at ${JSON.stringify(workItems.repair.path)} was rejected with ` + + `${workItems.repair.validationError.code}: ${workItems.repair.validationError.message}. ` + + `Do not read or overwrite the rejected proposal, repair transaction, canonical ledgers, fragment, or raw sources. ` + + `The bounded repair opportunity is exhausted unless a later user request authorizes a fresh transaction.`; + } + return `Write exactly one new immutable source repair transaction to ${JSON.stringify(workItems.repair.path)} as the next tool call. ` + + `Use workItems.repair.template and workItems.repair.repairSlice; cover every rejected fact exactly once with action replace or remove. ` + + `For replace, provide the complete corrected fact. For remove, provide a specific reason and rely on the unchanged full validator to enforce source disposition. ` + + `Do not read or overwrite the rejected proposal, readiness checkpoint, canonical ledgers, fragment, or raw sources. ` + + `The Legal Plugin will combine the repair with every unchanged fact and validate it before exposing an apply command.`; + } + if (first?.phase === "sources" && first.code === "source_pending" + && workItems?.group === "source-fragment-repair-apply") { + return `The immutable source repair is valid and state-bound. Execute sourceMergeRepairApplyCommand exactly as the next tool call: ${sourceRepairApply}. ` + + `Do not read or edit the repair, rejected proposal, canonical ledgers, fragment, or raw sources. ` + + `The command reconstructs the corrected proposal in memory, atomically updates sources.json and facts.json, and records a current canonical progress receipt.`; + } if (first?.phase === "sources" && first.code === "source_pending" && workItems?.group === "source-fragment-apply") { return `The bounded source-merge proposal is valid and state-bound. Execute sourceMergeApplyCommand exactly as the next tool call: ${sourceMergeApply}. ` diff --git a/tests/agent/legal-coverage-plugin-runtime.spec.ts b/tests/agent/legal-coverage-plugin-runtime.spec.ts index 43f20229..1b65c7be 100644 --- a/tests/agent/legal-coverage-plugin-runtime.spec.ts +++ b/tests/agent/legal-coverage-plugin-runtime.spec.ts @@ -4,6 +4,7 @@ import { createHash } from "node:crypto"; import { cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { createLocalGateway } from "../../src/cli/createLocalGateway.js"; import type { CanonicalMessage, CanonicalModelRequest, ModelRuntime } from "../../src/model/index.js"; import { DEFAULT_MODEL_CAPABILITIES } from "../../src/model/protocol/capabilities.js"; @@ -13,6 +14,15 @@ import { createAgentProjectSessionStorage } from "../../src/session/index.js"; const PLUGIN_ROOT = resolve("products/legal/plugins/legal-coverage"); const STATE_ROOT = join(".pilotdeck", "work", "legal-coverage"); +const REQUIRED_MATRIX_IDS = [ + "equity-capital-timeline", + "holding-platform-special-rights", + "governance-personnel-timeline", + "contract-key-terms", + "debt-collateral-liquidity", + "employment-ip-timeline", + "legal-authority", +]; test("real gateway drives legal plugin milestones through bounded artifact correction", async () => { const root = await mkdtemp(join(tmpdir(), "pilotdeck-legal-gateway-")); @@ -192,6 +202,328 @@ test("real gateway executes the state-bound legal authority closure with complet } }); +test("real gateway applies an immutable source repair before renewing legal progress", async () => { + const root = await mkdtemp(join(tmpdir(), "pilotdeck-legal-source-repair-gateway-")); + const projectRoot = join(root, "project"); + const pilotHome = join(root, "home"); + const installedPlugin = join(projectRoot, ".pilotdeck", "plugins", "legal-coverage"); + const requests: CanonicalModelRequest[] = []; + await mkdir(projectRoot, { recursive: true }); + await mkdir(pilotHome, { recursive: true }); + await cp(PLUGIN_ROOT, installedPlugin, { recursive: true }); + await writeFile(join(pilotHome, "pilotdeck.yaml"), SOURCE_REPAIR_TEST_CONFIG); + const fixture = await writeSourceRepairState(projectRoot); + + const runtime = createLocalGateway({ + projectRoot, + fallbackProjectRoot: projectRoot, + pilotHome, + env: { ...process.env, PILOT_HOME: pilotHome, PILOTDECK_BUILD_SHA: "source-repair-test" }, + __testModelFactory: () => sourceRepairModelRuntime(requests, projectRoot), + }); + try { + const events = []; + for await (const event of runtime.gateway.submitTurn({ + sessionKey: "legal-source-repair-session", + channelKey: "test", + projectKey: projectRoot, + message: "Continue the configured legal source review.", + canPrompt: false, + timeoutMs: 60_000, + })) { + events.push(event); + } + + const agentRequests = requests.filter((request) => !isCompactionRequest(request)); + assert.equal(agentRequests.length, 4, JSON.stringify(events)); + assert.match(messageText(agentRequests[0]?.messages ?? []), /"group": "source-fragment-repair"/u); + assert.match(messageText(agentRequests[0]?.messages ?? []), /Do not read or overwrite the rejected proposal/u); + assert.match(messageText(agentRequests[1]?.messages ?? []), /"group": "source-fragment-repair-apply"/u); + assert.match(messageText(agentRequests[1]?.messages ?? []), /source-repair-apply/u); + assert.match(messageText(agentRequests[2]?.messages ?? []), /"appliedRepair":/u); + assert.match(messageText(agentRequests[3]?.messages ?? []), /"milestone": "COMPLETE"/u); + const convergence = agentRequests.map((request) => request.metadata?.pilotdeckConvergence as { + progressOrdinal?: number; + repairPreparationOrdinal?: number; + } | undefined); + assert.deepEqual(convergence.map((item) => item?.progressOrdinal), [0, 0, 1, 2]); + assert.deepEqual(convergence.map((item) => item?.repairPreparationOrdinal), [0, 1, 1, 1]); + assert.equal(events.some((event) => event.type === "agent_status" + && event.event === "progress_lease_evaluated" + && event.detail?.decision === "fail_closed"), false); + assert.equal(events.some((event) => event.type === "turn_completed" && event.finishReason === "completed"), true); + + assert.deepEqual(await readFile(join(projectRoot, fixture.proposalPath)), fixture.proposalBytes); + const sources = JSON.parse(await readFile(join(projectRoot, STATE_ROOT, "sources.json"), "utf8")) as { + sources: Array<{ status: string }>; + }; + const facts = JSON.parse(await readFile(join(projectRoot, STATE_ROOT, "facts.json"), "utf8")) as { + facts: Array<{ missingTimeReason?: string }>; + }; + assert.equal(sources.sources.length, 5); + assert.equal(sources.sources.every((source) => source.status === "reviewed"), true); + assert.equal(facts.facts.length, 4); + assert.equal(facts.facts.slice(0, 2).every((fact) => typeof fact.missingTimeReason === "string"), true); + assert.equal((await stat(join(projectRoot, fixture.repairPath))).size > 0, true); + assert.equal((await stat(join(projectRoot, fixture.appliedReceiptPath))).size > 0, true); + + const storage = createAgentProjectSessionStorage({ + projectRoot, + pilotHome, + sessionId: "legal-source-repair-session", + }); + const observationPath = join(storage.observabilityDir, "observations.jsonl"); + const observations = await readObservationEvents(observationPath); + const integrity = JSON.parse(await readFile(join(storage.observabilityDir, "integrity.json"), "utf8")); + assert.equal(integrity.status, "complete"); + assert.equal(integrity.checks.modelRequestsPaired, true); + assert.equal(integrity.checks.toolCallsPaired, true); + assert.equal(integrity.checks.turnsPaired, true); + assert.equal(integrity.recorder.droppedEvents, 0); + assert.equal(observations.filter((event) => event.type === "tool.call.started").length, 2); + assert.equal(observations.filter((event) => event.type === "tool.call.completed").length, 2); + } finally { + await runtime.dispose(); + await rm(root, { recursive: true, force: true }); + } +}); + +function sourceRepairModelRuntime( + requests: CanonicalModelRequest[], + projectRoot: string, +): ModelRuntime { + return { + async *stream(request) { + if (isCompactionRequest(request)) { + yield { type: "message_start", role: "assistant" }; + yield { type: "text_delta", text: "Continue the immutable legal source repair." }; + yield { type: "message_end", finishReason: "stop" }; + return; + } + requests.push(request); + const envelope = legalEnvelopeFromMessages(request.messages); + yield { type: "message_start", role: "assistant" }; + if (envelope?.workItems?.group === "source-fragment-repair") { + const template = structuredClone(envelope.workItems.repair.template); + template.operations = template.operations.map((operation: any) => { + const fact = { ...operation.fact }; + delete fact.dateOrPeriod; + fact.missingTimeReason = "The reviewed synthetic source contains no usable date."; + return { ...operation, fact }; + }); + const toolCall = { + id: "source-repair-write", + name: "write_file", + input: { + file_path: envelope.workItems.repair.path, + content: `${JSON.stringify(template, null, 2)}\n`, + }, + }; + yield { type: "tool_call_start", id: toolCall.id, name: toolCall.name }; + yield { type: "tool_call_end", toolCall }; + yield { type: "message_end", finishReason: "tool_call" }; + return; + } + if (envelope?.workItems?.group === "source-fragment-repair-apply") { + const toolCall = { + id: "source-repair-apply", + name: "bash", + input: { command: envelope.sourceMergeRepairApplyCommand }, + }; + yield { type: "tool_call_start", id: toolCall.id, name: toolCall.name }; + yield { type: "tool_call_end", toolCall }; + yield { type: "message_end", finishReason: "tool_call" }; + return; + } + await writeCompletedSourceRepairState(projectRoot); + yield { type: "text_delta", text: "The immutable source repair is applied and the synthetic review is complete." }; + yield { type: "usage", usage: { inputTokens: 40, outputTokens: 10, totalTokens: 50 } }; + yield { type: "message_end", finishReason: "stop" }; + }, + async complete() { + return { role: "assistant", content: [{ type: "text", text: '{"title":"Immutable source repair QA"}' }], finishReason: "stop" }; + }, + getCapabilities: () => ({ ...DEFAULT_MODEL_CAPABILITIES, maxContextTokens: 1_048_576 }), + getMultimodal: () => DEFAULT_MULTIMODAL_CONSTRAINTS, + getProviderProtocol: () => "openai", + getProviderBaseUrl: () => "https://example.invalid", + }; +} + +async function writeSourceRepairState(workspace: string): Promise<{ + proposalPath: string; + proposalBytes: Buffer; + repairPath: string; + appliedReceiptPath: string; +}> { + const stateRoot = join(workspace, STATE_ROOT); + const sourceRoot = join(workspace, "source-room"); + const deliverablePath = join(workspace, "deliverables", "opinion.md"); + await mkdir(sourceRoot, { recursive: true }); + await mkdir(join(workspace, "deliverables"), { recursive: true }); + await mkdir(stateRoot, { recursive: true }); + await writeFile(deliverablePath, "# Draft legal review\n"); + const sources = []; + for (let index = 1; index <= 5; index += 1) { + const id = `S-${String(index).padStart(3, "0")}`; + const path = `source-room/source-${index}.txt`; + const content = `Reviewed synthetic source ${index}.\n`; + await writeFile(join(workspace, path), content); + sources.push({ id, path, content }); + } + await writeJson(join(stateRoot, "config.json"), { + schemaVersion: 1, + enabled: true, + jurisdiction: "Synthetic jurisdiction", + basisDate: "Synthetic review date", + allowNoMaterialFacts: false, + inputRoots: ["source-room"], + deliverables: [{ id: "opinion", path: "deliverables/opinion.md", required: true }], + }); + await writeJson(join(stateRoot, "sources.json"), { + schemaVersion: 1, + sources: sources.map((source) => ({ + id: source.id, + path: source.path, + sha256: sha256(source.content), + status: "pending", + })), + }); + await writeJson(join(stateRoot, "facts.json"), { schemaVersion: 1, facts: [] }); + await writeJson(join(stateRoot, "matrices.json"), { + schemaVersion: 1, + matrices: REQUIRED_MATRIX_IDS.map((id) => ({ id, status: "pending", entries: [] })), + }); + await writeJson(join(stateRoot, "issues.json"), { schemaVersion: 1, issues: [] }); + await writeJson(join(stateRoot, "authorities.json"), { schemaVersion: 1, authorities: [] }); + await writeJson(join(stateRoot, "coverage.json"), { + schemaVersion: 1, + deliverables: [], + sources: [], + facts: [], + issues: [], + authorities: [], + }); + + const legal = await import(pathToFileURL(join(PLUGIN_ROOT, "scripts", "lib", "legal-coverage.mjs")).href) as any; + const delegated = await legal.pendingSourceReviewPlan(workspace); + assert.equal(delegated.group, "pending-source-review"); + assert.equal(delegated.batches.length, 1); + const batch = delegated.batches[0]; + await mkdir(join(workspace, STATE_ROOT, "fragments"), { recursive: true }); + await writeJson(join(workspace, batch.fragmentPath), { + schemaVersion: 1, + fragmentType: "legal-evidence-source-batch-review", + fragmentId: batch.id, + assignedSourceIds: batch.sourceIds, + sources: sources.map((source) => ({ + sourceId: source.id, + sourcePath: source.path, + inspectionMethod: "plain-text inspection", + facts: [{ locator: "line 1", statement: source.content.trim() }], + evidenceClass: "other", + verificationState: "verified", + conflicts: [], + unresolvedItems: [], + proposedMateriality: "non-material", + })), + }); + const merge = await legal.pendingSourceReviewPlan(workspace); + assert.equal(merge.group, "source-fragment-merge"); + await legal.prepareSourceMergeProposal(workspace, { + readinessPath: merge.readiness.path, + expectedStateHash: merge.proposal.expectedStateHash, + fragmentPath: merge.proposal.fragmentPath, + receiptSha256: merge.proposal.receiptSha256, + sourceIds: merge.proposal.sourceIds, + maxRecords: 4, + maxSerializedBytes: 24576, + }); + const propose = await legal.pendingSourceReviewPlan(workspace); + assert.equal(propose.group, "source-fragment-propose"); + const selectedSources = sources.filter((source) => propose.proposal.sourceIds.includes(source.id)); + const proposal = { + schemaVersion: 1, + phase: "sources", + group: "source-fragment-merge", + expectedStateHash: propose.proposal.expectedStateHash, + fragmentPath: propose.proposal.fragmentPath, + receiptSha256: propose.proposal.receiptSha256, + sourceIds: propose.proposal.sourceIds, + facts: selectedSources.map((source, index) => ({ + subject: source.id, + predicate: "contains reviewed evidence", + value: source.content.trim(), + ...(index < 2 + ? { dateOrPeriod: null } + : { missingTimeReason: "The reviewed synthetic source contains no usable date." }), + sourceRefs: [{ sourceId: source.id, locator: "line 1" }], + evidenceClass: "other", + verificationStatus: "verified", + conflictStatus: "none", + material: false, + critical: false, + })), + noMaterialFacts: [], + }; + const proposalPath = propose.proposal.path; + await writeJson(join(workspace, proposalPath), proposal); + const proposalBytes = await readFile(join(workspace, proposalPath)); + const repair = await legal.pendingSourceReviewPlan(workspace); + assert.equal(repair.group, "source-fragment-repair"); + assert.deepEqual(repair.repair.repairSlice.rejectedFacts.map((item: any) => item.factNumber), [1, 2]); + return { + proposalPath, + proposalBytes, + repairPath: repair.repair.path, + appliedReceiptPath: repair.repair.appliedReceiptPath, + }; +} + +async function writeCompletedSourceRepairState(workspace: string): Promise { + const stateRoot = join(workspace, STATE_ROOT); + const opinion = "# Legal Review\nThe reviewed synthetic sources contain no material legal issue.\n"; + await writeFile(join(workspace, "deliverables", "opinion.md"), opinion); + const sourceLedger = JSON.parse(await readFile(join(stateRoot, "sources.json"), "utf8")) as { + schemaVersion: number; + sources: Array>; + }; + await writeJson(join(stateRoot, "sources.json"), { + ...sourceLedger, + sources: sourceLedger.sources.map((source) => source.status === "pending" ? { + ...source, + status: "reviewed", + extractionMethod: "plain-text inspection", + evidenceClass: "other", + factIds: [], + noMaterialFactsReason: "The final synthetic source contains no additional material fact.", + unresolvedItems: [], + } : source), + }); + await writeJson(join(stateRoot, "matrices.json"), { + schemaVersion: 1, + matrices: REQUIRED_MATRIX_IDS.map((id) => ({ + id, + status: "not-applicable", + entries: [], + notApplicableReason: "The repaired synthetic source facts are non-material.", + })), + }); + await writeJson(join(stateRoot, "issues.json"), { schemaVersion: 1, issues: [] }); + await writeJson(join(stateRoot, "authorities.json"), { schemaVersion: 1, authorities: [] }); + await writeJson(join(stateRoot, "coverage.json"), { + schemaVersion: 1, + deliverables: [{ path: "deliverables/opinion.md", sha256: sha256(opinion) }], + sources: [], + facts: [], + issues: [], + authorities: [], + }); + const legal = await import(pathToFileURL(join(PLUGIN_ROOT, "scripts", "lib", "legal-coverage.mjs")).href) as any; + const validation = await legal.validateWorkspace({ workspaceRoot: workspace, writeProof: false }); + assert.equal(validation.passed, true, JSON.stringify(validation.errors)); +} + function fakeModelRuntime( requests: CanonicalModelRequest[], projectRoot: string, @@ -594,3 +926,39 @@ observability: variant: candidate queueCapacity: 4096 `; + +const SOURCE_REPAIR_TEST_CONFIG = `schemaVersion: 1 +agent: + model: test/test-model + maxOutputTokens: 4096 + progressLease: + enabled: true + mode: evaluation + maxStagnantObservations: 2 + maxInitialStagnantObservations: 8 +model: + providers: + test: + protocol: openai + url: https://example.invalid + apiKey: test-key + models: + test-model: + capabilities: + maxContextTokens: 1048576 + maxOutputTokens: 8192 +router: + enabled: false + scenarios: + default: test/test-model +memory: + enabled: false +telemetry: + enabled: false +observability: + enabled: true + profile: diagnostic + campaignId: immutable-source-repair-qa + variant: candidate + queueCapacity: 4096 +`; diff --git a/tests/products/legal-coverage.spec.ts b/tests/products/legal-coverage.spec.ts index cd417e8c..1ea62f05 100644 --- a/tests/products/legal-coverage.spec.ts +++ b/tests/products/legal-coverage.spec.ts @@ -265,6 +265,7 @@ test("legal coverage source bootstrap creates only deterministic pending manifes test("legal coverage injects deterministic disjoint worker batches for large pending source rooms", async () => { const workspace = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-source-plan-")); + const outside = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-source-repair-outside-")); try { const fixture = await writeManifestBoundFixture(workspace); const manifestPath = join(workspace, ".pilotdeck/input-manifest.json"); @@ -620,7 +621,8 @@ test("legal coverage injects deterministic disjoint worker batches for large pen cwd: workspace, }); const invalidProposalContext = invalidProposal.hookSpecificOutput.additionalContext ?? ""; - assert.match(invalidProposalContext, /"group": "source-fragment-propose"/u); + assert.match(invalidProposalContext, /"group": "source-fragment-repair"/u); + assert.match(invalidProposalContext, /"mode": "main-agent-repair"/u); assert.match(invalidProposalContext, /source_merge_fact_locator_unverified/u); assert.match(invalidProposalContext, /source_merge_fact_time_invalid/u); const invalidProposalEnvelope = JSON.parse(invalidProposalContext @@ -629,21 +631,32 @@ test("legal coverage injects deterministic disjoint worker batches for large pen nextAction: string; workItems: { proposal: { - validationDiagnostics: { - total: number; - returned: number; - hasMore: boolean; - items: Array<{ factNumber?: number; code: string; message: string }>; + path: string; + expectedStateHash: string; + sourceIds: string[]; + }; + repair: { + path: string; + proposalPath: string; + proposalSha256: string; + diagnosticSha256: string; + appliedReceiptPath: string; + limits: { maxOperations: number; maxSerializedBytes: number }; + template: { + schemaVersion: number; + phase: string; + group: string; + expectedStateHash: string; + proposalPath: string; + proposalSha256: string; + diagnosticSha256: string; + operations: Array<{ factNumber: number; action: string; fact: Record }>; }; repairSlice: { - proposal: { - path: string; - sha256: string; - byteCount: number; - maxSerializedBytes: number; - }; - currentProposal: typeof invalidProposalBody; diagnostics: { + total: number; + returned: number; + hasMore: boolean; items: Array<{ factNumber?: number; code: string; message: string }>; }; rejectedFacts: Array<{ @@ -657,16 +670,16 @@ test("legal coverage injects deterministic disjoint worker batches for large pen conflicts: string[]; unresolvedItems: string[]; }>; - limits: { maxSerializedBytes: number }; }; }; }; }; - assert.equal(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.total, 3); - assert.equal(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.returned, 3); - assert.equal(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.hasMore, false); + const immutableRepair = invalidProposalEnvelope.workItems.repair; + assert.equal(immutableRepair.repairSlice.diagnostics.total, 3); + assert.equal(immutableRepair.repairSlice.diagnostics.returned, 3); + assert.equal(immutableRepair.repairSlice.diagnostics.hasMore, false); assert.deepEqual( - invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items.map((item) => item.code), + immutableRepair.repairSlice.diagnostics.items.map((item) => item.code), [ "source_merge_fact_locator_unverified", "source_merge_fact_time_invalid", @@ -674,45 +687,40 @@ test("legal coverage injects deterministic disjoint worker batches for large pen ], ); assert.deepEqual( - invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items.map((item) => item.factNumber), + immutableRepair.repairSlice.diagnostics.items.map((item) => item.factNumber), [1, 2, 2], ); - assert.match(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items[0]?.message ?? "", /Proposal fact 1 locator/u); - assert.match(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items[1]?.message ?? "", /Proposal fact 2 requires exactly one/u); - assert.match(invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items[2]?.message ?? "", /Proposal fact 2 has an invalid thresholdAssessment/u); - assert.match(invalidProposalEnvelope.nextAction, /Fix every entry in workItems\.proposal\.validationDiagnostics\.items in one rewrite/u); - assert.doesNotMatch(invalidProposalEnvelope.nextAction, /source_merge_fact_locator_unverified/u); - assert.doesNotMatch(invalidProposalEnvelope.nextAction, /source_merge_fact_time_invalid/u); - const repairSlice = invalidProposalEnvelope.workItems.proposal.repairSlice; + assert.match(immutableRepair.repairSlice.diagnostics.items[0]?.message ?? "", /Proposal fact 1 locator/u); + assert.match(immutableRepair.repairSlice.diagnostics.items[1]?.message ?? "", /Proposal fact 2 requires exactly one/u); + assert.match(immutableRepair.repairSlice.diagnostics.items[2]?.message ?? "", /Proposal fact 2 has an invalid thresholdAssessment/u); + assert.match(invalidProposalEnvelope.nextAction, /Write exactly one new immutable source repair transaction/u); + assert.match(invalidProposalEnvelope.nextAction, /Do not read or overwrite the rejected proposal/u); const invalidProposalBytes = await readFile(proposalPath); - assert.equal(repairSlice.proposal.path, proposal.path); - assert.equal(repairSlice.proposal.sha256, sha256(invalidProposalBytes)); - assert.equal(repairSlice.proposal.byteCount, invalidProposalBytes.byteLength); - assert.equal(repairSlice.proposal.maxSerializedBytes, 24576); - assert.equal(repairSlice.proposal.byteCount <= repairSlice.proposal.maxSerializedBytes, true); - assert.deepEqual(repairSlice.currentProposal, invalidProposalBody); - assert.deepEqual(repairSlice.currentProposal.facts[2], proposalBase.facts[2]); - assert.deepEqual(repairSlice.diagnostics.items, invalidProposalEnvelope.workItems.proposal.validationDiagnostics.items); - assert.deepEqual(repairSlice.rejectedFacts.map((item) => item.factNumber), [1, 2]); - assert.deepEqual(repairSlice.rejectedFacts[0]?.fact, invalidProposalBody.facts[0]); - assert.deepEqual(repairSlice.rejectedFacts[0]?.diagnosticCodes, ["source_merge_fact_locator_unverified"]); - const firstRepairSource = repairSlice.sourceContext.find((source) => source.sourceId === proposal.sourceIds[0]); + assert.equal(immutableRepair.proposalPath, proposal.path); + assert.equal(immutableRepair.proposalSha256, sha256(invalidProposalBytes)); + assert.match(immutableRepair.path, /source-repair-[a-f0-9]{12}\.json$/u); + assert.match(immutableRepair.appliedReceiptPath, /source-repair-applied-[a-f0-9]{12}\.json$/u); + assert.equal(immutableRepair.limits.maxOperations, 2); + assert.equal(immutableRepair.limits.maxSerializedBytes, 24576); + assert.deepEqual(immutableRepair.repairSlice.rejectedFacts.map((item) => item.factNumber), [1, 2]); + assert.deepEqual(immutableRepair.repairSlice.rejectedFacts[0]?.fact, invalidProposalBody.facts[0]); + assert.deepEqual(immutableRepair.repairSlice.rejectedFacts[0]?.diagnosticCodes, ["source_merge_fact_locator_unverified"]); + const firstRepairSource = immutableRepair.repairSlice.sourceContext + .find((source) => source.sourceId === proposal.sourceIds[0]); assert.deepEqual(firstRepairSource?.allowedFragmentFacts, [ { locator: "converted.txt:1", statement: `Reviewed ${proposal.sourceIds[0]}.` }, ]); assert.deepEqual(firstRepairSource?.conflicts, ["Synthetic conflict is preserved on the source ledger."]); assert.deepEqual(firstRepairSource?.unresolvedItems, ["Synthetic unresolved item is preserved on the source ledger."]); - assert.equal(Buffer.byteLength(JSON.stringify(repairSlice)) <= repairSlice.limits.maxSerializedBytes, true); - assert.match(invalidProposalContext, /Rewrite workItems\.proposal\.repairSlice\.currentProposal as one complete JSON document/u); - assert.match(invalidProposalContext, /preserve every unrelated fact exactly/u); - assert.match(invalidProposalContext, /instead of reconstructing the proposal through paginated reads/u); - assert.match(invalidProposalContext, /otherwise remove a fact that merely restates conflict or unresolved metadata/u); - assert.match(invalidProposalContext, /"preparedSlice":/u); + assert.doesNotMatch(invalidProposalContext, /"currentProposal":/u); + assert.doesNotMatch(invalidProposalContext, /"preparedSlice":/u); assert.doesNotMatch(invalidProposalContext, /"sourceFragmentCommand":/u); assert.doesNotMatch(invalidProposalContext, /"sourceMergeApplyCommand":/u); + assert.doesNotMatch(invalidProposalContext, /"sourceMergeRepairApplyCommand":/u); const repairConvergence = ( invalidProposal.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { stateHash?: string; + progressOrdinal: number; repairOrdinal: number; repairPreparationOrdinal: number; } @@ -721,9 +729,7 @@ test("legal coverage injects deterministic disjoint worker batches for large pen assert.notEqual(repairConvergenceHash, proposeConvergenceHash); assert.equal(repairConvergence.repairOrdinal, proposeConvergence.repairOrdinal + 1); assert.equal(repairConvergence.repairPreparationOrdinal, 0); - assert.match(invalidProposal.hookSpecificOutput.additionalContext ?? "", /Set thresholdAssessment to null/u); - assert.match(invalidProposalEnvelope.nextAction, /"operator":"gt","actual":120,"threshold":100/u); - assert.match(invalidProposalEnvelope.nextAction, /never use prose or alternate field names/u); + assert.equal(repairConvergence.progressOrdinal, proposeConvergence.progressOrdinal); const invalidProposalHash = sha256(invalidProposalBytes); const rejectedDirectApply = await runCli( @@ -736,14 +742,16 @@ test("legal coverage injects deterministic disjoint worker batches for large pen assert.match(rejectedDirectApply.stderr, /source_merge_fact_locator_unverified/u); assert.doesNotMatch(rejectedDirectApply.stderr, /source_merge_threshold_invalid/u); + const wrongRepairPath = join(workspace, STATE_ROOT, "fragments", "wrong-repair.json"); + await writeJson(wrongRepairPath, { wrong: true }); await runHook({ hookEventName: "PostToolUse", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, - toolName: "read_file", - toolInput: { file_path: envelope.workItems.readiness.path }, - toolUseId: "wrong-target-read", + toolName: "write_file", + toolInput: { file_path: `${STATE_ROOT}/fragments/wrong-repair.json` }, + toolUseId: "wrong-target-write", }); const afterWrongRead = await runHook({ hookEventName: "PreModelRequest", @@ -763,21 +771,21 @@ test("legal coverage injects deterministic disjoint worker batches for large pen sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, - toolName: "read_file", - toolInput: { file_path: proposal.path }, - toolUseId: "target-read", + toolName: "write_file", + toolInput: { file_path: immutableRepair.path }, + toolUseId: "missing-target-write", }); - const afterTargetRead = await runHook({ + const afterMissingTargetWrite = await runHook({ hookEventName: "PreModelRequest", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, }); assert.equal( - (afterTargetRead.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + (afterMissingTargetWrite.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { repairPreparationOrdinal: number; }).repairPreparationOrdinal, - 1, + 0, ); await runHook({ @@ -786,212 +794,320 @@ test("legal coverage injects deterministic disjoint worker batches for large pen transcriptPath: "", cwd: workspace, toolName: "read_file", - toolInput: { file_path: proposal.path, offset: 1, limit: 1 }, - toolUseId: "replayed-target-read", + toolInput: { file_path: proposal.path }, + toolUseId: "rejected-proposal-read", }); - const afterReplayedRead = await runHook({ + const afterRejectedProposalRead = await runHook({ hookEventName: "PreModelRequest", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, }); assert.equal( - (afterReplayedRead.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + (afterRejectedProposalRead.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { repairPreparationOrdinal: number; }).repairPreparationOrdinal, - 1, + 0, ); - await writeJson(proposalPath, { - ...proposalBase, - facts: [{ ...proposalBase.facts[0], subject: "" }], - noMaterialFacts: proposal.sourceIds.slice(1).map((sourceId) => ({ - sourceId, - reason: "Synthetic placeholder-proposal fixture.", - })), - }); - const placeholderProposal = await runHook({ - hookEventName: "PreModelRequest", - sessionId: "large-pending-source-plan", - transcriptPath: "", - cwd: workspace, - }); - assert.match(placeholderProposal.hookSpecificOutput.additionalContext ?? "", /source_merge_fact_content_missing/u); - assert.doesNotMatch(placeholderProposal.hookSpecificOutput.additionalContext ?? "", /"sourceMergeApplyCommand":/u); - const placeholderEnvelope = JSON.parse((placeholderProposal.hookSpecificOutput.additionalContext ?? "") - .replace(/^\n/u, "") - .replace(/\n<\/legal_coverage_state>$/u, "")) as { - workItems: { proposal: { repairSlice: { proposal: { sha256: string } } } }; + const repairBody = { + ...immutableRepair.template, + operations: [ + { + factNumber: 1, + action: "replace", + fact: { + ...invalidProposalBody.facts[0], + sourceRefs: [{ sourceId: proposal.sourceIds[0], locator: "converted.txt:1" }], + }, + }, + { + factNumber: 2, + action: "replace", + fact: { + ...invalidProposalBody.facts[1], + missingTimeReason: undefined, + thresholdAssessment: { + operator: "gt", + actual: 120, + threshold: 100, + breached: true, + }, + }, + }, + ], }; - assert.notEqual(placeholderEnvelope.workItems.proposal.repairSlice.proposal.sha256, repairSlice.proposal.sha256); - assert.equal( - (placeholderProposal.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { stateHash?: string })?.stateHash, - repairConvergenceHash, - ); - assert.equal( - (placeholderProposal.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { repairOrdinal: number }).repairOrdinal, - repairConvergence.repairOrdinal, - ); - - await writeJson(proposalPath, { - ...proposalBase, - facts: [null], - noMaterialFacts: proposal.sourceIds.map((sourceId) => ({ - sourceId, - reason: "Synthetic malformed-fact fixture.", - })), - }); - const malformedFactProposal = await runHook({ - hookEventName: "PreModelRequest", + const repairPath = join(workspace, immutableRepair.path); + const canonicalSourcesBeforeRepair = await readFile(join(workspace, STATE_ROOT, "sources.json")); + const canonicalFactsBeforeRepair = await readFile(join(workspace, STATE_ROOT, "facts.json")); + const invalidRepairs: Array<{ + name: string; + body: Record; + errorCode: RegExp; + }> = [ + { + name: "missing rejected fact operation", + body: { ...repairBody, operations: repairBody.operations.slice(0, 1) }, + errorCode: /source_repair_operation_count_invalid/u, + }, + { + name: "duplicate fact operation", + body: { ...repairBody, operations: [repairBody.operations[0], repairBody.operations[0]] }, + errorCode: /source_repair_fact_scope_invalid/u, + }, + { + name: "unknown fact operation", + body: { + ...repairBody, + operations: [repairBody.operations[0], { ...repairBody.operations[1], factNumber: 99 }], + }, + errorCode: /source_repair_fact_scope_invalid/u, + }, + { + name: "extra valid-fact operation", + body: { + ...repairBody, + operations: [ + ...repairBody.operations, + { factNumber: 3, action: "replace", fact: invalidProposalBody.facts[2] }, + ], + }, + errorCode: /source_repair_operation_count_invalid/u, + }, + { + name: "unsupported action", + body: { + ...repairBody, + operations: [repairBody.operations[0], { ...repairBody.operations[1], action: "merge" }], + }, + errorCode: /source_repair_operation_invalid/u, + }, + { + name: "placeholder replacement", + body: { + ...repairBody, + operations: [ + { + ...repairBody.operations[0], + fact: { ...repairBody.operations[0]!.fact, subject: "" }, + }, + repairBody.operations[1], + ], + }, + errorCode: /source_merge_fact_content_missing/u, + }, + { + name: "removal without reason", + body: { + ...repairBody, + operations: [ + repairBody.operations[0], + { factNumber: 2, action: "remove", reason: "" }, + ], + }, + errorCode: /source_repair_removal_invalid/u, + }, + { + name: "changed diagnostic identity", + body: { ...repairBody, diagnosticSha256: "0".repeat(64) }, + errorCode: /source_repair_scope_mismatch/u, + }, + { + name: "unverified replacement locator", + body: { + ...repairBody, + operations: [ + { + ...repairBody.operations[0], + fact: { + ...repairBody.operations[0]!.fact, + sourceRefs: [{ sourceId: proposal.sourceIds[0], locator: "converted.txt:999" }], + }, + }, + repairBody.operations[1], + ], + }, + errorCode: /source_merge_fact_locator_unverified/u, + }, + { + name: "byte overflow", + body: { ...repairBody, padding: "x".repeat(24576) }, + errorCode: /source_repair_byte_limit/u, + }, + ]; + for (const invalidRepair of invalidRepairs) { + await writeJson(repairPath, invalidRepair.body); + const rejectedRepair = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "large-pending-source-plan", + transcriptPath: "", + cwd: workspace, + }); + const rejectedRepairContext = rejectedRepair.hookSpecificOutput.additionalContext ?? ""; + assert.match(rejectedRepairContext, invalidRepair.errorCode, invalidRepair.name); + assert.doesNotMatch(rejectedRepairContext, /"sourceMergeRepairApplyCommand":/u, invalidRepair.name); + assert.deepEqual( + await readFile(join(workspace, STATE_ROOT, "sources.json")), + canonicalSourcesBeforeRepair, + invalidRepair.name, + ); + assert.deepEqual( + await readFile(join(workspace, STATE_ROOT, "facts.json")), + canonicalFactsBeforeRepair, + invalidRepair.name, + ); + } + await writeJson(repairPath, repairBody); + await runHook({ + hookEventName: "PostToolUse", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, + toolName: "write_file", + toolInput: { file_path: immutableRepair.path }, + toolUseId: "immutable-repair-write", }); - const malformedFactContext = malformedFactProposal.hookSpecificOutput.additionalContext ?? ""; - assert.match(malformedFactContext, /source_merge_fact_keys_invalid/u); - assert.doesNotMatch(malformedFactContext, /source_merge_fact_time_invalid/u); - assert.doesNotMatch(malformedFactContext, /source_merge_threshold_invalid/u); - assert.doesNotMatch(malformedFactContext, /source_merge_fact_sources_missing/u); - - await writeJson(proposalPath, { unsupportedTopLevelKey: true }); - const topLevelInvalidProposal = await runHook({ + const repairApplyReceipt = await runHook({ hookEventName: "PreModelRequest", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, }); - const topLevelInvalidContext = topLevelInvalidProposal.hookSpecificOutput.additionalContext ?? ""; - assert.match(topLevelInvalidContext, /source_merge_proposal_keys_invalid/u); - assert.doesNotMatch(topLevelInvalidContext, /"repairSlice":/u); - assert.doesNotMatch(topLevelInvalidContext, /repairSlice\.currentProposal/u); - assert.match(topLevelInvalidContext, /Rewrite that proposal from injected workItems\.preparedSlice and proposal\.template/u); - assert.equal( - (topLevelInvalidProposal.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { stateHash?: string })?.stateHash, - repairConvergenceHash, - ); - - await writeJson(proposalPath, { - ...proposalBase, - facts: Array.from({ length: 32 }, (_, index) => ({ - ...proposalBase.facts[0], - subject: `Synthetic invalid fact ${index + 1}`, - dateOrPeriod: "2026", - missingTimeReason: "Conflicting synthetic time fields.", - })), - noMaterialFacts: proposal.sourceIds.map((sourceId) => ({ - sourceId, - reason: "", - })), - }); - const boundedDiagnostics = await runHook({ - hookEventName: "PreModelRequest", - sessionId: "large-pending-source-plan", - transcriptPath: "", - cwd: workspace, - }); - const boundedContext = boundedDiagnostics.hookSpecificOutput.additionalContext ?? ""; - const boundedEnvelope = JSON.parse(boundedContext + const repairApplyContext = repairApplyReceipt.hookSpecificOutput.additionalContext ?? ""; + assert.match(repairApplyContext, /"group": "source-fragment-repair-apply"/u); + assert.match(repairApplyContext, /"mode": "main-agent-repair-apply"/u); + assert.match(repairApplyContext, /"sourceMergeRepairApplyCommand":/u); + assert.match(repairApplyContext, /source-repair-apply/u); + assert.doesNotMatch(repairApplyContext, /"repairSlice":/u); + assert.doesNotMatch(repairApplyContext, /"template":/u); + const repairApplyEnvelope = JSON.parse(repairApplyContext .replace(/^\n/u, "") .replace(/\n<\/legal_coverage_state>$/u, "")) as { - workItems: { - proposal: { - validationDiagnostics: { - total: number; - returned: number; - hasMore: boolean; - items: Array<{ code: string; message: string }>; - }; - }; - }; + sourceMergeRepairApplyCommand: string; + workItems: { repair: { path: string; repairSha256: string; validated: boolean } }; }; - assert.equal(boundedEnvelope.workItems.proposal.validationDiagnostics.total, 36); - assert.equal(boundedEnvelope.workItems.proposal.validationDiagnostics.returned, 36); - assert.equal(boundedEnvelope.workItems.proposal.validationDiagnostics.hasMore, false); - assert.equal(boundedEnvelope.workItems.proposal.validationDiagnostics.items.length, 36); - assert.deepEqual( - boundedEnvelope.workItems.proposal.validationDiagnostics.items.slice(0, 32).map((item) => item.code), - Array.from({ length: 32 }, () => "source_merge_fact_time_invalid"), - ); - assert.deepEqual( - boundedEnvelope.workItems.proposal.validationDiagnostics.items.slice(32).map((item) => item.code), - Array.from({ length: 4 }, () => "source_merge_no_material_invalid"), - ); - assert.equal( - (boundedDiagnostics.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { stateHash?: string })?.stateHash, - repairConvergenceHash, - ); + assert.equal(repairApplyEnvelope.workItems.repair.path, immutableRepair.path); + assert.equal(repairApplyEnvelope.workItems.repair.validated, true); + const repairApplyConvergence = repairApplyReceipt.hookSpecificOutput.modelRequestPatch?.metadata + ?.pilotdeckConvergence as { + progressOrdinal: number; + repairOrdinal: number; + repairPreparationOrdinal: number; + }; assert.equal( - (boundedDiagnostics.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { repairOrdinal: number }).repairOrdinal, - repairConvergence.repairOrdinal, + repairApplyConvergence.repairPreparationOrdinal, + 1, ); + assert.equal(repairApplyConvergence.progressOrdinal, repairConvergence.progressOrdinal); + assert.equal(repairApplyConvergence.repairOrdinal, repairConvergence.repairOrdinal); - await writeJson(proposalPath, proposalBase); - const applyReceipt = await runHook({ - hookEventName: "PreModelRequest", + await runHook({ + hookEventName: "PostToolUse", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, + toolName: "write_file", + toolInput: { file_path: immutableRepair.path }, + toolUseId: "replayed-repair-write", }); - const applyContext = applyReceipt.hookSpecificOutput.additionalContext ?? ""; - assert.match(applyContext, /"group": "source-fragment-apply"/u); - assert.match(applyContext, /"mode": "main-agent-apply"/u); - assert.doesNotMatch(applyContext, /"preparedSlice":/u); - assert.match(applyContext, /"sourceMergeApplyCommand":/u); - assert.match(applyContext, /source-merge-apply/u); - const applyConvergence = applyReceipt.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { - stateHash?: string; - progressOrdinal: number; - repairOrdinal: number; - nextBatch?: { group?: string; returned?: number; hasMore?: boolean }; - }; - assert.notEqual(applyConvergence.stateHash, proposeConvergenceHash); - assert.deepEqual(applyConvergence.nextBatch, { - group: "source-fragment-apply", - returned: 4, - hasMore: true, - }); - assert.equal(applyConvergence.progressOrdinal, proposeConvergence.progressOrdinal + 1); - assert.equal(applyConvergence.repairOrdinal, repairConvergence.repairOrdinal); - const replayedApplyReceipt = await runHook({ + const afterReplayedWrite = await runHook({ hookEventName: "PreModelRequest", sessionId: "large-pending-source-plan", transcriptPath: "", cwd: workspace, }); assert.equal( - (replayedApplyReceipt.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { progressOrdinal: number }).progressOrdinal, - applyConvergence.progressOrdinal, + (afterReplayedWrite.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + repairPreparationOrdinal: number; + }).repairPreparationOrdinal, + 1, ); - const validProposalBytes = await readFile(proposalPath); - const proposalHash = sha256(validProposalBytes); const beforeApplySources = await readFile(join(workspace, STATE_ROOT, "sources.json")); const beforeApplyFacts = await readFile(join(workspace, STATE_ROOT, "facts.json")); - await writeFile(proposalPath, Buffer.concat([validProposalBytes, Buffer.from("\n")])); + const validRepairBytes = await readFile(repairPath); + const repairHash = sha256(validRepairBytes); + + await writeFile(proposalPath, Buffer.concat([invalidProposalBytes, Buffer.from("\n")])); const changedProposal = await runCli( workspace, - "source-merge-apply", - "--input-file", proposal.path, - "--proposal-sha256", proposalHash, + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, ); assert.equal(changedProposal.exitCode, 1); - assert.match(changedProposal.stderr, /source_merge_proposal_changed/u); + assert.match(changedProposal.stderr, /source_repair_out_of_scope/u); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); + await writeFile(proposalPath, invalidProposalBytes); + + const originalSourcePath = join(workspace, fixture.originalPath); + const originalSourceBytes = await readFile(originalSourcePath); + await writeFile(originalSourcePath, Buffer.concat([originalSourceBytes, Buffer.from("changed")])); + const staleState = await runCli( + workspace, + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, + ); + assert.equal(staleState.exitCode, 1); + assert.match(staleState.stderr, /source_repair_out_of_scope/u); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); + await writeFile(originalSourcePath, originalSourceBytes); + + const traversal = await runCli( + workspace, + "source-repair-apply", + "--input-file", `../${immutableRepair.path}`, + "--repair-sha256", repairHash, + ); + assert.equal(traversal.exitCode, 1); + assert.match(traversal.stderr, /source_repair_out_of_scope/u); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); + + const outsideRepairPath = join(outside, "source-repair.json"); + await writeFile(outsideRepairPath, validRepairBytes); + await rm(repairPath); + await symlink(outsideRepairPath, repairPath); + const symlinkedRepair = await runCli( + workspace, + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, + ); + assert.equal(symlinkedRepair.exitCode, 1); + assert.match(symlinkedRepair.stderr, /source_repair_out_of_scope/u); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); + assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); + await rm(repairPath); + await writeFile(repairPath, validRepairBytes); + + await writeFile(repairPath, Buffer.concat([validRepairBytes, Buffer.from("\n")])); + const changedRepair = await runCli( + workspace, + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, + ); + assert.equal(changedRepair.exitCode, 1); + assert.match(changedRepair.stderr, /source_repair_changed/u); assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); - await writeFile(proposalPath, validProposalBytes); + await writeFile(repairPath, validRepairBytes); const applied = await runCli( workspace, - "source-merge-apply", - "--input-file", proposal.path, - "--proposal-sha256", proposalHash, - "--limit", "4", - "--max-bytes", "24576", + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, ); assert.equal(applied.exitCode, 0, applied.stderr); const appliedResult = JSON.parse(applied.stdout) as { applied: boolean; sourceCount: number; factCount: number }; assert.equal(appliedResult.applied, true); assert.equal(appliedResult.sourceCount, 4); - assert.equal(appliedResult.factCount, 4); + assert.equal(appliedResult.factCount, 3); + assert.deepEqual(await readFile(proposalPath), invalidProposalBytes); const sourcesAfterApply = JSON.parse(await readFile(join(workspace, STATE_ROOT, "sources.json"), "utf8")) as { sources: Array<{ id: string; status: string; extractionMethod?: string; factIds?: string[] }>; }; @@ -1001,10 +1117,10 @@ test("legal coverage injects deterministic disjoint worker batches for large pen const mergedSources = sourcesAfterApply.sources.filter((source) => proposal.sourceIds.includes(source.id)); assert.equal(mergedSources.every((source) => source.status === "reviewed"), true); assert.equal(mergedSources.every((source) => source.extractionMethod === "verified derived text inspection"), true); - assert.equal(mergedSources.every((source) => source.factIds?.length === 1), true); + assert.deepEqual(mergedSources.map((source) => source.factIds?.length ?? 0), [1, 1, 1, 0]); const factsBeforeApply = JSON.parse(beforeApplyFacts.toString("utf8")) as { facts: unknown[] }; - assert.equal(factsAfterApply.facts.length, factsBeforeApply.facts.length + 4); - for (const source of mergedSources) { + assert.equal(factsAfterApply.facts.length, factsBeforeApply.facts.length + 3); + for (const source of mergedSources.slice(0, 3)) { const factId = source.factIds?.[0]; assert.equal(factsAfterApply.facts.some((fact) => fact.id === factId && fact.sourceRefs.some((reference) => reference.sourceId === source.id)), true); @@ -1012,20 +1128,64 @@ test("legal coverage injects deterministic disjoint worker batches for large pen assert.notDeepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), beforeApplySources); assert.notDeepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), beforeApplyFacts); + const appliedReceiptPath = join(workspace, immutableRepair.appliedReceiptPath); + const appliedReceiptBytes = await readFile(appliedReceiptPath); + const tamperedAppliedReceipt = JSON.parse(appliedReceiptBytes.toString("utf8")) as { repairSha256: string }; + tamperedAppliedReceipt.repairSha256 = "0".repeat(64); + await writeJson(appliedReceiptPath, tamperedAppliedReceipt); + const ignoredTamperedReceipt = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "large-pending-source-plan", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (ignoredTamperedReceipt.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + progressOrdinal: number; + }).progressOrdinal, + repairApplyConvergence.progressOrdinal, + ); + assert.doesNotMatch(ignoredTamperedReceipt.hookSpecificOutput.additionalContext ?? "", /"appliedRepair":/u); + await writeFile(appliedReceiptPath, appliedReceiptBytes); + + const afterRepairApply = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "large-pending-source-plan", + transcriptPath: "", + cwd: workspace, + }); + const afterRepairProgress = afterRepairApply.hookSpecificOutput.modelRequestPatch?.metadata + ?.pilotdeckConvergence as { progressOrdinal: number }; + assert.equal(afterRepairProgress.progressOrdinal, repairApplyConvergence.progressOrdinal + 1); + assert.match(afterRepairApply.hookSpecificOutput.additionalContext ?? "", /"appliedRepair":/u); + const replayedAppliedReceipt = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "large-pending-source-plan", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (replayedAppliedReceipt.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + progressOrdinal: number; + }).progressOrdinal, + afterRepairProgress.progressOrdinal, + ); + const sourcesBeforeReplay = await readFile(join(workspace, STATE_ROOT, "sources.json")); const factsBeforeReplay = await readFile(join(workspace, STATE_ROOT, "facts.json")); const replay = await runCli( workspace, - "source-merge-apply", - "--input-file", proposal.path, - "--proposal-sha256", proposalHash, + "source-repair-apply", + "--input-file", immutableRepair.path, + "--repair-sha256", repairHash, ); assert.equal(replay.exitCode, 1); - assert.match(replay.stderr, /stale_state_hash/u); + assert.match(replay.stderr, /source_repair_out_of_scope/u); assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "sources.json")), sourcesBeforeReplay); assert.deepEqual(await readFile(join(workspace, STATE_ROOT, "facts.json")), factsBeforeReplay); } finally { await rm(workspace, { recursive: true, force: true }); + await rm(outside, { recursive: true, force: true }); } }); @@ -3364,7 +3524,7 @@ test("legal product plugin loads one skill and contains no benchmark-specific co assert.equal(plugin.skills?.[0]?.name, "legal-coverage:conduct-legal-due-diligence"); assert.equal(plugin.hooksConfig?.PreModelRequest?.length, 1); assert.equal(plugin.hooksConfig?.PostToolUse?.length, 1); - assert.equal(plugin.hooksConfig?.PostToolUse?.[0]?.matcher, "read_file"); + assert.equal(plugin.hooksConfig?.PostToolUse?.[0]?.matcher, "read_file|write_file"); assert.equal(plugin.hooksConfig?.PostCompact?.length, 1); const files = await collectFiles(PLUGIN_ROOT); From 9f2a128d43acf3213a6e8f46ba0dec0474e1c07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E8=BE=BE=E5=A5=87?= <> Date: Mon, 27 Jul 2026 07:51:53 +0800 Subject: [PATCH 2/4] docs(convergence): define V24R8 repair protocol --- ...NVERGENCE_V24R8_IMMUTABLE_SOURCE_REPAIR.md | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/PILOTDECK_CONVERGENCE_V24R8_IMMUTABLE_SOURCE_REPAIR.md diff --git a/docs/PILOTDECK_CONVERGENCE_V24R8_IMMUTABLE_SOURCE_REPAIR.md b/docs/PILOTDECK_CONVERGENCE_V24R8_IMMUTABLE_SOURCE_REPAIR.md new file mode 100644 index 00000000..dd414e06 --- /dev/null +++ b/docs/PILOTDECK_CONVERGENCE_V24R8_IMMUTABLE_SOURCE_REPAIR.md @@ -0,0 +1,103 @@ +# PilotDeck Convergence V24R8: Immutable Source Repair Transaction + +## Decision + +V24R8 addresses only the preserved Case 09 failure in the bounded source +proposal repair path. It adds an immutable Legal Coverage repair transaction; +it does not change Agent Core, O1, validator acceptance, issue rules, Progress +Lease thresholds `8/2`, V24R7 authority closure, Router, Memory, the model, or +the evaluation corpus. + +The V24R7 Gate v2 run `20260727_064631_2982fc7f` reached a rejected second +source proposal. Two facts had neither a usable `dateOrPeriod` nor a +`missingTimeReason`. The Agent correctly read and edited the existing proposal, +but those two preparation turns exhausted the bounded lease before canonical +apply. This is a protocol mismatch: PilotDeck requires reading an existing file +before mutation, while the repair prompt asks for an in-place rewrite. + +## Protocol + +When a source proposal has complete fact-level diagnostics and a bounded repair +slice, Legal Coverage derives one fresh path: + +```text +.pilotdeck/work/legal-coverage/fragments/source-repair-.json +``` + +The digest binds the repair to the current validator state, original proposal +path and SHA-256, ordered source IDs, and the complete diagnostic identity. The +injected repair contract contains only rejected fact rows, validated source +context, and a bounded template. It does not ask the Agent to read or overwrite +the original proposal. + +The Agent writes one immutable transaction containing exactly one operation for +every rejected fact: replace the full rejected fact or remove it with a specific +reason. The plugin rejects missing or extra operations, duplicate fact numbers, +unrelated-fact changes, placeholders, stale state, changed original proposals, +invalid locators, broken source disposition, byte overflow, traversal, symlink +ancestors, changed repair receipts, and replay. + +On the next model request, the plugin combines the transaction with the +unchanged original proposal in memory and runs the unchanged full source +proposal validator. A valid receipt exposes only the exact +`sourceMergeRepairApplyCommand`; it does not expose the complete corrected +proposal or mutate canonical ledgers. + +The CLI revalidates state, original proposal, repair transaction, fragment +receipt, and normalized result, then atomically projects the corrected source +transaction into `sources.json` and `facts.json`. It writes a compact applied +receipt bound to the resulting canonical state. The hook accepts that receipt +only while it matches the current validator state and reviewed source rows. + +## Lease Semantics + +- The initial invalid proposal increments the existing `repairOrdinal` once. +- A successful `write_file` to the fresh repair path increments the existing + `repairPreparationOrdinal` once. +- Reading the original proposal, writing another path, replaying the same + repair, invalid repair content, repair validation, and the apply command do + not increment `progressOrdinal`. +- Only the applied receipt, after verification against the resulting canonical + source/fact state, creates one semantic progress checkpoint. +- No additional grace kind or threshold is introduced. + +Core remains domain-neutral. It continues to compare only opaque ordinals, +counts, and hashes; every source field, locator, diagnostic, transaction, and +receipt rule remains inside Legal Coverage. + +## Bounds + +- At most 32 rejected fact operations. +- At most 24 KiB per repair transaction. +- One deterministic repair path and one applied receipt per stable rejection. +- Existing valid facts and `noMaterialFacts` rows are preserved byte-for-value + in the reconstructed proposal. +- The unchanged full proposal and post-write workspace validators remain the + final acceptance authority. + +## Counterexamples + +Tests must prove that no canonical mutation occurs for an unknown fact number, +missing rejected fact, duplicate operation, extra valid-fact operation, +placeholder replacement, invalid locator, unsupported action, missing removal +reason, changed original proposal, changed repair file, stale state, replay, +overflow, traversal, or symlink path. + +Tests must also prove that wrong-path reads/writes and repeated writes do not +advance repair preparation; a valid repair receipt does not advance semantic +progress; atomic apply changes only the selected source/fact transaction; the +current applied receipt advances progress once; and its replay does not. + +## Verification Gate + +1. Build and run focused Legal Coverage, hook runtime, Progress Lease, Gateway, + compaction, and O1 tests. +2. Run the complete repository suite. +3. Drive a real isolated local Gateway with a mock model and real tools, + Router/Memory disabled, O1 diagnostic/4096, and evidence on disk. +4. Replay the preserved v2 Case 09 source failure in a disposable workspace and + prove `feedback_grace -> repair_preparation_grace -> atomic apply -> genuine + renewed progress` without reading or editing the original proposal. +5. Commit and push, assemble dependencies, and create a new immutable campaign. + Run paired smoke, Case 05, then Case 09. V25 and 85 cases remain blocked + until the complete Case 09 product Gate passes. From 0e4348cfc392e7857b052cd70a4bb84c1843a517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E8=BE=BE=E5=A5=87?= <> Date: Mon, 27 Jul 2026 07:52:04 +0800 Subject: [PATCH 3/4] test(legal): record V24R8 QA evidence --- .../QA.md | 158 ++++++++++++++++++ .../case09-replay-result.json | 36 ++++ 2 files changed, 194 insertions(+) create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/QA.md create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/case09-replay-result.json diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/QA.md b/.omo/evidence/20260727-v24r8-immutable-source-repair/QA.md new file mode 100644 index 00000000..9760133f --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/QA.md @@ -0,0 +1,158 @@ +# V24R8 Immutable Source Repair QA + +## Scope and boundary + +V24R8 changes only the Legal Coverage source-proposal repair protocol. It does +not change PilotDeck Core, validator acceptance, issue taxonomy, Progress Lease +thresholds `8/2`, Router, Memory, O1, V24R7 authority closure, the model, or the +evaluation corpus. + +The success criterion is narrow: a rejected bounded source proposal can be +corrected through one immutable repair transaction, revalidated against the +unchanged proposal and current state, atomically applied, and counted as +semantic progress only after a verified applied receipt. + +## Static and automated verification + +### Syntax and patch hygiene + +Commands: + +```sh +node --check products/legal/plugins/legal-coverage/hook.mjs +node --check products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs +node --check products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs +git diff --check +``` + +Observed: all commands exited `0`; `git diff --check` produced no diagnostics. + +Why enough: these checks cover JavaScript parse validity and whitespace errors. +They do not substitute for behavior tests. + +### Complete repository suite + +Command: + +```sh +pnpm test +``` + +First observation: `307/308` passed. The only failure was the pre-existing O1 +equivalence test during recursive temporary-directory cleanup: + +```text +ENOTEMPTY: directory not empty, rmdir '.../pilotdeck-observation-equivalence-.../home' +``` + +Artifact: `full-test.log`. + +The failed test was then isolated with: + +```sh +node --test --test-name-pattern='enabling O1 does not change Agent-visible model input' \ + dist/tests/observability/local-gateway-observation.spec.js +``` + +Observed: `1/1` passed. Artifact: `o1-isolated-rerun.log`. + +The complete `pnpm test` command was rerun without concurrent diagnostics. +Observed: `308/308` passed, including build, Legal Coverage, Progress Lease, +compaction, Gateway, and O1 tests. Artifact: `full-test-rerun.log`. + +Why enough: the clean complete rerun and clean isolated reproduction show that +the first result was a temporary cleanup race, not a reproducible V24R8 or O1 +behavior failure. No unrelated O1 code was changed. + +## Real Gateway QA + +Command: + +```sh +node --test --test-force-exit \ + dist/tests/agent/legal-coverage-plugin-runtime.spec.js +``` + +Observed: `4/4` passed. Artifact: `real-gateway.log`. + +The immutable-repair case drove a real local Gateway and real PilotDeck tools +against an isolated temporary `PILOT_HOME`. Its fixture configuration had: + +- Router disabled. +- Memory disabled. +- Telemetry disabled. +- O1 enabled with the `diagnostic` profile and queue capacity `4096`. +- A local deterministic mock model, so no external model request or credential + was used. + +The observed trajectory was: + +```text +source-fragment-repair + -> real write_file to the deterministic immutable repair path +source-fragment-repair-apply + -> real bash execution of the exact state-bound apply command +verified applied receipt + -> progressOrdinal 0 -> 1 +``` + +The rejected proposal bytes remained unchanged. O1 integrity was `complete`; +model requests, tool calls, and turns were paired, with zero dropped events. +The test also proves that preparation advances once on the required write and +that validation or command execution alone does not manufacture progress. + +Why enough: this exercises hook subscription, dynamic prompt injection, tool +execution, CLI revalidation, atomic canonical projection, receipt verification, +Progress Lease metadata, and O1 recording together on the actual Gateway +surface rather than only calling library functions. + +## Preserved Case 09 failure replay + +Manual action: the preserved V24R7 Case 09 rejected source proposal was copied +to a disposable workspace at: + +```text +/private/tmp/pilotdeck-v24r8-case09-replay.AMGjFo +``` + +The two rejected facts (`4` and `7`) were repaired through the V24R8 immutable +transaction and exact apply command. Artifact: `case09-replay-result.json`. + +Observed: + +```text +groups: repair -> repair-apply -> merge +repairPreparationOrdinal: 0 -> 1 -> 1 +progressOrdinal: 0 -> 0 -> 1 +proposal unchanged: true +applied source rows: 4 +applied fact rows: 19 +``` + +The post-apply validator exposed more downstream matrix and coverage work +(`89 -> 181` errors). That is expected: the new canonical facts make previously +unobservable work visible. Error-count reduction is not the progress signal; +the verified applied receipt is. + +Why enough: this reproduces the exact failure shape that motivated V24R8 and +shows that both rejected facts can cross the former read/edit lease boundary +without weakening validation or mutating the rejected proposal. + +## Counterexample coverage + +Automated tests reject missing, duplicate, unknown, and extra fact operations; +unsupported actions; placeholders; missing removal reasons; changed diagnostic +identity; invalid locators; byte overflow; changed proposals; changed repairs; +stale state; traversal; symlinked repairs; replay; missing, wrong-path, and +repeated preparation writes; and tampered applied receipts. Canonical ledgers +remain unchanged on every rejected transaction. + +## Omitted and residual risk + +No API keys, authorization headers, environment dumps, private source contents, +or raw production model logs are included in this evidence directory. + +The full 35-minute Case 09 product run with the production model has not yet +been executed. Therefore V24R8 is protocol- and replay-verified, but Case 09 is +not yet a product Gate pass. V25 and the 85-case campaign remain blocked until +a new immutable campaign passes Gate 0, paired smoke, Case 05, and Case 09. diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/case09-replay-result.json b/.omo/evidence/20260727-v24r8-immutable-source-repair/case09-replay-result.json new file mode 100644 index 00000000..c805be70 --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/case09-replay-result.json @@ -0,0 +1,36 @@ +{ + "replay": "v24r8-preserved-case09-source-failure", + "rejectedFactNumbers": [ + 4, + 7 + ], + "repairPath": ".pilotdeck/work/legal-coverage/fragments/source-repair-ded7e1630645.json", + "appliedReceiptPath": ".pilotdeck/work/legal-coverage/fragments/source-repair-applied-ded7e1630645.json", + "groups": [ + "source-fragment-repair", + "source-fragment-repair-apply", + "source-fragment-merge" + ], + "repairPreparationOrdinals": [ + 0, + 1, + 1 + ], + "progressOrdinals": [ + 0, + 0, + 1 + ], + "proposalUnchanged": true, + "applied": { + "sourceCount": 4, + "factCount": 19, + "errorCountBefore": 89, + "errorCountAfter": 181 + }, + "canonical": { + "reviewedSources": 8, + "pendingSources": 16, + "factCount": 34 + } +} From 625d1b4ce03d95785bad9f61fea2f61a18529ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E8=BE=BE=E5=A5=87?= <> Date: Mon, 27 Jul 2026 07:52:40 +0800 Subject: [PATCH 4/4] test(legal): preserve V24R8 command logs --- .../diff-check.log | 0 .../full-test-rerun.log | 1899 ++++++++++++++++ .../full-test.log | 1904 +++++++++++++++++ .../o1-isolated-rerun.log | 20 + .../real-gateway.log | 36 + 5 files changed, 3859 insertions(+) create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/diff-check.log create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/full-test-rerun.log create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/full-test.log create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/o1-isolated-rerun.log create mode 100644 .omo/evidence/20260727-v24r8-immutable-source-repair/real-gateway.log diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/diff-check.log b/.omo/evidence/20260727-v24r8-immutable-source-repair/diff-check.log new file mode 100644 index 00000000..e69de29b diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test-rerun.log b/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test-rerun.log new file mode 100644 index 00000000..5840cf84 --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test-rerun.log @@ -0,0 +1,1899 @@ + +> pilotdeck@0.1.0 test /Users/da/Documents/PilotDeck-worktrees/20260727-fix-convergence-v24r8-immutable-source-repair-v1 +> npm run build && node --test --test-force-exit --test-timeout 60000 "dist/tests/**/*.test.js" "dist/tests/**/*.spec.js" + + +> pilotdeck@0.1.0 prebuild +> npm run check:runtime && node scripts/bootstrap-pilotdeck-config.mjs && cd src/context/memory/edgeclaw-memory-core && npm run build + + +> pilotdeck@0.1.0 check:runtime +> node scripts/check-node-runtime.mjs + + +> edgeclaw-memory-core@0.0.0 build +> node -e "require('fs').rmSync('lib',{recursive:true,force:true})" && tsc -p tsconfig.json + + +> pilotdeck@0.1.0 build +> node -e "require('fs').rmSync('dist',{recursive:true,force:true})" && tsc -p tsconfig.json && node -e "require('fs').cpSync('src/extension/plugins/builtin','dist/src/extension/plugins/builtin',{recursive:true})" + +TAP version 13 +# Subtest: Feishu handles permission replies before the active chat drain finishes +ok 1 - Feishu handles permission replies before the active chat drain finishes + --- + duration_ms: 1.269583 + type: 'test' + ... +# Subtest: ImPermissionHelper resolves all pending permission requests for a chat +ok 2 - ImPermissionHelper resolves all pending permission requests for a chat + --- + duration_ms: 1.353375 + type: 'test' + ... +# Subtest: ImPermissionHelper keeps pending requests when the reply is invalid +ok 3 - ImPermissionHelper keeps pending requests when the reply is invalid + --- + duration_ms: 0.063541 + type: 'test' + ... +# (node:67159) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: PreModelRequest mutations survive a post-routing request rebuild and remain model-only +ok 4 - PreModelRequest mutations survive a post-routing request rebuild and remain model-only + --- + duration_ms: 14.343333 + type: 'test' + ... +# Subtest: AgentLoop exposes explicit subagent identity to lifecycle hooks +ok 5 - AgentLoop exposes explicit subagent identity to lifecycle hooks + --- + duration_ms: 1.267042 + type: 'test' + ... +# Subtest: evaluation progress lease stops before a third unchanged model request when full compaction is rejected +ok 6 - evaluation progress lease stops before a third unchanged model request when full compaction is rejected + --- + duration_ms: 8.502292 + type: 'test' + ... +# Subtest: AgentLoop delivers newly surfaced repair feedback once after an applied boundary +ok 7 - AgentLoop delivers newly surfaced repair feedback once after an applied boundary + --- + duration_ms: 5.6095 + type: 'test' + ... +# Subtest: AgentLoop permits one prepared-target request and then requires genuine progress +ok 8 - AgentLoop permits one prepared-target request and then requires genuine progress + --- + duration_ms: 5.813375 + type: 'test' + ... +# Subtest: AgentLoop carries a bounded handoff through the required boundary to genuine progress +ok 9 - AgentLoop carries a bounded handoff through the required boundary to genuine progress + --- + duration_ms: 2.927541 + type: 'test' + ... +# Subtest: artifact failure injects one bounded correction turn and succeeds after validation +ok 10 - artifact failure injects one bounded correction turn and succeeds after validation + --- + duration_ms: 11.935541 + type: 'test' + ... +# Subtest: required artifact failure wins over max-turn completion after a final tool call +ok 11 - required artifact failure wins over max-turn completion after a final tool call + --- + duration_ms: 1.827375 + type: 'test' + ... +# Subtest: AgentSession restores terminal state and dispatches SessionEnd when the runner throws +ok 12 - AgentSession restores terminal state and dispatches SessionEnd when the runner throws + --- + duration_ms: 1.427833 + type: 'test' + ... +# Subtest: AgentSession cleanup runs when a consumer stops reading early +ok 13 - AgentSession cleanup runs when a consumer stops reading early + --- + duration_ms: 0.095833 + type: 'test' + ... +# (node:67161) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: real gateway drives legal plugin milestones through bounded artifact correction +ok 14 - real gateway drives legal plugin milestones through bounded artifact correction + --- + duration_ms: 1584.050458 + type: 'test' + ... +# Subtest: real gateway blocks completion when the legal Stop hook cannot read session state +ok 15 - real gateway blocks completion when the legal Stop hook cannot read session state + --- + duration_ms: 912.26725 + type: 'test' + ... +# Subtest: real gateway executes the state-bound legal authority closure with complete O1 evidence +ok 16 - real gateway executes the state-bound legal authority closure with complete O1 evidence + --- + duration_ms: 785.270709 + type: 'test' + ... +# Subtest: real gateway applies an immutable source repair before renewing legal progress +ok 17 - real gateway applies an immutable source repair before renewing legal progress + --- + duration_ms: 784.803959 + type: 'test' + ... +# (node:67162) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: local gateway applies project hook context and enforces artifact correction +ok 18 - local gateway applies project hook context and enforces artifact correction + --- + duration_ms: 1014.754125 + type: 'test' + ... +# Subtest: ordinary edit failures after a successful write do not enter large-file repair +ok 19 - ordinary edit failures after a successful write do not enter large-file repair + --- + duration_ms: 0.743833 + type: 'test' + ... +# Subtest: an explicit post-draft large-file failure starts bounded recovery +ok 20 - an explicit post-draft large-file failure starts bounded recovery + --- + duration_ms: 0.166042 + type: 'test' + ... +# Subtest: successful focused write clears an active large-file repair episode +ok 21 - successful focused write clears an active large-file repair episode + --- + duration_ms: 0.156958 + type: 'test' + ... +# Subtest: permission failures are never reclassified as large-file recovery +ok 22 - permission failures are never reclassified as large-file recovery + --- + duration_ms: 0.04375 + type: 'test' + ... +# Subtest: progress lease stays inert unless explicitly enabled +ok 23 - progress lease stays inert unless explicitly enabled + --- + duration_ms: 0.879625 + type: 'test' + ... +# Subtest: opaque hash churn is stagnant while a smaller remaining count renews the lease +ok 24 - opaque hash churn is stagnant while a smaller remaining count renews the lease + --- + duration_ms: 0.156541 + type: 'test' + ... +# Subtest: only a strictly increasing domain progress ordinal renews the lease +ok 25 - only a strictly increasing domain progress ordinal renews the lease + --- + duration_ms: 0.062541 + type: 'test' + ... +# Subtest: a lower remaining count cannot roll the stored progress ordinal backward +ok 26 - a lower remaining count cannot roll the stored progress ordinal backward + --- + duration_ms: 0.239917 + type: 'test' + ... +# Subtest: two stagnant observations require a boundary and allow exactly one post-boundary turn +ok 27 - two stagnant observations require a boundary and allow exactly one post-boundary turn + --- + duration_ms: 0.057417 + type: 'test' + ... +# Subtest: new repair feedback after a boundary gets one delivery turn without renewing progress +ok 28 - new repair feedback after a boundary gets one delivery turn without renewing progress + --- + duration_ms: 0.151917 + type: 'test' + ... +# Subtest: genuine progress after repair feedback renews the lease +ok 29 - genuine progress after repair feedback renews the lease + --- + duration_ms: 0.045959 + type: 'test' + ... +# Subtest: a newly prepared repair target gets one non-progress request after feedback +ok 30 - a newly prepared repair target gets one non-progress request after feedback + --- + duration_ms: 0.0525 + type: 'test' + ... +# Subtest: genuine progress immediately after repair preparation renews the lease +ok 31 - genuine progress immediately after repair preparation renews the lease + --- + duration_ms: 0.174375 + type: 'test' + ... +# Subtest: preparation observed before feedback cannot be replayed after the boundary +ok 32 - preparation observed before feedback cannot be replayed after the boundary + --- + duration_ms: 0.232334 + type: 'test' + ... +# Subtest: a second repair revision cannot replace missing progress after feedback +ok 33 - a second repair revision cannot replace missing progress after feedback + --- + duration_ms: 0.07475 + type: 'test' + ... +# Subtest: repair feedback already delivered before a boundary cannot be replayed as grace +ok 34 - repair feedback already delivered before a boundary cannot be replayed as grace + --- + duration_ms: 0.034916 + type: 'test' + ... +# Subtest: a new operational handoff gets one bounded request without renewing progress +ok 35 - a new operational handoff gets one bounded request without renewing progress + --- + duration_ms: 0.0495 + type: 'test' + ... +# Subtest: a post-boundary handoff is bounded and its replay still fails closed +ok 36 - a post-boundary handoff is bounded and its replay still fails closed + --- + duration_ms: 0.030417 + type: 'test' + ... +# Subtest: simultaneous repair and handoff revisions cannot be redeemed on separate turns +ok 37 - simultaneous repair and handoff revisions cannot be redeemed on separate turns + --- + duration_ms: 0.035916 + type: 'test' + ... +# Subtest: a lower handoff ordinal cannot be replayed as grace +ok 38 - a lower handoff ordinal cannot be replayed as grace + --- + duration_ms: 0.024208 + type: 'test' + ... +# Subtest: handoff allowance has a hard per-progress-epoch limit and resets only on progress +ok 39 - handoff allowance has a hard per-progress-epoch limit and resets only on progress + --- + duration_ms: 0.031333 + type: 'test' + ... +# Subtest: handoff cannot bypass an unavailable required boundary +ok 40 - handoff cannot bypass an unavailable required boundary + --- + duration_ms: 0.024542 + type: 'test' + ... +# Subtest: sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint +ok 41 - sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint + --- + duration_ms: 2.851708 + type: 'test' + ... +# Subtest: cold-start allowance expires after the first explicit domain progress +ok 42 - cold-start allowance expires after the first explicit domain progress + --- + duration_ms: 0.059 + type: 'test' + ... +# Subtest: evaluation mode fails closed when the required boundary is rejected or unavailable +ok 43 - evaluation mode fails closed when the required boundary is rejected or unavailable + --- + duration_ms: 0.041542 + type: 'test' + ... +# Subtest: a completed report releases the tracked scope +ok 44 - a completed report releases the tracked scope + --- + duration_ms: 0.0325 + type: 'test' + ... +# Subtest: convergence metadata parser rejects malformed and oversized reports +ok 45 - convergence metadata parser rejects malformed and oversized reports + --- + duration_ms: 0.091958 + type: 'test' + ... +# Subtest: explore subagent does not probe tool safety before execution +ok 46 - explore subagent does not probe tool safety before execution + --- + duration_ms: 23.166958 + type: 'test' + ... +# Subtest: explore registry ignores an unallowed dynamic execute_code tool without probing it +ok 47 - explore registry ignores an unallowed dynamic execute_code tool without probing it + --- + duration_ms: 0.604 + type: 'test' + ... +# Subtest: read-only subagent evaluates bash safety from the real command +ok 48 - read-only subagent evaluates bash safety from the real command + --- + duration_ms: 8.303792 + type: 'test' + ... +# Subtest: read-only execute_code checks the real code instead of crashing on registry setup +ok 49 - read-only execute_code checks the real code instead of crashing on registry setup + --- + duration_ms: 0.498 + type: 'test' + ... +# Subtest: subagent budget defaults to ten minutes without a parent deadline +ok 50 - subagent budget defaults to ten minutes without a parent deadline + --- + duration_ms: 1.024542 + type: 'test' + ... +# Subtest: subagent budget preserves the parent handoff window +ok 51 - subagent budget preserves the parent handoff window + --- + duration_ms: 0.114333 + type: 'test' + ... +# Subtest: subagent budget directive exposes the effective bound and diminishing-return rule +ok 52 - subagent budget directive exposes the effective bound and diminishing-return rule + --- + duration_ms: 0.0925 + type: 'test' + ... +# Subtest: composed subagent timeout aborts with a typed reason +ok 53 - composed subagent timeout aborts with a typed reason + --- + duration_ms: 6.028542 + type: 'test' + ... +# Subtest: subagent operation returns control even when the callee ignores abort +ok 54 - subagent operation returns control even when the callee ignores abort + --- + duration_ms: 16.850875 + type: 'test' + ... +# Subtest: parent abort wins without being misclassified as a child timeout +ok 55 - parent abort wins without being misclassified as a child timeout + --- + duration_ms: 0.40625 + type: 'test' + ... +# Subtest: filterIncompleteToolCalls tolerates malformed messages without content +ok 56 - filterIncompleteToolCalls tolerates malformed messages without content + --- + duration_ms: 8.765209 + type: 'test' + ... +# Subtest: fork timeout closes child lifecycle and lets the parent finish +ok 57 - fork timeout closes child lifecycle and lets the parent finish + --- + duration_ms: 30.896 + type: 'test' + ... +# Subtest: turn environment provides an isolated PilotDeck-owned work directory +ok 58 - turn environment provides an isolated PilotDeck-owned work directory + --- + duration_ms: 2.205541 + type: 'test' + ... +# Subtest: turn environment inherits the process environment when no override is configured +ok 59 - turn environment inherits the process environment when no override is configured + --- + duration_ms: 0.293875 + type: 'test' + ... +# Subtest: validates a required artifact and keeps contracts isolated by session +ok 60 - validates a required artifact and keeps contracts isolated by session + --- + duration_ms: 7.5695 + type: 'test' + ... +# Subtest: missing or wrong-format artifacts fail with reviewer-readable issues +ok 61 - missing or wrong-format artifacts fail with reviewer-readable issues + --- + duration_ms: 5.291 + type: 'test' + ... +# Subtest: empty required artifacts fail validation +ok 62 - empty required artifacts fail validation + --- + duration_ms: 3.071791 + type: 'test' + ... +# Subtest: rejects traversal and symlink escapes before a domain validator runs +ok 63 - rejects traversal and symlink escapes before a domain validator runs + --- + duration_ms: 4.153833 + type: 'test' + ... +# Subtest: legal-specific validator data stays in the plugin validator +ok 64 - legal-specific validator data stays in the plugin validator + --- + duration_ms: 1.1255 + type: 'test' + ... +# Subtest: validator exceptions become structured failures instead of escaping the runtime +ok 65 - validator exceptions become structured failures instead of escaping the runtime + --- + duration_ms: 1.131166 + type: 'test' + ... +# Subtest: duplicate validator ids cannot override an existing validator +ok 66 - duplicate validator ids cannot override an existing validator + --- + duration_ms: 0.389125 + type: 'test' + ... +# Subtest: contract registration is atomic when one contract is invalid +ok 67 - contract registration is atomic when one contract is invalid + --- + duration_ms: 0.139667 + type: 'test' + ... +# Subtest: extension watcher ignores generated Python and Office runtime files +ok 68 - extension watcher ignores generated Python and Office runtime files + --- + duration_ms: 2.434417 + type: 'test' + ... +# Subtest: config bootstrap does not copy bundled skills into user storage +ok 69 - config bootstrap does not copy bundled skills into user storage + --- + duration_ms: 61.20125 + type: 'test' + ... +# Subtest: Office attachments are reported unsupported before size checks +ok 70 - Office attachments are reported unsupported before size checks + --- + duration_ms: 5.713459 + type: 'test' + ... +# Subtest: auto compaction reports summary success separately from rejected oversized output +ok 71 - auto compaction reports summary success separately from rejected oversized output + --- + duration_ms: 1.171916 + type: 'test' + ... +# Subtest: auto compaction marks a budget-clearing summary as applied +ok 72 - auto compaction marks a budget-clearing summary as applied + --- + duration_ms: 0.080084 + type: 'test' + ... +# Subtest: progress policy can force a full boundary while the token budget is still healthy +ok 73 - progress policy can force a full boundary while the token budget is still healthy + --- + duration_ms: 0.090166 + type: 'test' + ... +# Subtest: full compaction distinguishes a protected prefix with no summarizable messages from model failure +ok 74 - full compaction distinguishes a protected prefix with no summarizable messages from model failure + --- + duration_ms: 461.075667 + type: 'test' + ... +# Subtest: auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt +ok 75 - auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt + --- + duration_ms: 0.254792 + type: 'test' + ... +# Subtest: bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair +ok 76 - bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair + --- + duration_ms: 3.377542 + type: 'test' + ... +# Subtest: full compaction summarizes a real-shaped single-prompt Agent trajectory +ok 77 - full compaction summarizes a real-shaped single-prompt Agent trajectory + --- + duration_ms: 0.859375 + type: 'test' + ... +# Subtest: full compaction aligns a message-count tail boundary to complete tool turns +ok 78 - full compaction aligns a message-count tail boundary to complete tool turns + --- + duration_ms: 0.628542 + type: 'test' + ... +# Subtest: sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions +ok 79 - sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions + --- + duration_ms: 7.807417 + type: 'test' + ... +# Subtest: lifecycle context is injected as a transient model-only message +ok 80 - lifecycle context is injected as a transient model-only message + --- + duration_ms: 6.40975 + type: 'test' + ... +# Subtest: lifecycle without a dynamic store preserves legacy hook messages +ok 81 - lifecycle without a dynamic store preserves legacy hook messages + --- + duration_ms: 0.103417 + type: 'test' + ... +# Subtest: SessionEnd clears session-scoped runtime state even when a hook throws +ok 82 - SessionEnd clears session-scoped runtime state even when a hook throws + --- + duration_ms: 0.279208 + type: 'test' + ... +# Subtest: merges pending context by priority and consumes it once +ok 83 - merges pending context by priority and consumes it once + --- + duration_ms: 1.440333 + type: 'test' + ... +# Subtest: re-registering the same source and id replaces stale content without changing order +ok 84 - re-registering the same source and id replaces stale content without changing order + --- + duration_ms: 0.3395 + type: 'test' + ... +# Subtest: expired context is pruned without affecting another session +ok 85 - expired context is pruned without affecting another session + --- + duration_ms: 0.112916 + type: 'test' + ... +# Subtest: blank context is ignored +ok 86 - blank context is ignored + --- + duration_ms: 0.0815 + type: 'test' + ... +# Subtest: clearing a session does not disturb another session with a shared prefix +ok 87 - clearing a session does not disturb another session with a shared prefix + --- + duration_ms: 0.171917 + type: 'test' + ... +# Subtest: context budgets retain higher-priority entries and cap merged prompt size +ok 88 - context budgets retain higher-priority entries and cap merged prompt size + --- + duration_ms: 0.984459 + type: 'test' + ... +# Subtest: source and id tuples cannot collide through delimiter characters +ok 89 - source and id tuples cannot collide through delimiter characters + --- + duration_ms: 0.153 + type: 'test' + ... +# Subtest: default prompt does not require Markdown links for generated files +ok 90 - default prompt does not require Markdown links for generated files + --- + duration_ms: 1.663917 + type: 'test' + ... +# Subtest: default prompt does not advertise web search when the tool is unavailable +ok 91 - default prompt does not advertise web search when the tool is unavailable + --- + duration_ms: 0.116666 + type: 'test' + ... +# Subtest: available skills include the resolved SKILL.md path and bounded lookup guidance +ok 92 - available skills include the resolved SKILL.md path and bounded lookup guidance + --- + duration_ms: 1.797292 + type: 'test' + ... +# Subtest: budget snapshot uses conservative budget tokens for ratio while preserving real tokens +ok 93 - budget snapshot uses conservative budget tokens for ratio while preserving real tokens + --- + duration_ms: 1.070459 + type: 'test' + ... +# Subtest: tool text under token budget remains inline even when over legacy byte threshold +ok 94 - tool text under token budget remains inline even when over legacy byte threshold + --- + duration_ms: 5787.219917 + type: 'test' + ... +# Subtest: tool text over token budget is persisted with expanded grep-first preview +ok 95 - tool text over token budget is persisted with expanded grep-first preview + --- + duration_ms: 7.785375 + type: 'test' + ... +# Subtest: large tool error references preserve error semantics for model replay +ok 96 - large tool error references preserve error semantics for model replay + --- + duration_ms: 2.461917 + type: 'test' + ... +# Subtest: multibyte truncated tool result references advertise read_file access +ok 97 - multibyte truncated tool result references advertise read_file access + --- + duration_ms: 1.624166 + type: 'test' + ... +# Subtest: parses declarative artifact contracts without domain-specific knowledge +ok 98 - parses declarative artifact contracts without domain-specific knowledge + --- + duration_ms: 2.28775 + type: 'test' + ... +# (node:67229) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: an active domain plugin contributes only generic runtime contracts +ok 99 - an active domain plugin contributes only generic runtime contracts + --- + duration_ms: 1.284584 + type: 'test' + ... +# Subtest: internal prompts do not reactivate domain detection +ok 100 - internal prompts do not reactivate domain detection + --- + duration_ms: 0.084 + type: 'test' + ... +# Subtest: parses only the supported model request patch fields +ok 101 - parses only the supported model request patch fields + --- + duration_ms: 1.113375 + type: 'test' + ... +# Subtest: parses bounded dynamic context controls for hook-driven injection +ok 102 - parses bounded dynamic context controls for hook-driven injection + --- + duration_ms: 0.121042 + type: 'test' + ... +# Subtest: standalone skills expose only their slug without a parent-directory namespace +ok 103 - standalone skills expose only their slug without a parent-directory namespace + --- + duration_ms: 8.567375 + type: 'test' + ... +# Subtest: a plugin skill directory used as the configured base never derives a parent namespace +ok 104 - a plugin skill directory used as the configured base never derives a parent namespace + --- + duration_ms: 0.330083 + type: 'test' + ... +# Subtest: standalone skill precedence is project > user > builtin without legacy aliases +ok 105 - standalone skill precedence is project > user > builtin without legacy aliases + --- + duration_ms: 19.862125 + type: 'test' + ... +# Subtest: legacy bundled migration backs up only byte-identical bootstrap copies +ok 106 - legacy bundled migration backs up only byte-identical bootstrap copies + --- + duration_ms: 26.388875 + type: 'test' + ... +# Subtest: legacy bundled migration leaves user skills untouched when the release bundle is missing +ok 107 - legacy bundled migration leaves user skills untouched when the release bundle is missing + --- + duration_ms: 4.425125 + type: 'test' + ... +# Subtest: SkillManager lists built-ins separately and describes override relationships +ok 108 - SkillManager lists built-ins separately and describes override relationships + --- + duration_ms: 25.062541 + type: 'test' + ... +# Subtest: SkillManager permits reading but rejects mutations of built-in skills +ok 109 - SkillManager permits reading but rejects mutations of built-in skills + --- + duration_ms: 3.766417 + type: 'test' + ... +# Subtest: registered plain-text attachments with non-whitelisted names are described as read_file inspectable +ok 110 - registered plain-text attachments with non-whitelisted names are described as read_file inspectable + --- + duration_ms: 9.613375 + type: 'test' + ... +# Subtest: registered Office attachments are still described as not directly inspectable +ok 111 - registered Office attachments are still described as not directly inspectable + --- + duration_ms: 3.705375 + type: 'test' + ... +# Subtest: startPilotDeckServer listens before a background channel finishes starting +ok 112 - startPilotDeckServer listens before a background channel finishes starting + --- + duration_ms: 40.684583 + type: 'test' + ... +# (node:67238) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: browser-use args do not inherit generic proxy env by default +ok 113 - browser-use args do not inherit generic proxy env by default + --- + duration_ms: 2.072208 + type: 'test' + ... +# Subtest: browser-use args use explicit browser proxy server +ok 114 - browser-use args use explicit browser proxy server + --- + duration_ms: 0.189208 + type: 'test' + ... +# Subtest: browser-use args only inherit generic proxy env when opted in +ok 115 - browser-use args only inherit generic proxy env when opted in + --- + duration_ms: 0.121583 + type: 'test' + ... +# Subtest: browser-use args use config proxy when browser env proxy is absent +ok 116 - browser-use args use config proxy when browser env proxy is absent + --- + duration_ms: 0.233833 + type: 'test' + ... +# Subtest: browser-use args allow explicit direct mode to disable config proxy +ok 117 - browser-use args allow explicit direct mode to disable config proxy + --- + duration_ms: 0.142333 + type: 'test' + ... +# Subtest: internal prompt dispatch waits for the user turn and fully drains the synthetic turn +ok 118 - internal prompt dispatch waits for the user turn and fully drains the synthetic turn + --- + duration_ms: 6.886875 + type: 'test' + ... +# Subtest: mapAgentEvent propagates runId to streaming lifecycle boundaries +ok 119 - mapAgentEvent propagates runId to streaming lifecycle boundaries + --- + duration_ms: 1.51575 + type: 'test' + ... +# Subtest: mapAgentEvent projects every convergence ordinal +ok 120 - mapAgentEvent projects every convergence ordinal + --- + duration_ms: 1.083292 + type: 'test' + ... +# Subtest: defer returns immediately while a user turn is busy +ok 121 - defer returns immediately while a user turn is busy + --- + duration_ms: 0.993 + type: 'test' + ... +# Subtest: enqueue waits for idle and dispatches exactly once +ok 122 - enqueue waits for idle and dispatches exactly once + --- + duration_ms: 1.594208 + type: 'test' + ... +# Subtest: same semantic key coalesces in-flight and dedupes after completion +ok 123 - same semantic key coalesces in-flight and dedupes after completion + --- + duration_ms: 0.155625 + type: 'test' + ... +# Subtest: an idle-settle race retries when the gateway reports busy +ok 124 - an idle-settle race retries when the gateway reports busy + --- + duration_ms: 0.15025 + type: 'test' + ... +# Subtest: abort while queued does not run an internal turn +ok 125 - abort while queued does not run an internal turn + --- + duration_ms: 0.100291 + type: 'test' + ... +# Subtest: turn timeouts are reported distinctly from generic execution failures +ok 126 - turn timeouts are reported distinctly from generic execution failures + --- + duration_ms: 0.050583 + type: 'test' + ... +# Subtest: a zero recent-dedupe window disables completed-result deduplication +ok 127 - a zero recent-dedupe window disables completed-result deduplication + --- + duration_ms: 0.146166 + type: 'test' + ... +# Subtest: dispose prevents a queued prompt from starting after shutdown +ok 128 - dispose prevents a queued prompt from starting after shutdown + --- + duration_ms: 0.110917 + type: 'test' + ... +# Subtest: the public WebSocket boundary strips trusted in-process turn controls +ok 129 - the public WebSocket boundary strips trusted in-process turn controls + --- + duration_ms: 0.919625 + type: 'test' + ... +# Subtest: waitForIdle resolves only after the matching turn slot is released +ok 130 - waitForIdle resolves only after the matching turn slot is released + --- + duration_ms: 2.954917 + type: 'test' + ... +# Subtest: waitForIdle honors AbortSignal without releasing the turn +ok 131 - waitForIdle honors AbortSignal without releasing the turn + --- + duration_ms: 1.176542 + type: 'test' + ... +# Subtest: mapAgentEvent bounds live tool result previews at the gateway +ok 132 - mapAgentEvent bounds live tool result previews at the gateway + --- + duration_ms: 2.174334 + type: 'test' + ... +# Subtest: mapAgentEvent bounds large strings inside successful tool data +ok 133 - mapAgentEvent bounds large strings inside successful tool data + --- + duration_ms: 1.912167 + type: 'test' + ... +# Subtest: mapAgentEvent bounds subagent tool result content and preview +ok 134 - mapAgentEvent bounds subagent tool result content and preview + --- + duration_ms: 0.335916 + type: 'test' + ... +# Subtest: Gateway propagates its absolute turn deadline into the Agent session +ok 135 - Gateway propagates its absolute turn deadline into the Agent session + --- + duration_ms: 1.674834 + type: 'test' + ... +# Subtest: WeixinChannel.start returns while QR login is waiting +ok 136 - WeixinChannel.start returns while QR login is waiting + --- + duration_ms: 23.230542 + type: 'test' + ... +# Subtest: channel runtime status reporter persists the latest channel state +ok 137 - channel runtime status reporter persists the latest channel state + --- + duration_ms: 3.361583 + type: 'test' + ... +# Subtest: UI weixin QR route only reads runtime status +ok 138 - UI weixin QR route only reads runtime status + --- + duration_ms: 2.81725 + type: 'test' + ... +# Subtest: UI weixin QR begin route delegates to gateway prepare RPC +ok 139 - UI weixin QR begin route delegates to gateway prepare RPC + --- + duration_ms: 0.169125 + type: 'test' + ... +# Subtest: Gateway settings keeps existing status rendered during silent refresh +ok 140 - Gateway settings keeps existing status rendered during silent refresh + --- + duration_ms: 0.295875 + type: 'test' + ... +# Subtest: Gateway settings starts weixin QR by begin route and ignores stale runtime errors +ok 141 - Gateway settings starts weixin QR by begin route and ignores stale runtime errors + --- + duration_ms: 0.257875 + type: 'test' + ... +# Subtest: Gateway protocol exposes prepare_weixin_login RPC +ok 142 - Gateway protocol exposes prepare_weixin_login RPC + --- + duration_ms: 0.433291 + type: 'test' + ... +# Subtest: applies context, system messages, and restricted model request patches +ok 143 - applies context, system messages, and restricted model request patches + --- + duration_ms: 1.64 + type: 'test' + ... +# Subtest: blocks before any request mutation is used +ok 144 - blocks before any request mutation is used + --- + duration_ms: 0.064791 + type: 'test' + ... +# Subtest: McpClient keeps stdio clients idle before connection +ok 145 - McpClient keeps stdio clients idle before connection + --- + duration_ms: 18.828417 + type: 'test' + ... +# Subtest: McpClient constructs streamable_http transport without requiring stdio fields +ok 146 - McpClient constructs streamable_http transport without requiring stdio fields + --- + duration_ms: 0.078 + type: 'test' + ... +# Subtest: McpClient routes streamable_http fetches with bounded timeouts +ok 147 - McpClient routes streamable_http fetches with bounded timeouts + --- + duration_ms: 2.127834 + type: 'test' + ... +# Subtest: expandMcpString expands ${env:*} placeholders +ok 148 - expandMcpString expands ${env:*} placeholders + --- + duration_ms: 0.619166 + type: 'test' + ... +# Subtest: expandMcpString expands ${userHome} placeholder +ok 149 - expandMcpString expands ${userHome} placeholder + --- + duration_ms: 0.09125 + type: 'test' + ... +# Subtest: expandMcpString expands ~ prefix +ok 150 - expandMcpString expands ~ prefix + --- + duration_ms: 0.042167 + type: 'test' + ... +# Subtest: expandMcpString leaves unknown env vars as empty string +ok 151 - expandMcpString leaves unknown env vars as empty string + --- + duration_ms: 0.035375 + type: 'test' + ... +# Subtest: expandMcpString handles combined placeholders +ok 152 - expandMcpString handles combined placeholders + --- + duration_ms: 0.03975 + type: 'test' + ... +# Subtest: expandMcpConfig recursively expands objects and arrays +ok 153 - expandMcpConfig recursively expands objects and arrays + --- + duration_ms: 0.523459 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio env +ok 154 - parsePluginMcpServers expands ${env:*} in stdio env + --- + duration_ms: 0.144417 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio command before transport detection +ok 155 - parsePluginMcpServers expands ${env:*} in stdio command before transport detection + --- + duration_ms: 0.042 + type: 'test' + ... +# Subtest: parsePluginMcpServers drops empty expanded stdio command +ok 156 - parsePluginMcpServers drops empty expanded stdio command + --- + duration_ms: 0.185709 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${userHome} in stdio cwd +ok 157 - parsePluginMcpServers expands ${userHome} in stdio cwd + --- + duration_ms: 0.228209 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ~ in stdio args (backward compat) +ok 158 - parsePluginMcpServers expands ~ in stdio args (backward compat) + --- + duration_ms: 0.102917 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http url +ok 159 - parsePluginMcpServers expands ${env:*} in streamable_http url + --- + duration_ms: 0.041541 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http headers +ok 160 - parsePluginMcpServers expands ${env:*} in streamable_http headers + --- + duration_ms: 0.035916 + type: 'test' + ... +# Subtest: user config and plugin config resolve same placeholders consistently +ok 161 - user config and plugin config resolve same placeholders consistently + --- + duration_ms: 0.069291 + type: 'test' + ... +# Subtest: Anthropic-compatible terminated SSE errors remain retryable +ok 162 - Anthropic-compatible terminated SSE errors remain retryable + --- + duration_ms: 5.84525 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is omitted +ok 163 - catalog provider resolves api key from default env var when apiKey is omitted + --- + duration_ms: 1.474125 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is blank +ok 164 - catalog provider resolves api key from default env var when apiKey is blank + --- + duration_ms: 0.895 + type: 'test' + ... +# Subtest: normalizeModelError classifies common network failures +ok 165 - normalizeModelError classifies common network failures + --- + duration_ms: 29.481791 + type: 'test' + ... +# Subtest: model request failure preserves provider raw message before action guidance +ok 166 - model request failure preserves provider raw message before action guidance + --- + duration_ms: 2.047625 + type: 'test' + ... +# Subtest: model_not_found guidance points users to local model settings +ok 167 - model_not_found guidance points users to local model settings + --- + duration_ms: 1.770125 + type: 'test' + ... +# Subtest: stream idle timeout is classified as timeout with network and timeoutMs guidance +ok 168 - stream idle timeout is classified as timeout with network and timeoutMs guidance + --- + duration_ms: 0.549708 + type: 'test' + ... +# Subtest: provider terminated stream is classified as a retryable connection reset +ok 169 - provider terminated stream is classified as a retryable connection reset + --- + duration_ms: 0.403458 + type: 'test' + ... +# Subtest: billing and rate limit guidance distinguish provider-side fixes +ok 170 - billing and rate limit guidance distinguish provider-side fixes + --- + duration_ms: 0.173709 + type: 'test' + ... +# Subtest: unknown provider errors still give actionable settings and provider checks +ok 171 - unknown provider errors still give actionable settings and provider checks + --- + duration_ms: 0.08475 + type: 'test' + ... +# Subtest: OpenAI Responses usage reads cached tokens from input token details +ok 172 - OpenAI Responses usage reads cached tokens from input token details + --- + duration_ms: 0.586875 + type: 'test' + ... +# Subtest: Gemini usage counts thoughts tokens as output consumption +ok 173 - Gemini usage counts thoughts tokens as output consumption + --- + duration_ms: 0.085208 + type: 'test' + ... +# Subtest: Ollama provider uses catalog defaults and does not require apiKey +ok 174 - Ollama provider uses catalog defaults and does not require apiKey + --- + duration_ms: 0.718792 + type: 'test' + ... +# Subtest: Ollama provider builds OpenAI-compatible chat completions body +ok 175 - Ollama provider builds OpenAI-compatible chat completions body + --- + duration_ms: 5.138042 + type: 'test' + ... +# Subtest: model request builders tolerate malformed messages with missing content +ok 176 - model request builders tolerate malformed messages with missing content + --- + duration_ms: 0.899417 + type: 'test' + ... +# Subtest: cloneMessages normalizes malformed messages with missing content +ok 177 - cloneMessages normalizes malformed messages with missing content + --- + duration_ms: 0.516792 + type: 'test' + ... +# Subtest: streamModel retries Anthropic-compatible terminated SSE errors +ok 178 - streamModel retries Anthropic-compatible terminated SSE errors + --- + duration_ms: 11.254709 + type: 'test' + ... +# Subtest: networkFetch retries retryable status responses and then succeeds +ok 179 - networkFetch retries retryable status responses and then succeeds + --- + duration_ms: 2.940375 + type: 'test' + ... +# Subtest: networkFetch uses retry-after when calculating retry delay +ok 180 - networkFetch uses retry-after when calculating retry delay + --- + duration_ms: 0.171333 + type: 'test' + ... +# Subtest: networkFetch caps retry-after delays with maxDelayMs +ok 181 - networkFetch caps retry-after delays with maxDelayMs + --- + duration_ms: 0.051709 + type: 'test' + ... +# Subtest: networkFetch normalizes DNS and reset errors +ok 182 - networkFetch normalizes DNS and reset errors + --- + duration_ms: 0.098042 + type: 'test' + ... +# Subtest: networkFetch times out requests +ok 183 - networkFetch times out requests + --- + duration_ms: 3.782375 + type: 'test' + ... +# Subtest: networkFetch honors init.signal abort reasons without options.signal +ok 184 - networkFetch honors init.signal abort reasons without options.signal + --- + duration_ms: 0.552792 + type: 'test' + ... +# Subtest: networkFetch preserves parent NetworkFetchError reasons passed through options.signal +ok 185 - networkFetch preserves parent NetworkFetchError reasons passed through options.signal + --- + duration_ms: 0.389875 + type: 'test' + ... +# (node:67280) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# Subtest: local gateway writes a complete shadow bundle linked to the final request +ok 186 - local gateway writes a complete shadow bundle linked to the final request + --- + duration_ms: 752.11275 + type: 'test' + ... +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# Subtest: enabling O1 does not change Agent-visible model input +ok 187 - enabling O1 does not change Agent-visible model input + --- + duration_ms: 410.389875 + type: 'test' + ... +# (node:67284) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# [session-title] generation skipped (invalid_json): Unexpected token 'B', "Bounded co"... is not valid JSON +# Subtest: local Gateway records a bounded handoff through boundary to progress +ok 188 - local Gateway records a bounded handoff through boundary to progress + --- + duration_ms: 1241.188125 + type: 'test' + ... +# (node:67285) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: local Gateway records a complete O1 trajectory when a bounded child times out +ok 189 - local Gateway records a complete O1 trajectory when a bounded child times out + --- + duration_ms: 1632.837458 + type: 'test' + ... +# Subtest: subagent tool detection and result pair at the model projection layer +ok 190 - subagent tool detection and result pair at the model projection layer + --- + duration_ms: 10.151625 + type: 'test' + ... +# Subtest: main tool detection and result produce one exact O1 pair +ok 191 - main tool detection and result produce one exact O1 pair + --- + duration_ms: 1.136083 + type: 'test' + ... +# Subtest: repair preparation grace is observable without becoming progress +ok 192 - repair preparation grace is observable without becoming progress + --- + duration_ms: 0.134666 + type: 'test' + ... +# Subtest: recorder finalizes a complete hash-only trajectory +ok 193 - recorder finalizes a complete hash-only trajectory + --- + duration_ms: 10.517583 + type: 'test' + ... +# Subtest: queue overflow is explicit and makes integrity partial +ok 194 - queue overflow is explicit and makes integrity partial + --- + duration_ms: 5.301667 + type: 'test' + ... +# Subtest: verifier rejects reused model request identity and duplicate terminals +ok 195 - verifier rejects reused model request identity and duplicate terminals + --- + duration_ms: 4.017042 + type: 'test' + ... +# Subtest: verifier rejects duplicate identity and secret-bearing keys +ok 196 - verifier rejects duplicate identity and secret-bearing keys + --- + duration_ms: 0.254125 + type: 'test' + ... +# [PilotDeck] transientRetry: rate_limit (attempt 1/1, delay=1ms) +# Subtest: router observation closes every fallback and retry attempt +ok 197 - router observation closes every fallback and retry attempt + --- + duration_ms: 33.320333 + type: 'test' + ... +# Subtest: router request identity separates sessions sharing a turn id +ok 198 - router request identity separates sessions sharing a turn id + --- + duration_ms: 6.626667 + type: 'test' + ... +# Subtest: router request identity advances across sequential calls in one turn +ok 199 - router request identity advances across sequential calls in one turn + --- + duration_ms: 25.785959 + type: 'test' + ... +# Subtest: observability is disabled by default and preserves the O1 diagnostic profile +ok 200 - observability is disabled by default and preserves the O1 diagnostic profile + --- + duration_ms: 23.650875 + type: 'test' + ... +# Subtest: O1 rejects unsupported profiles and unsafe labels +ok 201 - O1 rejects unsupported profiles and unsafe labels + --- + duration_ms: 4.119542 + type: 'test' + ... +# Subtest: web search can be explicitly disabled without discarding provider config +ok 202 - web search can be explicitly disabled without discarding provider config + --- + duration_ms: 1.036083 + type: 'test' + ... +# Subtest: web search enabled remains optional for backwards compatibility +ok 203 - web search enabled remains optional for backwards compatibility + --- + duration_ms: 0.051333 + type: 'test' + ... +# Subtest: web search enabled must be a boolean +ok 204 - web search enabled must be a boolean + --- + duration_ms: 0.050208 + type: 'test' + ... +# Subtest: progress lease is disabled by default and opt-in evaluation config is preserved +ok 205 - progress lease is disabled by default and opt-in evaluation config is preserved + --- + duration_ms: 24.464041 + type: 'test' + ... +# Subtest: progress lease preserves an explicit cold-start allowance +ok 206 - progress lease preserves an explicit cold-start allowance + --- + duration_ms: 4.578083 + type: 'test' + ... +# Subtest: progress lease rejects non-evaluation modes +ok 207 - progress lease rejects non-evaluation modes + --- + duration_ms: 4.667334 + type: 'test' + ... +# Subtest: progress lease rejects a cold-start allowance below the steady-state limit +ok 208 - progress lease rejects a cold-start allowance below the steady-state limit + --- + duration_ms: 7.759541 + type: 'test' + ... +# Subtest: legal coverage validator creates a current proof and removes it when the deliverable changes +ok 209 - legal coverage validator creates a current proof and removes it when the deliverable changes + --- + duration_ms: 333.714375 + type: 'test' + ... +# Subtest: legal coverage initializer creates a text skeleton before source review and remains idempotent +ok 210 - legal coverage initializer creates a text skeleton before source review and remains idempotent + --- + duration_ms: 385.888667 + type: 'test' + ... +# Subtest: legal coverage initializer binds trusted manifests to original inputs +ok 211 - legal coverage initializer binds trusted manifests to original inputs + --- + duration_ms: 391.47525 + type: 'test' + ... +# Subtest: legal coverage source bootstrap creates only deterministic pending manifest rows +ok 212 - legal coverage source bootstrap creates only deterministic pending manifest rows + --- + duration_ms: 414.730875 + type: 'test' + ... +# Subtest: legal coverage injects deterministic disjoint worker batches for large pending source rooms +ok 213 - legal coverage injects deterministic disjoint worker batches for large pending source rooms + --- + duration_ms: 3206.632625 + type: 'test' + ... +# Subtest: legal coverage CLI exposes bundled guidance through stable named references +ok 214 - legal coverage CLI exposes bundled guidance through stable named references + --- + duration_ms: 134.472167 + type: 'test' + ... +# Subtest: legal coverage initializer creates only explicit text formats and preserves existing content +ok 215 - legal coverage initializer creates only explicit text formats and preserves existing content + --- + duration_ms: 53.18075 + type: 'test' + ... +# Subtest: legal coverage initializer rejects traversal and symlink ancestors without external writes +ok 216 - legal coverage initializer rejects traversal and symlink ancestors without external writes + --- + duration_ms: 143.261916 + type: 'test' + ... +# Subtest: legal coverage validator binds runner originals and derivations into its proof +ok 217 - legal coverage validator binds runner originals and derivations into its proof + --- + duration_ms: 210.571459 + type: 'test' + ... +# Subtest: legal coverage validator rejects missing lineage and mutable or derived input roots +ok 218 - legal coverage validator rejects missing lineage and mutable or derived input roots + --- + duration_ms: 263.275416 + type: 'test' + ... +# Subtest: legal coverage next-batch exposes one bounded deterministic repair slice +ok 219 - legal coverage next-batch exposes one bounded deterministic repair slice + --- + duration_ms: 207.792042 + type: 'test' + ... +# Subtest: legal coverage apply-batch atomically updates only the current bounded slice +ok 220 - legal coverage apply-batch atomically updates only the current bounded slice + --- + duration_ms: 306.993417 + type: 'test' + ... +# Subtest: legal coverage apply-batch rejects record and byte limit violations before writing +ok 221 - legal coverage apply-batch rejects record and byte limit violations before writing + --- + duration_ms: 198.587958 + type: 'test' + ... +# Subtest: legal coverage validator rejects orphaned conflicts and incomplete final disclosure +ok 222 - legal coverage validator rejects orphaned conflicts and incomplete final disclosure + --- + duration_ms: 100.971417 + type: 'test' + ... +# Subtest: legal coverage validator detects un-inventoried sources and cross-fact timeline collisions +ok 223 - legal coverage validator detects un-inventoried sources and cross-fact timeline collisions + --- + duration_ms: 107.544792 + type: 'test' + ... +# Subtest: legal coverage validator rejects reused generic quotes and unsupported fact coverage +ok 224 - legal coverage validator rejects reused generic quotes and unsupported fact coverage + --- + duration_ms: 101.977833 + type: 'test' + ... +# Subtest: legal coverage validator requires authority links for critical issues and legal-authority matrices +ok 225 - legal coverage validator requires authority links for critical issues and legal-authority matrices + --- + duration_ms: 102.264625 + type: 'test' + ... +# Subtest: legal coverage validator rejects non-object canonical JSON documents +ok 226 - legal coverage validator rejects non-object canonical JSON documents + --- + duration_ms: 705.644041 + type: 'test' + ... +# Subtest: legal coverage validator binds reviewed source bytes to the ledger and state hash +ok 227 - legal coverage validator binds reviewed source bytes to the ledger and state hash + --- + duration_ms: 204.919833 + type: 'test' + ... +# Subtest: legal coverage validator rejects ancestor symlinks and a symlinked proof +ok 228 - legal coverage validator rejects ancestor symlinks and a symlinked proof + --- + duration_ms: 152.754041 + type: 'test' + ... +# Subtest: legal coverage validator does not read or write through a symlinked state ancestor +ok 229 - legal coverage validator does not read or write through a symlinked state ancestor + --- + duration_ms: 98.317416 + type: 'test' + ... +# Subtest: legal coverage validator requires unresolved disclosure for unverified material facts +ok 230 - legal coverage validator requires unresolved disclosure for unverified material facts + --- + duration_ms: 101.535 + type: 'test' + ... +# Subtest: legal coverage validator rejects unique but irrelevant issue and authority quotes +ok 231 - legal coverage validator rejects unique but irrelevant issue and authority quotes + --- + duration_ms: 101.510041 + type: 'test' + ... +# Subtest: legal coverage validator enforces reciprocal and same-entry ledger relationships +ok 232 - legal coverage validator enforces reciprocal and same-entry ledger relationships + --- + duration_ms: 102.526458 + type: 'test' + ... +# Subtest: legal coverage validator accepts null for an optional threshold assessment +ok 233 - legal coverage validator accepts null for an optional threshold assessment + --- + duration_ms: 103.114875 + type: 'test' + ... +# Subtest: legal coverage validator uses locators without quotes for binary deliverables +ok 234 - legal coverage validator uses locators without quotes for binary deliverables + --- + duration_ms: 103.719167 + type: 'test' + ... +# Subtest: legal coverage validator rejects empty-fact shortcuts and material facts outside matrices +ok 235 - legal coverage validator rejects empty-fact shortcuts and material facts outside matrices + --- + duration_ms: 206.048042 + type: 'test' + ... +# Subtest: legal coverage hook activates only legal work and injects one observable milestone +ok 236 - legal coverage hook activates only legal work and injects one observable milestone + --- + duration_ms: 554.802417 + type: 'test' + ... +# Subtest: legal coverage hook groups repeated validator errors into one bounded milestone +ok 237 - legal coverage hook groups repeated validator errors into one bounded milestone + --- + duration_ms: 542.565125 + type: 'test' + ... +# Subtest: legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress +ok 238 - legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress + --- + duration_ms: 1046.946958 + type: 'test' + ... +# Subtest: legal coverage progress ordinal advances once for a new legal phase +ok 239 - legal coverage progress ordinal advances once for a new legal phase + --- + duration_ms: 224.104167 + type: 'test' + ... +# Subtest: legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals +ok 240 - legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals + --- + duration_ms: 508.18375 + type: 'test' + ... +# Subtest: legal coverage rejects oversized matrix evidence before making a selection receipt immutable +ok 241 - legal coverage rejects oversized matrix evidence before making a selection receipt immutable + --- + duration_ms: 165.214625 + type: 'test' + ... +# Subtest: legal coverage fails closed when one matrix index item exceeds the bounded page +ok 242 - legal coverage fails closed when one matrix index item exceeds the bounded page + --- + duration_ms: 163.986875 + type: 'test' + ... +# Subtest: legal coverage injects a bounded deterministic matrix relation-closure batch +ok 243 - legal coverage injects a bounded deterministic matrix relation-closure batch + --- + duration_ms: 221.453958 + type: 'test' + ... +# Subtest: legal coverage authority closure uses one bounded state-bound transaction +ok 244 - legal coverage authority closure uses one bounded state-bound transaction + --- + duration_ms: 598.642083 + type: 'test' + ... +# Subtest: legal coverage rejects changed and symlinked authority closure proposals without ledger mutation +ok 245 - legal coverage rejects changed and symlinked authority closure proposals without ledger mutation + --- + duration_ms: 256.910709 + type: 'test' + ... +# Subtest: legal coverage milestone digest ignores opaque incomplete-state hash churn +ok 246 - legal coverage milestone digest ignores opaque incomplete-state hash churn + --- + duration_ms: 2.487209 + type: 'test' + ... +# Subtest: legal coverage hook activates a malformed configured workspace so the agent can repair it +ok 247 - legal coverage hook activates a malformed configured workspace so the agent can repair it + --- + duration_ms: 158.309916 + type: 'test' + ... +# Subtest: legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace +ok 248 - legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace + --- + duration_ms: 50.98425 + type: 'test' + ... +# Subtest: legal product plugin loads one skill and contains no benchmark-specific controls +ok 249 - legal product plugin loads one skill and contains no benchmark-specific controls + --- + duration_ms: 3.45625 + type: 'test' + ... +# Subtest: file artifacts include every meaningful workspace change without an extension allowlist +ok 250 - file artifacts include every meaningful workspace change without an extension allowlist + --- + duration_ms: 59.3645 + type: 'test' + ... +# Subtest: file artifact fingerprints are reused for unchanged files across scans and turns +ok 251 - file artifact fingerprints are reused for unchanged files across scans and turns + --- + duration_ms: 14.238416 + type: 'test' + ... +# Subtest: TurnRunner emits and persists file artifacts before completing the turn +ok 252 - TurnRunner emits and persists file artifacts before completing the turn + --- + duration_ms: 20.169625 + type: 'test' + ... +# Subtest: TurnRunner does not collect generated files when artifacts are disabled +ok 253 - TurnRunner does not collect generated files when artifacts are disabled + --- + duration_ms: 2.834833 + type: 'test' + ... +# Subtest: bash success result is formatted with assertions, stdout, and stderr +ok 254 - bash success result is formatted with assertions, stdout, and stderr + --- + duration_ms: 3.711833 + type: 'test' + ... +# Subtest: bash allows hyphenated commands whose names merely start with next +ok 255 - bash allows hyphenated commands whose names merely start with next + --- + duration_ms: 3.066083 + type: 'test' + ... +# Subtest: bash still rejects exact framework CLI tokens that start long-lived processes +ok 256 - bash still rejects exact framework CLI tokens that start long-lived processes + --- + duration_ms: 2.005792 + type: 'test' + ... +# Subtest: bash failure tool result includes raw stdout and stderr tail for UI and model +ok 257 - bash failure tool result includes raw stdout and stderr tail for UI and model + --- + duration_ms: 17.321041 + type: 'test' + ... +# Subtest: bash failure diagnostic extracts Python traceback location and exception +ok 258 - bash failure diagnostic extracts Python traceback location and exception + --- + duration_ms: 1.381541 + type: 'test' + ... +# Subtest: bash success keeps full output for ToolResultBudget persistence +ok 259 - bash success keeps full output for ToolResultBudget persistence + --- + duration_ms: 6.031958 + type: 'test' + ... +# Subtest: agent tool accepts explorer as an alias for explore +ok 260 - agent tool accepts explorer as an alias for explore + --- + duration_ms: 2.497125 + type: 'test' + ... +# Subtest: agent tool defaults general-purpose to explore in ask mode +ok 261 - agent tool defaults general-purpose to explore in ask mode + --- + duration_ms: 0.123625 + type: 'test' + ... +# Subtest: agent tool injects and forwards the parent-bounded timeout +ok 262 - agent tool injects and forwards the parent-bounded timeout + --- + duration_ms: 0.186833 + type: 'test' + ... +# Subtest: agent tool rejects a launch that would consume the parent handoff window +ok 263 - agent tool rejects a launch that would consume the parent handoff window + --- + duration_ms: 0.247709 + type: 'test' + ... +# Subtest: agent fallback path enforces the same timeout +ok 264 - agent fallback path enforces the same timeout + --- + duration_ms: 7.086041 + type: 'test' + ... +# Subtest: agent tool preserves unknown custom fallback subagent names +ok 265 - agent tool preserves unknown custom fallback subagent names + --- + duration_ms: 0.594834 + type: 'test' + ... +# Subtest: execute_code read-only probe handles missing input +ok 266 - execute_code read-only probe handles missing input + --- + duration_ms: 1.14575 + type: 'test' + ... +# Subtest: disabling web search removes it from the registry but keeps web fetch +ok 267 - disabling web search removes it from the registry but keeps web fetch + --- + duration_ms: 2.617917 + type: 'test' + ... +# Subtest: execute_code rejects nested web search calls when web search is disabled +ok 268 - execute_code rejects nested web search calls when web search is disabled + --- + duration_ms: 0.274417 + type: 'test' + ... +# Subtest: read_skill returns the resolved SKILL.md path with the skill body +ok 269 - read_skill returns the resolved SKILL.md path with the skill body + --- + duration_ms: 1.1535 + type: 'test' + ... +# Subtest: read_skill preserves legacy content-only loading when metadata is unavailable +ok 270 - read_skill preserves legacy content-only loading when metadata is unavailable + --- + duration_ms: 0.133833 + type: 'test' + ... +# Subtest: web_search retries transient provider failures +ok 271 - web_search retries transient provider failures + --- + duration_ms: 576.844541 + type: 'test' + ... +# Subtest: web_search turns request timeout into tool_timeout +ok 272 - web_search turns request timeout into tool_timeout + --- + duration_ms: 1.1945 + type: 'test' + ... +# Subtest: web_search turns network timeout errors into tool_timeout +ok 273 - web_search turns network timeout errors into tool_timeout + --- + duration_ms: 2.228125 + type: 'test' + ... +# Subtest: write_file freshness error tells the model to read the file first +ok 274 - write_file freshness error tells the model to read the file first + --- + duration_ms: 0.955083 + type: 'test' + ... +# Subtest: write_file missing content points at the missing field and chunked recovery +ok 275 - write_file missing content points at the missing field and chunked recovery + --- + duration_ms: 0.255084 + type: 'test' + ... +# Subtest: invalid tool input keeps a bounded original error block +ok 276 - invalid tool input keeps a bounded original error block + --- + duration_ms: 0.207791 + type: 'test' + ... +# Subtest: edit_file old_string miss tells the model to reread and copy exact text +ok 277 - edit_file old_string miss tells the model to reread and copy exact text + --- + duration_ms: 0.061667 + type: 'test' + ... +# Subtest: read_file invalid range preserves the concrete issue +ok 278 - read_file invalid range preserves the concrete issue + --- + duration_ms: 0.108708 + type: 'test' + ... +# Subtest: bash missing command reports the required command parameter +ok 279 - bash missing command reports the required command parameter + --- + duration_ms: 0.098833 + type: 'test' + ... +# Subtest: bash timeout over max keeps foreground and task_wait guidance +ok 280 - bash timeout over max keeps foreground and task_wait guidance + --- + duration_ms: 0.1425 + type: 'test' + ... +# Subtest: bash background rejection keeps background-specific guidance +ok 281 - bash background rejection keeps background-specific guidance + --- + duration_ms: 0.075333 + type: 'test' + ... +# Subtest: write_file can create a new file without a prior read_file call +ok 282 - write_file can create a new file without a prior read_file call + --- + duration_ms: 5.79175 + type: 'test' + ... +# Subtest: edit_file can create a new file with empty old_string without a prior read_file call +ok 283 - edit_file can create a new file with empty old_string without a prior read_file call + --- + duration_ms: 3.101125 + type: 'test' + ... +# Subtest: read_file auto-pages large text files instead of failing +ok 284 - read_file auto-pages large text files instead of failing + --- + duration_ms: 2175.683875 + type: 'test' + ... +# Subtest: read_file rejects Office container files during validation +ok 285 - read_file rejects Office container files during validation + --- + duration_ms: 0.881709 + type: 'test' + ... +# Subtest: read_file explicit limit reads a large file range without auto paging +ok 286 - read_file explicit limit reads a large file range without auto paging + --- + duration_ms: 5.269959 + type: 'test' + ... +# Subtest: read_file auto-shrinks oversized persisted tool-result ref ranges +ok 287 - read_file auto-shrinks oversized persisted tool-result ref ranges + --- + duration_ms: 22615.731875 + type: 'test' + ... +# Subtest: read_file keeps explicit oversized ordinary file ranges strict +ok 288 - read_file keeps explicit oversized ordinary file ranges strict + --- + duration_ms: 2.420417 + type: 'test' + ... +# Subtest: read_file explicit limit records a ranged snapshot for follow-up edits +ok 289 - read_file explicit limit records a ranged snapshot for follow-up edits + --- + duration_ms: 1.750958 + type: 'test' + ... +# Subtest: read_file auto-paged large files record a ranged snapshot for follow-up edits +ok 290 - read_file auto-paged large files record a ranged snapshot for follow-up edits + --- + duration_ms: 1650.350958 + type: 'test' + ... +# Subtest: read_file returns a head-tail preview for a single oversized line +ok 291 - read_file returns a head-tail preview for a single oversized line + --- + duration_ms: 3.908125 + type: 'test' + ... +# Subtest: read_file classifies common Office formats as binary +ok 292 - read_file classifies common Office formats as binary + --- + duration_ms: 0.206583 + type: 'test' + ... +# Subtest: read_file rejects ranged binary input instead of hanging +ok 293 - read_file rejects ranged binary input instead of hanging + --- + duration_ms: 1.263875 + type: 'test' + ... +# Subtest: read_file range honors an aborted signal +ok 294 - read_file range honors an aborted signal + --- + duration_ms: 0.6045 + type: 'test' + ... +# Subtest: glob result text tells the model when more files are available +ok 295 - glob result text tells the model when more files are available + --- + duration_ms: 35.440917 + type: 'test' + ... +# Subtest: grep result text includes next offset when paginated +ok 296 - grep result text includes next offset when paginated + --- + duration_ms: 13.784041 + type: 'test' + ... +# Subtest: todo_write returns the actual todo list in model-visible content +ok 297 - todo_write returns the actual todo list in model-visible content + --- + duration_ms: 0.674458 + type: 'test' + ... +# Subtest: task_list returns model-visible status and next action hints +ok 298 - task_list returns model-visible status and next action hints + --- + duration_ms: 0.746958 + type: 'test' + ... +# Subtest: applyResultSizeLimit keeps both head and tail when truncating text output +ok 299 - applyResultSizeLimit keeps both head and tail when truncating text output + --- + duration_ms: 0.843792 + type: 'test' + ... +# Subtest: large tool results are persisted under workspace .pilotdeck and readable by read_file +ok 300 - large tool results are persisted under workspace .pilotdeck and readable by read_file + --- + duration_ms: 13.573875 + type: 'test' + ... +# Subtest: large tool result read_file aliases are short and sequential +ok 301 - large tool result read_file aliases are short and sequential + --- + duration_ms: 5.222708 + type: 'test' + ... +# Subtest: history replay restores structured agent file artifacts +ok 302 - history replay restores structured agent file artifacts + --- + duration_ms: 13.286792 + type: 'test' + ... +# Subtest: history replay hides Agent file artifacts in general conversations +ok 303 - history replay hides Agent file artifacts in general conversations + --- + duration_ms: 3.985709 + type: 'test' + ... +# Subtest: history replay preserves agent status i18n metadata and user hint +ok 304 - history replay preserves agent status i18n metadata and user hint + --- + duration_ms: 10.028833 + type: 'test' + ... +# Subtest: history token usage restores latest non-empty turn past latest empty turn result +ok 305 - history token usage restores latest non-empty turn past latest empty turn result + --- + duration_ms: 3.933375 + type: 'test' + ... +# Subtest: history token usage prefers persisted context budget snapshot +ok 306 - history token usage prefers persisted context budget snapshot + --- + duration_ms: 3.829 + type: 'test' + ... +# Subtest: web reducer merges persisted tool result detail path into existing tool result +ok 307 - web reducer merges persisted tool result detail path into existing tool result + --- + duration_ms: 1.306875 + type: 'test' + ... +# Subtest: web reducer bounds huge live tool result previews +ok 308 - web reducer bounds huge live tool result previews + --- + duration_ms: 0.178583 + type: 'test' + ... +1..308 +# tests 308 +# suites 0 +# pass 308 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 29182.34575 diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test.log b/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test.log new file mode 100644 index 00000000..2e353681 --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/full-test.log @@ -0,0 +1,1904 @@ + +> pilotdeck@0.1.0 test /Users/da/Documents/PilotDeck-worktrees/20260727-fix-convergence-v24r8-immutable-source-repair-v1 +> npm run build && node --test --test-force-exit --test-timeout 60000 "dist/tests/**/*.test.js" "dist/tests/**/*.spec.js" + + +> pilotdeck@0.1.0 prebuild +> npm run check:runtime && node scripts/bootstrap-pilotdeck-config.mjs && cd src/context/memory/edgeclaw-memory-core && npm run build + + +> pilotdeck@0.1.0 check:runtime +> node scripts/check-node-runtime.mjs + + +> edgeclaw-memory-core@0.0.0 build +> node -e "require('fs').rmSync('lib',{recursive:true,force:true})" && tsc -p tsconfig.json + + +> pilotdeck@0.1.0 build +> node -e "require('fs').rmSync('dist',{recursive:true,force:true})" && tsc -p tsconfig.json && node -e "require('fs').cpSync('src/extension/plugins/builtin','dist/src/extension/plugins/builtin',{recursive:true})" + +TAP version 13 +# Subtest: Feishu handles permission replies before the active chat drain finishes +ok 1 - Feishu handles permission replies before the active chat drain finishes + --- + duration_ms: 1.306417 + type: 'test' + ... +# Subtest: ImPermissionHelper resolves all pending permission requests for a chat +ok 2 - ImPermissionHelper resolves all pending permission requests for a chat + --- + duration_ms: 1.262958 + type: 'test' + ... +# Subtest: ImPermissionHelper keeps pending requests when the reply is invalid +ok 3 - ImPermissionHelper keeps pending requests when the reply is invalid + --- + duration_ms: 0.057333 + type: 'test' + ... +# (node:66052) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: PreModelRequest mutations survive a post-routing request rebuild and remain model-only +ok 4 - PreModelRequest mutations survive a post-routing request rebuild and remain model-only + --- + duration_ms: 6.694084 + type: 'test' + ... +# Subtest: AgentLoop exposes explicit subagent identity to lifecycle hooks +ok 5 - AgentLoop exposes explicit subagent identity to lifecycle hooks + --- + duration_ms: 0.363375 + type: 'test' + ... +# Subtest: evaluation progress lease stops before a third unchanged model request when full compaction is rejected +ok 6 - evaluation progress lease stops before a third unchanged model request when full compaction is rejected + --- + duration_ms: 7.366542 + type: 'test' + ... +# Subtest: AgentLoop delivers newly surfaced repair feedback once after an applied boundary +ok 7 - AgentLoop delivers newly surfaced repair feedback once after an applied boundary + --- + duration_ms: 2.859417 + type: 'test' + ... +# Subtest: AgentLoop permits one prepared-target request and then requires genuine progress +ok 8 - AgentLoop permits one prepared-target request and then requires genuine progress + --- + duration_ms: 3.46425 + type: 'test' + ... +# Subtest: AgentLoop carries a bounded handoff through the required boundary to genuine progress +ok 9 - AgentLoop carries a bounded handoff through the required boundary to genuine progress + --- + duration_ms: 5.016917 + type: 'test' + ... +# Subtest: artifact failure injects one bounded correction turn and succeeds after validation +ok 10 - artifact failure injects one bounded correction turn and succeeds after validation + --- + duration_ms: 18.405833 + type: 'test' + ... +# Subtest: required artifact failure wins over max-turn completion after a final tool call +ok 11 - required artifact failure wins over max-turn completion after a final tool call + --- + duration_ms: 3.915333 + type: 'test' + ... +# Subtest: AgentSession restores terminal state and dispatches SessionEnd when the runner throws +ok 12 - AgentSession restores terminal state and dispatches SessionEnd when the runner throws + --- + duration_ms: 3.089583 + type: 'test' + ... +# Subtest: AgentSession cleanup runs when a consumer stops reading early +ok 13 - AgentSession cleanup runs when a consumer stops reading early + --- + duration_ms: 0.202166 + type: 'test' + ... +# (node:66054) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: real gateway drives legal plugin milestones through bounded artifact correction +ok 14 - real gateway drives legal plugin milestones through bounded artifact correction + --- + duration_ms: 1485.101708 + type: 'test' + ... +# Subtest: real gateway blocks completion when the legal Stop hook cannot read session state +ok 15 - real gateway blocks completion when the legal Stop hook cannot read session state + --- + duration_ms: 863.160375 + type: 'test' + ... +# Subtest: real gateway executes the state-bound legal authority closure with complete O1 evidence +ok 16 - real gateway executes the state-bound legal authority closure with complete O1 evidence + --- + duration_ms: 825.665916 + type: 'test' + ... +# Subtest: real gateway applies an immutable source repair before renewing legal progress +ok 17 - real gateway applies an immutable source repair before renewing legal progress + --- + duration_ms: 803.499208 + type: 'test' + ... +# (node:66055) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: local gateway applies project hook context and enforces artifact correction +ok 18 - local gateway applies project hook context and enforces artifact correction + --- + duration_ms: 976.794834 + type: 'test' + ... +# Subtest: ordinary edit failures after a successful write do not enter large-file repair +ok 19 - ordinary edit failures after a successful write do not enter large-file repair + --- + duration_ms: 0.745791 + type: 'test' + ... +# Subtest: an explicit post-draft large-file failure starts bounded recovery +ok 20 - an explicit post-draft large-file failure starts bounded recovery + --- + duration_ms: 0.169458 + type: 'test' + ... +# Subtest: successful focused write clears an active large-file repair episode +ok 21 - successful focused write clears an active large-file repair episode + --- + duration_ms: 0.13925 + type: 'test' + ... +# Subtest: permission failures are never reclassified as large-file recovery +ok 22 - permission failures are never reclassified as large-file recovery + --- + duration_ms: 0.040417 + type: 'test' + ... +# Subtest: progress lease stays inert unless explicitly enabled +ok 23 - progress lease stays inert unless explicitly enabled + --- + duration_ms: 0.639375 + type: 'test' + ... +# Subtest: opaque hash churn is stagnant while a smaller remaining count renews the lease +ok 24 - opaque hash churn is stagnant while a smaller remaining count renews the lease + --- + duration_ms: 0.132208 + type: 'test' + ... +# Subtest: only a strictly increasing domain progress ordinal renews the lease +ok 25 - only a strictly increasing domain progress ordinal renews the lease + --- + duration_ms: 0.057 + type: 'test' + ... +# Subtest: a lower remaining count cannot roll the stored progress ordinal backward +ok 26 - a lower remaining count cannot roll the stored progress ordinal backward + --- + duration_ms: 0.078542 + type: 'test' + ... +# Subtest: two stagnant observations require a boundary and allow exactly one post-boundary turn +ok 27 - two stagnant observations require a boundary and allow exactly one post-boundary turn + --- + duration_ms: 0.052875 + type: 'test' + ... +# Subtest: new repair feedback after a boundary gets one delivery turn without renewing progress +ok 28 - new repair feedback after a boundary gets one delivery turn without renewing progress + --- + duration_ms: 0.145083 + type: 'test' + ... +# Subtest: genuine progress after repair feedback renews the lease +ok 29 - genuine progress after repair feedback renews the lease + --- + duration_ms: 0.048208 + type: 'test' + ... +# Subtest: a newly prepared repair target gets one non-progress request after feedback +ok 30 - a newly prepared repair target gets one non-progress request after feedback + --- + duration_ms: 0.052792 + type: 'test' + ... +# Subtest: genuine progress immediately after repair preparation renews the lease +ok 31 - genuine progress immediately after repair preparation renews the lease + --- + duration_ms: 0.17075 + type: 'test' + ... +# Subtest: preparation observed before feedback cannot be replayed after the boundary +ok 32 - preparation observed before feedback cannot be replayed after the boundary + --- + duration_ms: 0.223875 + type: 'test' + ... +# Subtest: a second repair revision cannot replace missing progress after feedback +ok 33 - a second repair revision cannot replace missing progress after feedback + --- + duration_ms: 0.076208 + type: 'test' + ... +# Subtest: repair feedback already delivered before a boundary cannot be replayed as grace +ok 34 - repair feedback already delivered before a boundary cannot be replayed as grace + --- + duration_ms: 0.035083 + type: 'test' + ... +# Subtest: a new operational handoff gets one bounded request without renewing progress +ok 35 - a new operational handoff gets one bounded request without renewing progress + --- + duration_ms: 0.052166 + type: 'test' + ... +# Subtest: a post-boundary handoff is bounded and its replay still fails closed +ok 36 - a post-boundary handoff is bounded and its replay still fails closed + --- + duration_ms: 0.033917 + type: 'test' + ... +# Subtest: simultaneous repair and handoff revisions cannot be redeemed on separate turns +ok 37 - simultaneous repair and handoff revisions cannot be redeemed on separate turns + --- + duration_ms: 0.035042 + type: 'test' + ... +# Subtest: a lower handoff ordinal cannot be replayed as grace +ok 38 - a lower handoff ordinal cannot be replayed as grace + --- + duration_ms: 0.023458 + type: 'test' + ... +# Subtest: handoff allowance has a hard per-progress-epoch limit and resets only on progress +ok 39 - handoff allowance has a hard per-progress-epoch limit and resets only on progress + --- + duration_ms: 0.032375 + type: 'test' + ... +# Subtest: handoff cannot bypass an unavailable required boundary +ok 40 - handoff cannot bypass an unavailable required boundary + --- + duration_ms: 0.026042 + type: 'test' + ... +# Subtest: sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint +ok 41 - sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint + --- + duration_ms: 2.559042 + type: 'test' + ... +# Subtest: cold-start allowance expires after the first explicit domain progress +ok 42 - cold-start allowance expires after the first explicit domain progress + --- + duration_ms: 0.059583 + type: 'test' + ... +# Subtest: evaluation mode fails closed when the required boundary is rejected or unavailable +ok 43 - evaluation mode fails closed when the required boundary is rejected or unavailable + --- + duration_ms: 0.039541 + type: 'test' + ... +# Subtest: a completed report releases the tracked scope +ok 44 - a completed report releases the tracked scope + --- + duration_ms: 0.033667 + type: 'test' + ... +# Subtest: convergence metadata parser rejects malformed and oversized reports +ok 45 - convergence metadata parser rejects malformed and oversized reports + --- + duration_ms: 0.09325 + type: 'test' + ... +# Subtest: explore subagent does not probe tool safety before execution +ok 46 - explore subagent does not probe tool safety before execution + --- + duration_ms: 27.8075 + type: 'test' + ... +# Subtest: explore registry ignores an unallowed dynamic execute_code tool without probing it +ok 47 - explore registry ignores an unallowed dynamic execute_code tool without probing it + --- + duration_ms: 0.329 + type: 'test' + ... +# Subtest: read-only subagent evaluates bash safety from the real command +ok 48 - read-only subagent evaluates bash safety from the real command + --- + duration_ms: 2.776208 + type: 'test' + ... +# Subtest: read-only execute_code checks the real code instead of crashing on registry setup +ok 49 - read-only execute_code checks the real code instead of crashing on registry setup + --- + duration_ms: 0.266917 + type: 'test' + ... +# Subtest: subagent budget defaults to ten minutes without a parent deadline +ok 50 - subagent budget defaults to ten minutes without a parent deadline + --- + duration_ms: 0.922583 + type: 'test' + ... +# Subtest: subagent budget preserves the parent handoff window +ok 51 - subagent budget preserves the parent handoff window + --- + duration_ms: 0.106584 + type: 'test' + ... +# Subtest: subagent budget directive exposes the effective bound and diminishing-return rule +ok 52 - subagent budget directive exposes the effective bound and diminishing-return rule + --- + duration_ms: 0.087625 + type: 'test' + ... +# Subtest: composed subagent timeout aborts with a typed reason +ok 53 - composed subagent timeout aborts with a typed reason + --- + duration_ms: 7.243667 + type: 'test' + ... +# Subtest: subagent operation returns control even when the callee ignores abort +ok 54 - subagent operation returns control even when the callee ignores abort + --- + duration_ms: 10.173667 + type: 'test' + ... +# Subtest: parent abort wins without being misclassified as a child timeout +ok 55 - parent abort wins without being misclassified as a child timeout + --- + duration_ms: 0.946166 + type: 'test' + ... +# Subtest: filterIncompleteToolCalls tolerates malformed messages without content +ok 56 - filterIncompleteToolCalls tolerates malformed messages without content + --- + duration_ms: 0.910583 + type: 'test' + ... +# Subtest: fork timeout closes child lifecycle and lets the parent finish +ok 57 - fork timeout closes child lifecycle and lets the parent finish + --- + duration_ms: 29.848167 + type: 'test' + ... +# Subtest: turn environment provides an isolated PilotDeck-owned work directory +ok 58 - turn environment provides an isolated PilotDeck-owned work directory + --- + duration_ms: 0.61125 + type: 'test' + ... +# Subtest: turn environment inherits the process environment when no override is configured +ok 59 - turn environment inherits the process environment when no override is configured + --- + duration_ms: 0.152333 + type: 'test' + ... +# Subtest: validates a required artifact and keeps contracts isolated by session +ok 60 - validates a required artifact and keeps contracts isolated by session + --- + duration_ms: 13.173792 + type: 'test' + ... +# Subtest: missing or wrong-format artifacts fail with reviewer-readable issues +ok 61 - missing or wrong-format artifacts fail with reviewer-readable issues + --- + duration_ms: 1.664958 + type: 'test' + ... +# Subtest: empty required artifacts fail validation +ok 62 - empty required artifacts fail validation + --- + duration_ms: 5.796667 + type: 'test' + ... +# Subtest: rejects traversal and symlink escapes before a domain validator runs +ok 63 - rejects traversal and symlink escapes before a domain validator runs + --- + duration_ms: 4.048375 + type: 'test' + ... +# Subtest: legal-specific validator data stays in the plugin validator +ok 64 - legal-specific validator data stays in the plugin validator + --- + duration_ms: 1.971542 + type: 'test' + ... +# Subtest: validator exceptions become structured failures instead of escaping the runtime +ok 65 - validator exceptions become structured failures instead of escaping the runtime + --- + duration_ms: 2.227792 + type: 'test' + ... +# Subtest: duplicate validator ids cannot override an existing validator +ok 66 - duplicate validator ids cannot override an existing validator + --- + duration_ms: 0.2205 + type: 'test' + ... +# Subtest: contract registration is atomic when one contract is invalid +ok 67 - contract registration is atomic when one contract is invalid + --- + duration_ms: 0.053875 + type: 'test' + ... +# Subtest: extension watcher ignores generated Python and Office runtime files +ok 68 - extension watcher ignores generated Python and Office runtime files + --- + duration_ms: 1.409834 + type: 'test' + ... +# Subtest: config bootstrap does not copy bundled skills into user storage +ok 69 - config bootstrap does not copy bundled skills into user storage + --- + duration_ms: 72.413333 + type: 'test' + ... +# Subtest: Office attachments are reported unsupported before size checks +ok 70 - Office attachments are reported unsupported before size checks + --- + duration_ms: 6.108458 + type: 'test' + ... +# Subtest: auto compaction reports summary success separately from rejected oversized output +ok 71 - auto compaction reports summary success separately from rejected oversized output + --- + duration_ms: 1.157917 + type: 'test' + ... +# Subtest: auto compaction marks a budget-clearing summary as applied +ok 72 - auto compaction marks a budget-clearing summary as applied + --- + duration_ms: 0.078666 + type: 'test' + ... +# Subtest: progress policy can force a full boundary while the token budget is still healthy +ok 73 - progress policy can force a full boundary while the token budget is still healthy + --- + duration_ms: 0.09025 + type: 'test' + ... +# Subtest: full compaction distinguishes a protected prefix with no summarizable messages from model failure +ok 74 - full compaction distinguishes a protected prefix with no summarizable messages from model failure + --- + duration_ms: 469.121 + type: 'test' + ... +# Subtest: auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt +ok 75 - auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt + --- + duration_ms: 0.139667 + type: 'test' + ... +# Subtest: bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair +ok 76 - bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair + --- + duration_ms: 1.27425 + type: 'test' + ... +# Subtest: full compaction summarizes a real-shaped single-prompt Agent trajectory +ok 77 - full compaction summarizes a real-shaped single-prompt Agent trajectory + --- + duration_ms: 1.960208 + type: 'test' + ... +# Subtest: full compaction aligns a message-count tail boundary to complete tool turns +ok 78 - full compaction aligns a message-count tail boundary to complete tool turns + --- + duration_ms: 1.125959 + type: 'test' + ... +# Subtest: sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions +ok 79 - sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions + --- + duration_ms: 13.627042 + type: 'test' + ... +# Subtest: lifecycle context is injected as a transient model-only message +ok 80 - lifecycle context is injected as a transient model-only message + --- + duration_ms: 3.903833 + type: 'test' + ... +# Subtest: lifecycle without a dynamic store preserves legacy hook messages +ok 81 - lifecycle without a dynamic store preserves legacy hook messages + --- + duration_ms: 0.092542 + type: 'test' + ... +# Subtest: SessionEnd clears session-scoped runtime state even when a hook throws +ok 82 - SessionEnd clears session-scoped runtime state even when a hook throws + --- + duration_ms: 0.270042 + type: 'test' + ... +# Subtest: merges pending context by priority and consumes it once +ok 83 - merges pending context by priority and consumes it once + --- + duration_ms: 1.081042 + type: 'test' + ... +# Subtest: re-registering the same source and id replaces stale content without changing order +ok 84 - re-registering the same source and id replaces stale content without changing order + --- + duration_ms: 0.111292 + type: 'test' + ... +# Subtest: expired context is pruned without affecting another session +ok 85 - expired context is pruned without affecting another session + --- + duration_ms: 0.059583 + type: 'test' + ... +# Subtest: blank context is ignored +ok 86 - blank context is ignored + --- + duration_ms: 0.035458 + type: 'test' + ... +# Subtest: clearing a session does not disturb another session with a shared prefix +ok 87 - clearing a session does not disturb another session with a shared prefix + --- + duration_ms: 0.085 + type: 'test' + ... +# Subtest: context budgets retain higher-priority entries and cap merged prompt size +ok 88 - context budgets retain higher-priority entries and cap merged prompt size + --- + duration_ms: 0.48275 + type: 'test' + ... +# Subtest: source and id tuples cannot collide through delimiter characters +ok 89 - source and id tuples cannot collide through delimiter characters + --- + duration_ms: 0.076 + type: 'test' + ... +# Subtest: default prompt does not require Markdown links for generated files +ok 90 - default prompt does not require Markdown links for generated files + --- + duration_ms: 1.194875 + type: 'test' + ... +# Subtest: default prompt does not advertise web search when the tool is unavailable +ok 91 - default prompt does not advertise web search when the tool is unavailable + --- + duration_ms: 0.107125 + type: 'test' + ... +# Subtest: available skills include the resolved SKILL.md path and bounded lookup guidance +ok 92 - available skills include the resolved SKILL.md path and bounded lookup guidance + --- + duration_ms: 1.760292 + type: 'test' + ... +# Subtest: budget snapshot uses conservative budget tokens for ratio while preserving real tokens +ok 93 - budget snapshot uses conservative budget tokens for ratio while preserving real tokens + --- + duration_ms: 4.4675 + type: 'test' + ... +# Subtest: tool text under token budget remains inline even when over legacy byte threshold +ok 94 - tool text under token budget remains inline even when over legacy byte threshold + --- + duration_ms: 5571.079458 + type: 'test' + ... +# Subtest: tool text over token budget is persisted with expanded grep-first preview +ok 95 - tool text over token budget is persisted with expanded grep-first preview + --- + duration_ms: 9.595416 + type: 'test' + ... +# Subtest: large tool error references preserve error semantics for model replay +ok 96 - large tool error references preserve error semantics for model replay + --- + duration_ms: 2.153542 + type: 'test' + ... +# Subtest: multibyte truncated tool result references advertise read_file access +ok 97 - multibyte truncated tool result references advertise read_file access + --- + duration_ms: 1.389375 + type: 'test' + ... +# Subtest: parses declarative artifact contracts without domain-specific knowledge +ok 98 - parses declarative artifact contracts without domain-specific knowledge + --- + duration_ms: 1.446791 + type: 'test' + ... +# (node:66125) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: an active domain plugin contributes only generic runtime contracts +ok 99 - an active domain plugin contributes only generic runtime contracts + --- + duration_ms: 2.529292 + type: 'test' + ... +# Subtest: internal prompts do not reactivate domain detection +ok 100 - internal prompts do not reactivate domain detection + --- + duration_ms: 0.182541 + type: 'test' + ... +# Subtest: parses only the supported model request patch fields +ok 101 - parses only the supported model request patch fields + --- + duration_ms: 2.302209 + type: 'test' + ... +# Subtest: parses bounded dynamic context controls for hook-driven injection +ok 102 - parses bounded dynamic context controls for hook-driven injection + --- + duration_ms: 0.236084 + type: 'test' + ... +# Subtest: standalone skills expose only their slug without a parent-directory namespace +ok 103 - standalone skills expose only their slug without a parent-directory namespace + --- + duration_ms: 10.168708 + type: 'test' + ... +# Subtest: a plugin skill directory used as the configured base never derives a parent namespace +ok 104 - a plugin skill directory used as the configured base never derives a parent namespace + --- + duration_ms: 0.308458 + type: 'test' + ... +# Subtest: standalone skill precedence is project > user > builtin without legacy aliases +ok 105 - standalone skill precedence is project > user > builtin without legacy aliases + --- + duration_ms: 16.691916 + type: 'test' + ... +# Subtest: legacy bundled migration backs up only byte-identical bootstrap copies +ok 106 - legacy bundled migration backs up only byte-identical bootstrap copies + --- + duration_ms: 25.562417 + type: 'test' + ... +# Subtest: legacy bundled migration leaves user skills untouched when the release bundle is missing +ok 107 - legacy bundled migration leaves user skills untouched when the release bundle is missing + --- + duration_ms: 3.565916 + type: 'test' + ... +# Subtest: SkillManager lists built-ins separately and describes override relationships +ok 108 - SkillManager lists built-ins separately and describes override relationships + --- + duration_ms: 48.779542 + type: 'test' + ... +# Subtest: SkillManager permits reading but rejects mutations of built-in skills +ok 109 - SkillManager permits reading but rejects mutations of built-in skills + --- + duration_ms: 5.2675 + type: 'test' + ... +# Subtest: registered plain-text attachments with non-whitelisted names are described as read_file inspectable +ok 110 - registered plain-text attachments with non-whitelisted names are described as read_file inspectable + --- + duration_ms: 11.831708 + type: 'test' + ... +# Subtest: registered Office attachments are still described as not directly inspectable +ok 111 - registered Office attachments are still described as not directly inspectable + --- + duration_ms: 2.314625 + type: 'test' + ... +# Subtest: startPilotDeckServer listens before a background channel finishes starting +ok 112 - startPilotDeckServer listens before a background channel finishes starting + --- + duration_ms: 28.760375 + type: 'test' + ... +# (node:66134) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: browser-use args do not inherit generic proxy env by default +ok 113 - browser-use args do not inherit generic proxy env by default + --- + duration_ms: 1.015667 + type: 'test' + ... +# Subtest: browser-use args use explicit browser proxy server +ok 114 - browser-use args use explicit browser proxy server + --- + duration_ms: 0.08775 + type: 'test' + ... +# Subtest: browser-use args only inherit generic proxy env when opted in +ok 115 - browser-use args only inherit generic proxy env when opted in + --- + duration_ms: 0.062791 + type: 'test' + ... +# Subtest: browser-use args use config proxy when browser env proxy is absent +ok 116 - browser-use args use config proxy when browser env proxy is absent + --- + duration_ms: 0.086 + type: 'test' + ... +# Subtest: browser-use args allow explicit direct mode to disable config proxy +ok 117 - browser-use args allow explicit direct mode to disable config proxy + --- + duration_ms: 0.055584 + type: 'test' + ... +# Subtest: internal prompt dispatch waits for the user turn and fully drains the synthetic turn +ok 118 - internal prompt dispatch waits for the user turn and fully drains the synthetic turn + --- + duration_ms: 3.25575 + type: 'test' + ... +# Subtest: mapAgentEvent propagates runId to streaming lifecycle boundaries +ok 119 - mapAgentEvent propagates runId to streaming lifecycle boundaries + --- + duration_ms: 0.740916 + type: 'test' + ... +# Subtest: mapAgentEvent projects every convergence ordinal +ok 120 - mapAgentEvent projects every convergence ordinal + --- + duration_ms: 0.515083 + type: 'test' + ... +# Subtest: defer returns immediately while a user turn is busy +ok 121 - defer returns immediately while a user turn is busy + --- + duration_ms: 1.018458 + type: 'test' + ... +# Subtest: enqueue waits for idle and dispatches exactly once +ok 122 - enqueue waits for idle and dispatches exactly once + --- + duration_ms: 1.590541 + type: 'test' + ... +# Subtest: same semantic key coalesces in-flight and dedupes after completion +ok 123 - same semantic key coalesces in-flight and dedupes after completion + --- + duration_ms: 0.15175 + type: 'test' + ... +# Subtest: an idle-settle race retries when the gateway reports busy +ok 124 - an idle-settle race retries when the gateway reports busy + --- + duration_ms: 0.147125 + type: 'test' + ... +# Subtest: abort while queued does not run an internal turn +ok 125 - abort while queued does not run an internal turn + --- + duration_ms: 0.103375 + type: 'test' + ... +# Subtest: turn timeouts are reported distinctly from generic execution failures +ok 126 - turn timeouts are reported distinctly from generic execution failures + --- + duration_ms: 0.049458 + type: 'test' + ... +# Subtest: a zero recent-dedupe window disables completed-result deduplication +ok 127 - a zero recent-dedupe window disables completed-result deduplication + --- + duration_ms: 0.144833 + type: 'test' + ... +# Subtest: dispose prevents a queued prompt from starting after shutdown +ok 128 - dispose prevents a queued prompt from starting after shutdown + --- + duration_ms: 0.114875 + type: 'test' + ... +# Subtest: the public WebSocket boundary strips trusted in-process turn controls +ok 129 - the public WebSocket boundary strips trusted in-process turn controls + --- + duration_ms: 0.927833 + type: 'test' + ... +# Subtest: waitForIdle resolves only after the matching turn slot is released +ok 130 - waitForIdle resolves only after the matching turn slot is released + --- + duration_ms: 1.463041 + type: 'test' + ... +# Subtest: waitForIdle honors AbortSignal without releasing the turn +ok 131 - waitForIdle honors AbortSignal without releasing the turn + --- + duration_ms: 0.567083 + type: 'test' + ... +# Subtest: mapAgentEvent bounds live tool result previews at the gateway +ok 132 - mapAgentEvent bounds live tool result previews at the gateway + --- + duration_ms: 1.625 + type: 'test' + ... +# Subtest: mapAgentEvent bounds large strings inside successful tool data +ok 133 - mapAgentEvent bounds large strings inside successful tool data + --- + duration_ms: 0.686417 + type: 'test' + ... +# Subtest: mapAgentEvent bounds subagent tool result content and preview +ok 134 - mapAgentEvent bounds subagent tool result content and preview + --- + duration_ms: 0.323084 + type: 'test' + ... +# Subtest: Gateway propagates its absolute turn deadline into the Agent session +ok 135 - Gateway propagates its absolute turn deadline into the Agent session + --- + duration_ms: 1.661708 + type: 'test' + ... +# Subtest: WeixinChannel.start returns while QR login is waiting +ok 136 - WeixinChannel.start returns while QR login is waiting + --- + duration_ms: 17.202625 + type: 'test' + ... +# Subtest: channel runtime status reporter persists the latest channel state +ok 137 - channel runtime status reporter persists the latest channel state + --- + duration_ms: 2.642125 + type: 'test' + ... +# Subtest: UI weixin QR route only reads runtime status +ok 138 - UI weixin QR route only reads runtime status + --- + duration_ms: 1.347 + type: 'test' + ... +# Subtest: UI weixin QR begin route delegates to gateway prepare RPC +ok 139 - UI weixin QR begin route delegates to gateway prepare RPC + --- + duration_ms: 0.172584 + type: 'test' + ... +# Subtest: Gateway settings keeps existing status rendered during silent refresh +ok 140 - Gateway settings keeps existing status rendered during silent refresh + --- + duration_ms: 0.315792 + type: 'test' + ... +# Subtest: Gateway settings starts weixin QR by begin route and ignores stale runtime errors +ok 141 - Gateway settings starts weixin QR by begin route and ignores stale runtime errors + --- + duration_ms: 0.273875 + type: 'test' + ... +# Subtest: Gateway protocol exposes prepare_weixin_login RPC +ok 142 - Gateway protocol exposes prepare_weixin_login RPC + --- + duration_ms: 0.459334 + type: 'test' + ... +# Subtest: applies context, system messages, and restricted model request patches +ok 143 - applies context, system messages, and restricted model request patches + --- + duration_ms: 0.983583 + type: 'test' + ... +# Subtest: blocks before any request mutation is used +ok 144 - blocks before any request mutation is used + --- + duration_ms: 0.054375 + type: 'test' + ... +# Subtest: McpClient keeps stdio clients idle before connection +ok 145 - McpClient keeps stdio clients idle before connection + --- + duration_ms: 0.559917 + type: 'test' + ... +# Subtest: McpClient constructs streamable_http transport without requiring stdio fields +ok 146 - McpClient constructs streamable_http transport without requiring stdio fields + --- + duration_ms: 0.169583 + type: 'test' + ... +# Subtest: McpClient routes streamable_http fetches with bounded timeouts +ok 147 - McpClient routes streamable_http fetches with bounded timeouts + --- + duration_ms: 1.88625 + type: 'test' + ... +# Subtest: expandMcpString expands ${env:*} placeholders +ok 148 - expandMcpString expands ${env:*} placeholders + --- + duration_ms: 0.616625 + type: 'test' + ... +# Subtest: expandMcpString expands ${userHome} placeholder +ok 149 - expandMcpString expands ${userHome} placeholder + --- + duration_ms: 0.09425 + type: 'test' + ... +# Subtest: expandMcpString expands ~ prefix +ok 150 - expandMcpString expands ~ prefix + --- + duration_ms: 0.042125 + type: 'test' + ... +# Subtest: expandMcpString leaves unknown env vars as empty string +ok 151 - expandMcpString leaves unknown env vars as empty string + --- + duration_ms: 0.032667 + type: 'test' + ... +# Subtest: expandMcpString handles combined placeholders +ok 152 - expandMcpString handles combined placeholders + --- + duration_ms: 0.039541 + type: 'test' + ... +# Subtest: expandMcpConfig recursively expands objects and arrays +ok 153 - expandMcpConfig recursively expands objects and arrays + --- + duration_ms: 0.518167 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio env +ok 154 - parsePluginMcpServers expands ${env:*} in stdio env + --- + duration_ms: 0.139833 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio command before transport detection +ok 155 - parsePluginMcpServers expands ${env:*} in stdio command before transport detection + --- + duration_ms: 0.043666 + type: 'test' + ... +# Subtest: parsePluginMcpServers drops empty expanded stdio command +ok 156 - parsePluginMcpServers drops empty expanded stdio command + --- + duration_ms: 0.186541 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${userHome} in stdio cwd +ok 157 - parsePluginMcpServers expands ${userHome} in stdio cwd + --- + duration_ms: 0.229042 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ~ in stdio args (backward compat) +ok 158 - parsePluginMcpServers expands ~ in stdio args (backward compat) + --- + duration_ms: 0.109208 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http url +ok 159 - parsePluginMcpServers expands ${env:*} in streamable_http url + --- + duration_ms: 0.041334 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http headers +ok 160 - parsePluginMcpServers expands ${env:*} in streamable_http headers + --- + duration_ms: 0.035916 + type: 'test' + ... +# Subtest: user config and plugin config resolve same placeholders consistently +ok 161 - user config and plugin config resolve same placeholders consistently + --- + duration_ms: 0.038208 + type: 'test' + ... +# Subtest: Anthropic-compatible terminated SSE errors remain retryable +ok 162 - Anthropic-compatible terminated SSE errors remain retryable + --- + duration_ms: 3.338084 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is omitted +ok 163 - catalog provider resolves api key from default env var when apiKey is omitted + --- + duration_ms: 0.7725 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is blank +ok 164 - catalog provider resolves api key from default env var when apiKey is blank + --- + duration_ms: 0.083041 + type: 'test' + ... +# Subtest: normalizeModelError classifies common network failures +ok 165 - normalizeModelError classifies common network failures + --- + duration_ms: 2.606875 + type: 'test' + ... +# Subtest: model request failure preserves provider raw message before action guidance +ok 166 - model request failure preserves provider raw message before action guidance + --- + duration_ms: 1.684042 + type: 'test' + ... +# Subtest: model_not_found guidance points users to local model settings +ok 167 - model_not_found guidance points users to local model settings + --- + duration_ms: 0.784583 + type: 'test' + ... +# Subtest: stream idle timeout is classified as timeout with network and timeoutMs guidance +ok 168 - stream idle timeout is classified as timeout with network and timeoutMs guidance + --- + duration_ms: 0.272916 + type: 'test' + ... +# Subtest: provider terminated stream is classified as a retryable connection reset +ok 169 - provider terminated stream is classified as a retryable connection reset + --- + duration_ms: 0.197792 + type: 'test' + ... +# Subtest: billing and rate limit guidance distinguish provider-side fixes +ok 170 - billing and rate limit guidance distinguish provider-side fixes + --- + duration_ms: 0.079875 + type: 'test' + ... +# Subtest: unknown provider errors still give actionable settings and provider checks +ok 171 - unknown provider errors still give actionable settings and provider checks + --- + duration_ms: 0.039167 + type: 'test' + ... +# Subtest: OpenAI Responses usage reads cached tokens from input token details +ok 172 - OpenAI Responses usage reads cached tokens from input token details + --- + duration_ms: 0.631583 + type: 'test' + ... +# Subtest: Gemini usage counts thoughts tokens as output consumption +ok 173 - Gemini usage counts thoughts tokens as output consumption + --- + duration_ms: 0.089541 + type: 'test' + ... +# Subtest: Ollama provider uses catalog defaults and does not require apiKey +ok 174 - Ollama provider uses catalog defaults and does not require apiKey + --- + duration_ms: 0.7615 + type: 'test' + ... +# Subtest: Ollama provider builds OpenAI-compatible chat completions body +ok 175 - Ollama provider builds OpenAI-compatible chat completions body + --- + duration_ms: 0.834333 + type: 'test' + ... +# Subtest: model request builders tolerate malformed messages with missing content +ok 176 - model request builders tolerate malformed messages with missing content + --- + duration_ms: 0.850125 + type: 'test' + ... +# Subtest: cloneMessages normalizes malformed messages with missing content +ok 177 - cloneMessages normalizes malformed messages with missing content + --- + duration_ms: 0.495542 + type: 'test' + ... +# Subtest: streamModel retries Anthropic-compatible terminated SSE errors +ok 178 - streamModel retries Anthropic-compatible terminated SSE errors + --- + duration_ms: 9.232625 + type: 'test' + ... +# Subtest: networkFetch retries retryable status responses and then succeeds +ok 179 - networkFetch retries retryable status responses and then succeeds + --- + duration_ms: 2.535166 + type: 'test' + ... +# Subtest: networkFetch uses retry-after when calculating retry delay +ok 180 - networkFetch uses retry-after when calculating retry delay + --- + duration_ms: 0.063709 + type: 'test' + ... +# Subtest: networkFetch caps retry-after delays with maxDelayMs +ok 181 - networkFetch caps retry-after delays with maxDelayMs + --- + duration_ms: 0.033375 + type: 'test' + ... +# Subtest: networkFetch normalizes DNS and reset errors +ok 182 - networkFetch normalizes DNS and reset errors + --- + duration_ms: 0.085167 + type: 'test' + ... +# Subtest: networkFetch times out requests +ok 183 - networkFetch times out requests + --- + duration_ms: 2.706917 + type: 'test' + ... +# Subtest: networkFetch honors init.signal abort reasons without options.signal +ok 184 - networkFetch honors init.signal abort reasons without options.signal + --- + duration_ms: 0.557042 + type: 'test' + ... +# Subtest: networkFetch preserves parent NetworkFetchError reasons passed through options.signal +ok 185 - networkFetch preserves parent NetworkFetchError reasons passed through options.signal + --- + duration_ms: 0.590125 + type: 'test' + ... +# (node:66170) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# Subtest: local gateway writes a complete shadow bundle linked to the final request +ok 186 - local gateway writes a complete shadow bundle linked to the final request + --- + duration_ms: 698.895166 + type: 'test' + ... +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# Subtest: enabling O1 does not change Agent-visible model input +not ok 187 - enabling O1 does not change Agent-visible model input + --- + duration_ms: 519.577083 + type: 'test' + location: '/Users/da/Documents/PilotDeck-worktrees/20260727-fix-convergence-v24r8-immutable-source-repair-v1/dist/tests/observability/local-gateway-observation.spec.js:74:1' + failureType: 'testCodeFailure' + error: "ENOTEMPTY: directory not empty, rmdir '/var/folders/yz/6smg7hrj57l05w6jlb3vgn240000gn/T/pilotdeck-observation-equivalence-sZBmWw/home'" + code: 'ENOTEMPTY' + ... +# (node:66172) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# [session-title] generation skipped (invalid_json): Unexpected token 'B', "Bounded co"... is not valid JSON +# Subtest: local Gateway records a bounded handoff through boundary to progress +ok 188 - local Gateway records a bounded handoff through boundary to progress + --- + duration_ms: 1237.994 + type: 'test' + ... +# (node:66173) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: local Gateway records a complete O1 trajectory when a bounded child times out +ok 189 - local Gateway records a complete O1 trajectory when a bounded child times out + --- + duration_ms: 1599.549833 + type: 'test' + ... +# Subtest: subagent tool detection and result pair at the model projection layer +ok 190 - subagent tool detection and result pair at the model projection layer + --- + duration_ms: 5.629333 + type: 'test' + ... +# Subtest: main tool detection and result produce one exact O1 pair +ok 191 - main tool detection and result produce one exact O1 pair + --- + duration_ms: 0.569917 + type: 'test' + ... +# Subtest: repair preparation grace is observable without becoming progress +ok 192 - repair preparation grace is observable without becoming progress + --- + duration_ms: 0.070208 + type: 'test' + ... +# Subtest: recorder finalizes a complete hash-only trajectory +ok 193 - recorder finalizes a complete hash-only trajectory + --- + duration_ms: 8.552583 + type: 'test' + ... +# Subtest: queue overflow is explicit and makes integrity partial +ok 194 - queue overflow is explicit and makes integrity partial + --- + duration_ms: 5.949708 + type: 'test' + ... +# Subtest: verifier rejects reused model request identity and duplicate terminals +ok 195 - verifier rejects reused model request identity and duplicate terminals + --- + duration_ms: 2.65925 + type: 'test' + ... +# Subtest: verifier rejects duplicate identity and secret-bearing keys +ok 196 - verifier rejects duplicate identity and secret-bearing keys + --- + duration_ms: 0.237667 + type: 'test' + ... +# [PilotDeck] transientRetry: rate_limit (attempt 1/1, delay=1ms) +# Subtest: router observation closes every fallback and retry attempt +ok 197 - router observation closes every fallback and retry attempt + --- + duration_ms: 33.208541 + type: 'test' + ... +# Subtest: router request identity separates sessions sharing a turn id +ok 198 - router request identity separates sessions sharing a turn id + --- + duration_ms: 5.739292 + type: 'test' + ... +# Subtest: router request identity advances across sequential calls in one turn +ok 199 - router request identity advances across sequential calls in one turn + --- + duration_ms: 6.041125 + type: 'test' + ... +# Subtest: observability is disabled by default and preserves the O1 diagnostic profile +ok 200 - observability is disabled by default and preserves the O1 diagnostic profile + --- + duration_ms: 24.957084 + type: 'test' + ... +# Subtest: O1 rejects unsupported profiles and unsafe labels +ok 201 - O1 rejects unsupported profiles and unsafe labels + --- + duration_ms: 7.209958 + type: 'test' + ... +# Subtest: web search can be explicitly disabled without discarding provider config +ok 202 - web search can be explicitly disabled without discarding provider config + --- + duration_ms: 2.089667 + type: 'test' + ... +# Subtest: web search enabled remains optional for backwards compatibility +ok 203 - web search enabled remains optional for backwards compatibility + --- + duration_ms: 0.103708 + type: 'test' + ... +# Subtest: web search enabled must be a boolean +ok 204 - web search enabled must be a boolean + --- + duration_ms: 0.095792 + type: 'test' + ... +# Subtest: progress lease is disabled by default and opt-in evaluation config is preserved +ok 205 - progress lease is disabled by default and opt-in evaluation config is preserved + --- + duration_ms: 26.454625 + type: 'test' + ... +# Subtest: progress lease preserves an explicit cold-start allowance +ok 206 - progress lease preserves an explicit cold-start allowance + --- + duration_ms: 2.45775 + type: 'test' + ... +# Subtest: progress lease rejects non-evaluation modes +ok 207 - progress lease rejects non-evaluation modes + --- + duration_ms: 3.421625 + type: 'test' + ... +# Subtest: progress lease rejects a cold-start allowance below the steady-state limit +ok 208 - progress lease rejects a cold-start allowance below the steady-state limit + --- + duration_ms: 2.885792 + type: 'test' + ... +# Subtest: legal coverage validator creates a current proof and removes it when the deliverable changes +ok 209 - legal coverage validator creates a current proof and removes it when the deliverable changes + --- + duration_ms: 305.578542 + type: 'test' + ... +# Subtest: legal coverage initializer creates a text skeleton before source review and remains idempotent +ok 210 - legal coverage initializer creates a text skeleton before source review and remains idempotent + --- + duration_ms: 361.378625 + type: 'test' + ... +# Subtest: legal coverage initializer binds trusted manifests to original inputs +ok 211 - legal coverage initializer binds trusted manifests to original inputs + --- + duration_ms: 459.68425 + type: 'test' + ... +# Subtest: legal coverage source bootstrap creates only deterministic pending manifest rows +ok 212 - legal coverage source bootstrap creates only deterministic pending manifest rows + --- + duration_ms: 434.87675 + type: 'test' + ... +# Subtest: legal coverage injects deterministic disjoint worker batches for large pending source rooms +ok 213 - legal coverage injects deterministic disjoint worker batches for large pending source rooms + --- + duration_ms: 3213.761 + type: 'test' + ... +# Subtest: legal coverage CLI exposes bundled guidance through stable named references +ok 214 - legal coverage CLI exposes bundled guidance through stable named references + --- + duration_ms: 135.042583 + type: 'test' + ... +# Subtest: legal coverage initializer creates only explicit text formats and preserves existing content +ok 215 - legal coverage initializer creates only explicit text formats and preserves existing content + --- + duration_ms: 53.257417 + type: 'test' + ... +# Subtest: legal coverage initializer rejects traversal and symlink ancestors without external writes +ok 216 - legal coverage initializer rejects traversal and symlink ancestors without external writes + --- + duration_ms: 149.695375 + type: 'test' + ... +# Subtest: legal coverage validator binds runner originals and derivations into its proof +ok 217 - legal coverage validator binds runner originals and derivations into its proof + --- + duration_ms: 218.626833 + type: 'test' + ... +# Subtest: legal coverage validator rejects missing lineage and mutable or derived input roots +ok 218 - legal coverage validator rejects missing lineage and mutable or derived input roots + --- + duration_ms: 269.208167 + type: 'test' + ... +# Subtest: legal coverage next-batch exposes one bounded deterministic repair slice +ok 219 - legal coverage next-batch exposes one bounded deterministic repair slice + --- + duration_ms: 210.371083 + type: 'test' + ... +# Subtest: legal coverage apply-batch atomically updates only the current bounded slice +ok 220 - legal coverage apply-batch atomically updates only the current bounded slice + --- + duration_ms: 315.909791 + type: 'test' + ... +# Subtest: legal coverage apply-batch rejects record and byte limit violations before writing +ok 221 - legal coverage apply-batch rejects record and byte limit violations before writing + --- + duration_ms: 198.942875 + type: 'test' + ... +# Subtest: legal coverage validator rejects orphaned conflicts and incomplete final disclosure +ok 222 - legal coverage validator rejects orphaned conflicts and incomplete final disclosure + --- + duration_ms: 101.10125 + type: 'test' + ... +# Subtest: legal coverage validator detects un-inventoried sources and cross-fact timeline collisions +ok 223 - legal coverage validator detects un-inventoried sources and cross-fact timeline collisions + --- + duration_ms: 106.00175 + type: 'test' + ... +# Subtest: legal coverage validator rejects reused generic quotes and unsupported fact coverage +ok 224 - legal coverage validator rejects reused generic quotes and unsupported fact coverage + --- + duration_ms: 101.730584 + type: 'test' + ... +# Subtest: legal coverage validator requires authority links for critical issues and legal-authority matrices +ok 225 - legal coverage validator requires authority links for critical issues and legal-authority matrices + --- + duration_ms: 101.4815 + type: 'test' + ... +# Subtest: legal coverage validator rejects non-object canonical JSON documents +ok 226 - legal coverage validator rejects non-object canonical JSON documents + --- + duration_ms: 704.840583 + type: 'test' + ... +# Subtest: legal coverage validator binds reviewed source bytes to the ledger and state hash +ok 227 - legal coverage validator binds reviewed source bytes to the ledger and state hash + --- + duration_ms: 203.071125 + type: 'test' + ... +# Subtest: legal coverage validator rejects ancestor symlinks and a symlinked proof +ok 228 - legal coverage validator rejects ancestor symlinks and a symlinked proof + --- + duration_ms: 152.282625 + type: 'test' + ... +# Subtest: legal coverage validator does not read or write through a symlinked state ancestor +ok 229 - legal coverage validator does not read or write through a symlinked state ancestor + --- + duration_ms: 97.075917 + type: 'test' + ... +# Subtest: legal coverage validator requires unresolved disclosure for unverified material facts +ok 230 - legal coverage validator requires unresolved disclosure for unverified material facts + --- + duration_ms: 100.48275 + type: 'test' + ... +# Subtest: legal coverage validator rejects unique but irrelevant issue and authority quotes +ok 231 - legal coverage validator rejects unique but irrelevant issue and authority quotes + --- + duration_ms: 101.279166 + type: 'test' + ... +# Subtest: legal coverage validator enforces reciprocal and same-entry ledger relationships +ok 232 - legal coverage validator enforces reciprocal and same-entry ledger relationships + --- + duration_ms: 104.213292 + type: 'test' + ... +# Subtest: legal coverage validator accepts null for an optional threshold assessment +ok 233 - legal coverage validator accepts null for an optional threshold assessment + --- + duration_ms: 102.903709 + type: 'test' + ... +# Subtest: legal coverage validator uses locators without quotes for binary deliverables +ok 234 - legal coverage validator uses locators without quotes for binary deliverables + --- + duration_ms: 101.862458 + type: 'test' + ... +# Subtest: legal coverage validator rejects empty-fact shortcuts and material facts outside matrices +ok 235 - legal coverage validator rejects empty-fact shortcuts and material facts outside matrices + --- + duration_ms: 203.584917 + type: 'test' + ... +# Subtest: legal coverage hook activates only legal work and injects one observable milestone +ok 236 - legal coverage hook activates only legal work and injects one observable milestone + --- + duration_ms: 542.206041 + type: 'test' + ... +# Subtest: legal coverage hook groups repeated validator errors into one bounded milestone +ok 237 - legal coverage hook groups repeated validator errors into one bounded milestone + --- + duration_ms: 538.122542 + type: 'test' + ... +# Subtest: legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress +ok 238 - legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress + --- + duration_ms: 998.45325 + type: 'test' + ... +# Subtest: legal coverage progress ordinal advances once for a new legal phase +ok 239 - legal coverage progress ordinal advances once for a new legal phase + --- + duration_ms: 210.352417 + type: 'test' + ... +# Subtest: legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals +ok 240 - legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals + --- + duration_ms: 476.987083 + type: 'test' + ... +# Subtest: legal coverage rejects oversized matrix evidence before making a selection receipt immutable +ok 241 - legal coverage rejects oversized matrix evidence before making a selection receipt immutable + --- + duration_ms: 158.995583 + type: 'test' + ... +# Subtest: legal coverage fails closed when one matrix index item exceeds the bounded page +ok 242 - legal coverage fails closed when one matrix index item exceeds the bounded page + --- + duration_ms: 158.315166 + type: 'test' + ... +# Subtest: legal coverage injects a bounded deterministic matrix relation-closure batch +ok 243 - legal coverage injects a bounded deterministic matrix relation-closure batch + --- + duration_ms: 213.858875 + type: 'test' + ... +# Subtest: legal coverage authority closure uses one bounded state-bound transaction +ok 244 - legal coverage authority closure uses one bounded state-bound transaction + --- + duration_ms: 589.660292 + type: 'test' + ... +# Subtest: legal coverage rejects changed and symlinked authority closure proposals without ledger mutation +ok 245 - legal coverage rejects changed and symlinked authority closure proposals without ledger mutation + --- + duration_ms: 250.884584 + type: 'test' + ... +# Subtest: legal coverage milestone digest ignores opaque incomplete-state hash churn +ok 246 - legal coverage milestone digest ignores opaque incomplete-state hash churn + --- + duration_ms: 2.454875 + type: 'test' + ... +# Subtest: legal coverage hook activates a malformed configured workspace so the agent can repair it +ok 247 - legal coverage hook activates a malformed configured workspace so the agent can repair it + --- + duration_ms: 101.5135 + type: 'test' + ... +# Subtest: legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace +ok 248 - legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace + --- + duration_ms: 51.50375 + type: 'test' + ... +# Subtest: legal product plugin loads one skill and contains no benchmark-specific controls +ok 249 - legal product plugin loads one skill and contains no benchmark-specific controls + --- + duration_ms: 3.156625 + type: 'test' + ... +# Subtest: file artifacts include every meaningful workspace change without an extension allowlist +ok 250 - file artifacts include every meaningful workspace change without an extension allowlist + --- + duration_ms: 33.382083 + type: 'test' + ... +# Subtest: file artifact fingerprints are reused for unchanged files across scans and turns +ok 251 - file artifact fingerprints are reused for unchanged files across scans and turns + --- + duration_ms: 4.2505 + type: 'test' + ... +# Subtest: TurnRunner emits and persists file artifacts before completing the turn +ok 252 - TurnRunner emits and persists file artifacts before completing the turn + --- + duration_ms: 21.172833 + type: 'test' + ... +# Subtest: TurnRunner does not collect generated files when artifacts are disabled +ok 253 - TurnRunner does not collect generated files when artifacts are disabled + --- + duration_ms: 2.306833 + type: 'test' + ... +# Subtest: bash success result is formatted with assertions, stdout, and stderr +ok 254 - bash success result is formatted with assertions, stdout, and stderr + --- + duration_ms: 5.777125 + type: 'test' + ... +# Subtest: bash allows hyphenated commands whose names merely start with next +ok 255 - bash allows hyphenated commands whose names merely start with next + --- + duration_ms: 1.9155 + type: 'test' + ... +# Subtest: bash still rejects exact framework CLI tokens that start long-lived processes +ok 256 - bash still rejects exact framework CLI tokens that start long-lived processes + --- + duration_ms: 1.031875 + type: 'test' + ... +# Subtest: bash failure tool result includes raw stdout and stderr tail for UI and model +ok 257 - bash failure tool result includes raw stdout and stderr tail for UI and model + --- + duration_ms: 13.045583 + type: 'test' + ... +# Subtest: bash failure diagnostic extracts Python traceback location and exception +ok 258 - bash failure diagnostic extracts Python traceback location and exception + --- + duration_ms: 1.947917 + type: 'test' + ... +# Subtest: bash success keeps full output for ToolResultBudget persistence +ok 259 - bash success keeps full output for ToolResultBudget persistence + --- + duration_ms: 6.486542 + type: 'test' + ... +# Subtest: agent tool accepts explorer as an alias for explore +ok 260 - agent tool accepts explorer as an alias for explore + --- + duration_ms: 1.2815 + type: 'test' + ... +# Subtest: agent tool defaults general-purpose to explore in ask mode +ok 261 - agent tool defaults general-purpose to explore in ask mode + --- + duration_ms: 0.083542 + type: 'test' + ... +# Subtest: agent tool injects and forwards the parent-bounded timeout +ok 262 - agent tool injects and forwards the parent-bounded timeout + --- + duration_ms: 0.157333 + type: 'test' + ... +# Subtest: agent tool rejects a launch that would consume the parent handoff window +ok 263 - agent tool rejects a launch that would consume the parent handoff window + --- + duration_ms: 0.231791 + type: 'test' + ... +# Subtest: agent fallback path enforces the same timeout +ok 264 - agent fallback path enforces the same timeout + --- + duration_ms: 6.089583 + type: 'test' + ... +# Subtest: agent tool preserves unknown custom fallback subagent names +ok 265 - agent tool preserves unknown custom fallback subagent names + --- + duration_ms: 0.582292 + type: 'test' + ... +# Subtest: execute_code read-only probe handles missing input +ok 266 - execute_code read-only probe handles missing input + --- + duration_ms: 0.536125 + type: 'test' + ... +# Subtest: disabling web search removes it from the registry but keeps web fetch +ok 267 - disabling web search removes it from the registry but keeps web fetch + --- + duration_ms: 1.119958 + type: 'test' + ... +# Subtest: execute_code rejects nested web search calls when web search is disabled +ok 268 - execute_code rejects nested web search calls when web search is disabled + --- + duration_ms: 0.124125 + type: 'test' + ... +# Subtest: read_skill returns the resolved SKILL.md path with the skill body +ok 269 - read_skill returns the resolved SKILL.md path with the skill body + --- + duration_ms: 0.688625 + type: 'test' + ... +# Subtest: read_skill preserves legacy content-only loading when metadata is unavailable +ok 270 - read_skill preserves legacy content-only loading when metadata is unavailable + --- + duration_ms: 0.069167 + type: 'test' + ... +# Subtest: web_search retries transient provider failures +ok 271 - web_search retries transient provider failures + --- + duration_ms: 608.11 + type: 'test' + ... +# Subtest: web_search turns request timeout into tool_timeout +ok 272 - web_search turns request timeout into tool_timeout + --- + duration_ms: 3.0795 + type: 'test' + ... +# Subtest: web_search turns network timeout errors into tool_timeout +ok 273 - web_search turns network timeout errors into tool_timeout + --- + duration_ms: 1.01075 + type: 'test' + ... +# Subtest: write_file freshness error tells the model to read the file first +ok 274 - write_file freshness error tells the model to read the file first + --- + duration_ms: 1.147458 + type: 'test' + ... +# Subtest: write_file missing content points at the missing field and chunked recovery +ok 275 - write_file missing content points at the missing field and chunked recovery + --- + duration_ms: 0.25675 + type: 'test' + ... +# Subtest: invalid tool input keeps a bounded original error block +ok 276 - invalid tool input keeps a bounded original error block + --- + duration_ms: 0.208375 + type: 'test' + ... +# Subtest: edit_file old_string miss tells the model to reread and copy exact text +ok 277 - edit_file old_string miss tells the model to reread and copy exact text + --- + duration_ms: 0.06425 + type: 'test' + ... +# Subtest: read_file invalid range preserves the concrete issue +ok 278 - read_file invalid range preserves the concrete issue + --- + duration_ms: 0.10825 + type: 'test' + ... +# Subtest: bash missing command reports the required command parameter +ok 279 - bash missing command reports the required command parameter + --- + duration_ms: 0.103584 + type: 'test' + ... +# Subtest: bash timeout over max keeps foreground and task_wait guidance +ok 280 - bash timeout over max keeps foreground and task_wait guidance + --- + duration_ms: 0.131833 + type: 'test' + ... +# Subtest: bash background rejection keeps background-specific guidance +ok 281 - bash background rejection keeps background-specific guidance + --- + duration_ms: 0.077209 + type: 'test' + ... +# Subtest: write_file can create a new file without a prior read_file call +ok 282 - write_file can create a new file without a prior read_file call + --- + duration_ms: 7.9875 + type: 'test' + ... +# Subtest: edit_file can create a new file with empty old_string without a prior read_file call +ok 283 - edit_file can create a new file with empty old_string without a prior read_file call + --- + duration_ms: 1.879875 + type: 'test' + ... +# Subtest: read_file auto-pages large text files instead of failing +ok 284 - read_file auto-pages large text files instead of failing + --- + duration_ms: 2208.131708 + type: 'test' + ... +# Subtest: read_file rejects Office container files during validation +ok 285 - read_file rejects Office container files during validation + --- + duration_ms: 0.833167 + type: 'test' + ... +# Subtest: read_file explicit limit reads a large file range without auto paging +ok 286 - read_file explicit limit reads a large file range without auto paging + --- + duration_ms: 5.253625 + type: 'test' + ... +# Subtest: read_file auto-shrinks oversized persisted tool-result ref ranges +ok 287 - read_file auto-shrinks oversized persisted tool-result ref ranges + --- + duration_ms: 22487.385 + type: 'test' + ... +# Subtest: read_file keeps explicit oversized ordinary file ranges strict +ok 288 - read_file keeps explicit oversized ordinary file ranges strict + --- + duration_ms: 2.213125 + type: 'test' + ... +# Subtest: read_file explicit limit records a ranged snapshot for follow-up edits +ok 289 - read_file explicit limit records a ranged snapshot for follow-up edits + --- + duration_ms: 1.768958 + type: 'test' + ... +# Subtest: read_file auto-paged large files record a ranged snapshot for follow-up edits +ok 290 - read_file auto-paged large files record a ranged snapshot for follow-up edits + --- + duration_ms: 1569.467417 + type: 'test' + ... +# Subtest: read_file returns a head-tail preview for a single oversized line +ok 291 - read_file returns a head-tail preview for a single oversized line + --- + duration_ms: 2.585792 + type: 'test' + ... +# Subtest: read_file classifies common Office formats as binary +ok 292 - read_file classifies common Office formats as binary + --- + duration_ms: 0.19125 + type: 'test' + ... +# Subtest: read_file rejects ranged binary input instead of hanging +ok 293 - read_file rejects ranged binary input instead of hanging + --- + duration_ms: 0.979834 + type: 'test' + ... +# Subtest: read_file range honors an aborted signal +ok 294 - read_file range honors an aborted signal + --- + duration_ms: 0.575834 + type: 'test' + ... +# Subtest: glob result text tells the model when more files are available +ok 295 - glob result text tells the model when more files are available + --- + duration_ms: 37.216417 + type: 'test' + ... +# Subtest: grep result text includes next offset when paginated +ok 296 - grep result text includes next offset when paginated + --- + duration_ms: 21.328833 + type: 'test' + ... +# Subtest: todo_write returns the actual todo list in model-visible content +ok 297 - todo_write returns the actual todo list in model-visible content + --- + duration_ms: 0.665 + type: 'test' + ... +# Subtest: task_list returns model-visible status and next action hints +ok 298 - task_list returns model-visible status and next action hints + --- + duration_ms: 1.059208 + type: 'test' + ... +# Subtest: applyResultSizeLimit keeps both head and tail when truncating text output +ok 299 - applyResultSizeLimit keeps both head and tail when truncating text output + --- + duration_ms: 0.684708 + type: 'test' + ... +# Subtest: large tool results are persisted under workspace .pilotdeck and readable by read_file +ok 300 - large tool results are persisted under workspace .pilotdeck and readable by read_file + --- + duration_ms: 12.274375 + type: 'test' + ... +# Subtest: large tool result read_file aliases are short and sequential +ok 301 - large tool result read_file aliases are short and sequential + --- + duration_ms: 4.371833 + type: 'test' + ... +# Subtest: history replay restores structured agent file artifacts +ok 302 - history replay restores structured agent file artifacts + --- + duration_ms: 18.233291 + type: 'test' + ... +# Subtest: history replay hides Agent file artifacts in general conversations +ok 303 - history replay hides Agent file artifacts in general conversations + --- + duration_ms: 4.371292 + type: 'test' + ... +# Subtest: history replay preserves agent status i18n metadata and user hint +ok 304 - history replay preserves agent status i18n metadata and user hint + --- + duration_ms: 11.009375 + type: 'test' + ... +# Subtest: history token usage restores latest non-empty turn past latest empty turn result +ok 305 - history token usage restores latest non-empty turn past latest empty turn result + --- + duration_ms: 4.3425 + type: 'test' + ... +# Subtest: history token usage prefers persisted context budget snapshot +ok 306 - history token usage prefers persisted context budget snapshot + --- + duration_ms: 4.146208 + type: 'test' + ... +# Subtest: web reducer merges persisted tool result detail path into existing tool result +ok 307 - web reducer merges persisted tool result detail path into existing tool result + --- + duration_ms: 1.319375 + type: 'test' + ... +# Subtest: web reducer bounds huge live tool result previews +ok 308 - web reducer bounds huge live tool result previews + --- + duration_ms: 0.181916 + type: 'test' + ... +1..308 +# tests 308 +# suites 0 +# pass 307 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 28785.315875 + ELIFECYCLE  Test failed. See above for more details. diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/o1-isolated-rerun.log b/.omo/evidence/20260727-v24r8-immutable-source-repair/o1-isolated-rerun.log new file mode 100644 index 00000000..ff071793 --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/o1-isolated-rerun.log @@ -0,0 +1,20 @@ +TAP version 13 +# (node:68346) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# [session-title] generation skipped (invalid_json): Unexpected token 'Q', "QA" is not valid JSON +# Subtest: enabling O1 does not change Agent-visible model input +ok 1 - enabling O1 does not change Agent-visible model input + --- + duration_ms: 499.953417 + type: 'test' + ... +1..1 +# tests 1 +# suites 0 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 915.445458 diff --git a/.omo/evidence/20260727-v24r8-immutable-source-repair/real-gateway.log b/.omo/evidence/20260727-v24r8-immutable-source-repair/real-gateway.log new file mode 100644 index 00000000..b1f35a59 --- /dev/null +++ b/.omo/evidence/20260727-v24r8-immutable-source-repair/real-gateway.log @@ -0,0 +1,36 @@ +TAP version 13 +# (node:68772) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: real gateway drives legal plugin milestones through bounded artifact correction +ok 1 - real gateway drives legal plugin milestones through bounded artifact correction + --- + duration_ms: 717.367709 + type: 'test' + ... +# Subtest: real gateway blocks completion when the legal Stop hook cannot read session state +ok 2 - real gateway blocks completion when the legal Stop hook cannot read session state + --- + duration_ms: 428.993292 + type: 'test' + ... +# Subtest: real gateway executes the state-bound legal authority closure with complete O1 evidence +ok 3 - real gateway executes the state-bound legal authority closure with complete O1 evidence + --- + duration_ms: 491.581917 + type: 'test' + ... +# Subtest: real gateway applies an immutable source repair before renewing legal progress +ok 4 - real gateway applies an immutable source repair before renewing legal progress + --- + duration_ms: 648.222583 + type: 'test' + ... +1..4 +# tests 4 +# suites 0 +# pass 4 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 2670.662875