From fd296ccbfea0b8cf0ea1e03d506372e30714e140 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 13:36:42 +0800 Subject: [PATCH 1/6] feat(legal): add state-bound issue closure --- .../legal/plugins/legal-coverage/hook.mjs | 17 + .../legal-coverage/scripts/legal-coverage.mjs | 20 +- .../scripts/lib/legal-coverage.mjs | 461 ++++++++++++++++++ .../legal-coverage-plugin-runtime.spec.ts | 310 ++++++++++++ .../case-09-issue-closure-replay.json | 19 + tests/products/legal-coverage.spec.ts | 429 ++++++++++++++++ 6 files changed, 1255 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/convergence/case-09-issue-closure-replay.json diff --git a/products/legal/plugins/legal-coverage/hook.mjs b/products/legal/plugins/legal-coverage/hook.mjs index bc47d3ca..3c3c6a2a 100644 --- a/products/legal/plugins/legal-coverage/hook.mjs +++ b/products/legal/plugins/legal-coverage/hook.mjs @@ -9,6 +9,7 @@ import { authorityClosurePlan, convergenceStateHash, ensureWorkspace, + issueClosurePlan, milestoneDigest, milestoneEnvelopeFor, milestoneFor, @@ -227,6 +228,9 @@ async function dynamicWorkItems(workspaceRoot, result) { if (first?.phase === "matrices" && first.code === "matrix_pending") { return pendingMatrixPlan(workspaceRoot, { validationResult: result }); } + if (first?.phase === "issues" && first.code === "risk_signal_orphaned") { + return issueClosurePlan(workspaceRoot, { validationResult: result }); + } if (first?.phase === "authorities" && first.code === "legal_authority_links_missing") { return authorityClosurePlan(workspaceRoot, { validationResult: result }); } @@ -419,6 +423,19 @@ function legalHandoffCheckpointDigest(workItems) { targetEntryId: workItems.proposal.targetEntryId, proposalSha256: workItems.proposal.proposalSha256, }; + } else if (workItems?.group === "issue-closure-apply" + && workItems.proposal?.validated === true + && validStateHash(workItems.proposal.expectedStateHash) + && validStateHash(workItems.proposal.proposalSha256) + && nonEmptyString(workItems.proposal.targetMatrixId) + && nonEmptyString(workItems.proposal.targetEntryId)) { + checkpoint = { + kind: "issue-closure-apply-ready", + expectedStateHash: workItems.proposal.expectedStateHash, + targetMatrixId: workItems.proposal.targetMatrixId, + targetEntryId: workItems.proposal.targetEntryId, + proposalSha256: workItems.proposal.proposalSha256, + }; } else if (workItems?.group === "source-fragment-apply" && workItems.proposal?.validated === true && validStateHash(workItems.proposal.expectedStateHash) diff --git a/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs b/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs index 4f9ec5dc..aefab90d 100644 --- a/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs +++ b/products/legal/plugins/legal-coverage/scripts/legal-coverage.mjs @@ -4,6 +4,7 @@ import { resolve } from "node:path"; import { applyAuthorityClosure, applyCoverageBatch, + applyIssueClosure, applyMatrixProposal, applyMatrixSelection, applySourceMergeProposal, @@ -231,6 +232,23 @@ if (command === "init") { })); process.exitCode = 1; } +} else if (command === "issue-closure-apply") { + try { + const result = await applyIssueClosure(workspaceRoot, { + proposalPath: readOption(args, "--input-file"), + proposalSha256: readOption(args, "--proposal-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 : "issue_closure_apply_failed", + message: error instanceof Error ? error.message : String(error), + }, + })); + process.exitCode = 1; + } } else if (command === "authority-closure-apply") { try { const result = await applyAuthorityClosure(workspaceRoot, { @@ -294,7 +312,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] [--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]"); + 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 7be0328c..5148563f 100644 --- a/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs +++ b/products/legal/plugins/legal-coverage/scripts/lib/legal-coverage.mjs @@ -90,6 +90,11 @@ const MATRIX_INDEX_MAX_BYTES = 8192; const MATRIX_SELECTED_FACT_MAX_RECORDS = 12; const MATRIX_SELECTED_FACT_MAX_BYTES = 8192; const MATRIX_MUTATION_MAX_BYTES = 24576; +const ISSUE_TRANSACTION_DIRECTORY = `${STATE_DIRECTORY}/issue-transactions`; +const ISSUE_CLOSURE_PATTERN = /^issue-closure-[a-f0-9]{12}\.json$/u; +const ISSUE_CLOSURE_MAX_RECORDS = Object.keys(ISSUE_RULES).length; +const ISSUE_CLOSURE_MAX_FACTS = 12; +const ISSUE_CLOSURE_MAX_BYTES = 24576; const AUTHORITY_TRANSACTION_DIRECTORY = `${STATE_DIRECTORY}/authority-transactions`; const AUTHORITY_CLOSURE_PATTERN = /^authority-closure-[a-f0-9]{12}\.json$/u; const AUTHORITY_CLOSURE_MAX_RECORDS = 8; @@ -2401,6 +2406,375 @@ export async function applyMatrixProposal(workspaceRoot, options = {}) { }; } +export async function issueClosurePlan(workspaceRoot, options = {}) { + const loaded = await readWorkspaceState(workspaceRoot); + const validation = options.validationResult + ?? await validateWorkspace({ workspaceRoot: loaded.workspace, writeProof: false }); + const first = validation.errors[0]; + if (first?.phase !== "issues" || first.code !== "risk_signal_orphaned") return undefined; + + const matrices = Array.isArray(loaded.state.matrices?.matrices) ? loaded.state.matrices.matrices : []; + let matrixIndex = -1; + let entryIndex = -1; + let matrix; + let entry; + for (const [candidateMatrixIndex, candidateMatrix] of matrices.entries()) { + const entries = Array.isArray(candidateMatrix?.entries) ? candidateMatrix.entries : []; + const candidateEntryIndex = entries.findIndex((candidate) => isRecord(candidate) + && nonEmpty(candidate.id) + && stringArray(candidate.riskSignals).length > 0 + && stringArray(candidate.issueIds).length === 0); + if (candidateEntryIndex < 0) continue; + matrixIndex = candidateMatrixIndex; + entryIndex = candidateEntryIndex; + matrix = candidateMatrix; + entry = entries[candidateEntryIndex]; + break; + } + if (matrixIndex < 0 || entryIndex < 0 || !isRecord(matrix) || !nonEmpty(matrix.id) + || !isRecord(entry) || !nonEmpty(entry.id)) return undefined; + + const factIds = stringArray(entry.factIds); + const riskSignals = [...new Set(stringArray(entry.riskSignals))]; + const factsById = recordMap(loaded.state.facts?.facts); + const factItems = factIds.map((factId) => factsById.get(factId)).filter(Boolean).map(matrixRelationFactItem); + const allowedIssueRules = riskSignals.map((riskSignal) => { + const ruleId = Object.keys(ISSUE_RULES).find((candidate) => ISSUE_RULES[candidate] === riskSignal); + return ruleId ? { ruleId, riskSignal, whenToUse: ISSUE_RULE_GUIDANCE[ruleId] } : undefined; + }).filter(Boolean); + const preparedSlice = { + stateHash: validation.stateHash, + target: { + matrixId: matrix.id, + matrixIndex, + entryId: entry.id, + entryIndex, + summary: entry.summary, + factIds, + riskSignals, + issueIds: stringArray(entry.issueIds), + authorityIds: stringArray(entry.authorityIds), + }, + allowedIssueRules, + facts: factItems, + }; + const preparedBytes = Buffer.byteLength(JSON.stringify(preparedSlice)); + const preparedSliceSha256 = sha256(JSON.stringify(preparedSlice)); + const targetCritical = factItems.some((fact) => fact.critical === true); + const digest = sha256([ + validation.stateHash, + matrix.id, + entry.id, + String(entryIndex), + preparedSliceSha256, + ].join("\0")).slice(0, 12); + const proposalPath = `${ISSUE_TRANSACTION_DIRECTORY}/issue-closure-${digest}.json`; + const issueTemplate = allowedIssueRules.map(({ ruleId }) => ({ + id: ``, + ruleId, + status: "open", + severity: "", + critical: targetCritical, + factIds: [...factIds], + authorityIds: [], + analysis: "", + conclusion: "", + recommendations: [""], + })); + const proposal = { + path: proposalPath, + expectedStateHash: validation.stateHash, + targetMatrixId: matrix.id, + targetEntryId: entry.id, + preparedSliceSha256, + template: { + schemaVersion: 1, + phase: "issues", + group: "issue-closure-proposal", + expectedStateHash: validation.stateHash, + targetMatrixId: matrix.id, + targetEntryId: entry.id, + preparedSliceSha256, + issueUpserts: issueTemplate, + matrixEntryLinks: { issueIds: issueTemplate.map((issue) => issue.id) }, + }, + }; + const base = { + phase: "issues", + group: "issue-closure-propose", + mode: "main-agent-propose", + stateHash: validation.stateHash, + target: { matrixId: matrix.id, matrixIndex, entryId: entry.id, entryIndex }, + returned: 1, + hasMore: matrices.some((candidateMatrix, candidateMatrixIndex) => + (Array.isArray(candidateMatrix?.entries) ? candidateMatrix.entries : []).some((candidateEntry, candidateEntryIndex) => + (candidateMatrixIndex > matrixIndex + || (candidateMatrixIndex === matrixIndex && candidateEntryIndex > entryIndex)) + && isRecord(candidateEntry) + && stringArray(candidateEntry.riskSignals).length > 0 + && stringArray(candidateEntry.issueIds).length === 0 + ) + ), + limits: { + maxRecords: ISSUE_CLOSURE_MAX_RECORDS, + maxFacts: ISSUE_CLOSURE_MAX_FACTS, + maxSerializedBytes: ISSUE_CLOSURE_MAX_BYTES, + }, + preparedSlice: preparedBytes <= ISSUE_CLOSURE_MAX_BYTES ? preparedSlice : { + stateHash: validation.stateHash, + target: preparedSlice.target, + oversized: true, + serializedBytes: preparedBytes, + }, + proposal, + }; + if (factIds.length === 0 || factItems.length !== factIds.length + || factIds.length > ISSUE_CLOSURE_MAX_FACTS + || riskSignals.length === 0 || allowedIssueRules.length !== riskSignals.length + || riskSignals.length > ISSUE_CLOSURE_MAX_RECORDS + || preparedBytes > ISSUE_CLOSURE_MAX_BYTES) { + return { + ...base, + proposal: { + ...proposal, + validationError: { + code: "issue_closure_slice_invalid", + message: `Issue closure requires 1-${ISSUE_CLOSURE_MAX_FACTS} known target facts, 1-${ISSUE_CLOSURE_MAX_RECORDS} known risk signals, and at most ${ISSUE_CLOSURE_MAX_BYTES} serialized bytes.`, + }, + repairRequired: true, + }, + }; + } + const receipt = await validIssueClosureProposal(workspaceRoot, proposal, base, loaded.state); + if (receipt?.valid) { + const { preparedSlice: _preparedSlice, ...applyBase } = base; + return { + ...applyBase, + group: "issue-closure-apply", + mode: "main-agent-apply", + proposal: { + path: proposal.path, + expectedStateHash: proposal.expectedStateHash, + targetMatrixId: proposal.targetMatrixId, + targetEntryId: proposal.targetEntryId, + preparedSliceSha256: proposal.preparedSliceSha256, + validated: true, + proposalSha256: receipt.sha256, + transactionBytes: receipt.transactionBytes, + ...(options.includeValidatedTransaction === true ? { transaction: receipt.transaction } : {}), + }, + }; + } + if (receipt?.error) { + return { + ...base, + proposal: { ...proposal, validationError: receipt.error, repairRequired: true }, + }; + } + return base; +} + +export async function applyIssueClosure(workspaceRoot, options = {}) { + if (!nonEmpty(options.proposalPath) + || !ISSUE_CLOSURE_PATTERN.test(options.proposalPath.split("/").at(-1) ?? "")) { + throw batchError("issue_closure_path_invalid", "issue-closure-apply requires the injected deterministic proposal path."); + } + if (!/^[a-f0-9]{64}$/u.test(String(options.proposalSha256 ?? ""))) { + throw batchError("issue_closure_hash_invalid", "issue-closure-apply requires the injected lowercase proposal SHA-256."); + } + const proposalPath = await resolveSafeWorkspacePath(workspaceRoot, options.proposalPath); + const proposalBytes = await readFile(proposalPath); + if (sha256(proposalBytes) !== options.proposalSha256) { + throw batchError("issue_closure_changed", "The issue closure proposal changed after validation; request a fresh apply step."); + } + if (proposalBytes.byteLength > ISSUE_CLOSURE_MAX_BYTES) { + throw batchError("issue_closure_byte_limit", `Issue closure proposal is ${proposalBytes.byteLength} bytes; maximum is ${ISSUE_CLOSURE_MAX_BYTES}.`); + } + + const before = await validateWorkspace({ workspaceRoot, writeProof: false }); + const plan = await issueClosurePlan(workspaceRoot, { + validationResult: before, + includeValidatedTransaction: true, + }); + if (plan?.group !== "issue-closure-apply" || plan.proposal?.path !== options.proposalPath + || plan.proposal?.proposalSha256 !== options.proposalSha256) { + throw batchError("issue_closure_out_of_scope", "The proposal is not the current validated issue closure transaction."); + } + const loaded = await readWorkspaceState(workspaceRoot); + const transaction = plan.proposal.transaction; + const matrices = Array.isArray(loaded.state.matrices?.matrices) ? loaded.state.matrices.matrices : []; + const matrix = matrices[plan.target.matrixIndex]; + const entries = Array.isArray(matrix?.entries) ? matrix.entries : []; + const entry = entries[plan.target.entryIndex]; + if (!isRecord(matrix) || matrix.id !== plan.target.matrixId || !isRecord(entry) + || entry.id !== plan.target.entryId || stringArray(entry.issueIds).length > 0 + || stableStringify(stringArray(entry.riskSignals)) + !== stableStringify(plan.proposal.transaction.targetRiskSignals)) { + throw batchError("issue_closure_target_changed", "The target matrix entry is no longer the current orphaned risk-signal entry."); + } + const nextIssues = upsertLedgerRows(loaded.state.issues, "issues", transaction.issueUpserts); + const nextMatrices = { + ...loaded.state.matrices, + matrices: matrices.map((candidate, candidateMatrixIndex) => candidateMatrixIndex === plan.target.matrixIndex + ? { + ...candidate, + entries: entries.map((candidateEntry, candidateEntryIndex) => candidateEntryIndex === plan.target.entryIndex + ? { ...candidateEntry, issueIds: transaction.matrixEntryLinks.issueIds } + : candidateEntry), + } + : candidate), + }; + try { + await writeJsonAtomic(loaded.paths.issues, nextIssues); + await writeJsonAtomic(loaded.paths.matrices, nextMatrices); + } catch (error) { + await writeJsonAtomic(loaded.paths.issues, loaded.state.issues); + await writeJsonAtomic(loaded.paths.matrices, loaded.state.matrices); + throw error; + } + const after = await validateWorkspace({ workspaceRoot, writeProof: true }); + return { + applied: true, + phase: "issues", + group: "issue-closure-proposal", + matrixId: plan.target.matrixId, + entryId: plan.target.entryId, + issuesUpserted: transaction.issueUpserts.length, + previousStateHash: before.stateHash, + stateHash: after.stateHash, + passed: after.passed, + errorCountBefore: before.errors.length, + errorCountAfter: after.errors.length, + }; +} + +async function validIssueClosureProposal(workspaceRoot, expected, plan, state) { + try { + const path = await resolveSafeWorkspacePath(workspaceRoot, expected.path); + const bytes = await readFile(path); + if (bytes.byteLength > ISSUE_CLOSURE_MAX_BYTES) { + throw batchError("issue_closure_byte_limit", `Issue closure proposal is ${bytes.byteLength} bytes; maximum is ${ISSUE_CLOSURE_MAX_BYTES}.`); + } + let patch; + try { + patch = JSON.parse(bytes.toString("utf8")); + } catch (error) { + throw batchError("issue_closure_json_invalid", errorMessage(error)); + } + const transaction = validateIssueClosureProposal(patch, expected, plan, state); + return { + valid: true, + sha256: sha256(bytes), + transactionBytes: Buffer.byteLength(JSON.stringify(transaction)), + transaction, + }; + } catch (error) { + if (error?.code === "ENOENT") return undefined; + return { + valid: false, + error: { + code: typeof error?.code === "string" ? error.code : "issue_closure_invalid", + message: errorMessage(error), + }, + }; + } +} + +function validateIssueClosureProposal(patch, expected, plan, state) { + if (!isRecord(patch) || !hasOnlyKeys(patch, [ + "schemaVersion", "phase", "group", "expectedStateHash", "targetMatrixId", + "targetEntryId", "preparedSliceSha256", "issueUpserts", "matrixEntryLinks", + ])) throw batchError("issue_closure_keys_invalid", "Issue closure proposal contains missing or unsupported envelope fields."); + if (patch.schemaVersion !== 1 || patch.phase !== "issues" || patch.group !== "issue-closure-proposal") { + throw batchError("issue_closure_envelope_invalid", "Issue closure proposal requires schemaVersion 1 and the injected phase/group."); + } + if (patch.expectedStateHash !== expected.expectedStateHash + || patch.targetMatrixId !== expected.targetMatrixId + || patch.targetEntryId !== expected.targetEntryId + || patch.preparedSliceSha256 !== expected.preparedSliceSha256) { + throw batchError("issue_closure_binding_invalid", "Issue closure proposal does not match the injected state, matrix entry, or prepared fact slice."); + } + if (!Array.isArray(patch.issueUpserts) || patch.issueUpserts.length < 1 + || patch.issueUpserts.length > ISSUE_CLOSURE_MAX_RECORDS + || !isRecord(patch.matrixEntryLinks) + || !hasOnlyKeys(patch.matrixEntryLinks, ["issueIds"])) { + throw batchError("issue_closure_records_invalid", `Issue closure requires 1-${ISSUE_CLOSURE_MAX_RECORDS} issue upserts plus matrixEntryLinks.`); + } + if (containsProposalPlaceholder(patch)) { + throw batchError("issue_closure_placeholder", "Replace every issue closure proposal placeholder before validation."); + } + + const targetFactIds = plan.preparedSlice.target.factIds; + const targetRiskSignals = plan.preparedSlice.target.riskSignals; + const expectedRuleIds = targetRiskSignals.map((signal) => + Object.keys(ISSUE_RULES).find((candidate) => ISSUE_RULES[candidate] === signal) + ); + if (expectedRuleIds.some((ruleId) => !ruleId)) { + throw batchError("issue_closure_signal_invalid", "Every target risk signal must map to one known issue rule."); + } + if (patch.issueUpserts.length !== expectedRuleIds.length) { + throw batchError("issue_closure_rule_count_invalid", "Issue closure requires exactly one issue for each target risk signal."); + } + const existingIssues = recordMap(state.issues?.issues); + const issueUpserts = patch.issueUpserts.map((issue) => normalizeIssueClosureIssue( + issue, + targetFactIds, + new Set(expectedRuleIds), + plan.preparedSlice.facts.some((fact) => fact.critical === true), + )); + const issueUpsertsById = recordMap(issueUpserts); + if (issueUpsertsById.size !== issueUpserts.length + || issueUpserts.some((issue) => existingIssues.has(issue.id))) { + throw batchError("issue_closure_duplicate_id", "Issue closure IDs must be new and unique."); + } + if (new Set(issueUpserts.map((issue) => issue.ruleId)).size !== expectedRuleIds.length + || expectedRuleIds.some((ruleId) => !issueUpserts.some((issue) => issue.ruleId === ruleId))) { + throw batchError("issue_closure_rule_coverage_invalid", "Issue closure must cover every target risk signal exactly once."); + } + const issueIds = uniqueNonEmptyIds(patch.matrixEntryLinks.issueIds, "issue_closure_links_invalid"); + if (issueIds.length !== issueUpserts.length + || issueIds.some((issueId) => !issueUpsertsById.has(issueId)) + || issueUpserts.some((issue) => !issueIds.includes(issue.id))) { + throw batchError("issue_closure_links_invalid", "Matrix issue links must equal the proposed issue IDs exactly."); + } + return { + issueUpserts, + matrixEntryLinks: { issueIds }, + targetRiskSignals, + }; +} + +function normalizeIssueClosureIssue(issue, targetFactIds, expectedRuleIds, targetCritical) { + const allowed = [ + "id", "ruleId", "status", "severity", "critical", "factIds", "authorityIds", + "analysis", "conclusion", "recommendations", + ]; + if (!isRecord(issue) || !hasOnlyKeys(issue, allowed) || !nonEmpty(issue.id) + || !expectedRuleIds.has(issue.ruleId) || !ISSUE_STATUSES.has(issue.status) + || !nonEmpty(issue.severity) || typeof issue.critical !== "boolean" + || !nonEmpty(issue.analysis) || !nonEmpty(issue.conclusion) + || stringArray(issue.recommendations).length === 0) { + throw batchError("issue_closure_issue_invalid", "Every issue upsert requires the canonical issue fields and legal reasoning."); + } + if (issue.critical !== targetCritical) { + throw batchError( + "issue_closure_criticality_invalid", + `Issue ${issue.id} critical must equal the criticality derived from the complete target fact slice.`, + ); + } + const factIds = uniqueNonEmptyIds(issue.factIds, "issue_closure_issue_facts_invalid"); + const authorityIds = uniqueNonEmptyIds(issue.authorityIds, "issue_closure_issue_authorities_invalid"); + if (stableStringify(factIds) !== stableStringify(targetFactIds) || authorityIds.length > 0) { + throw batchError("issue_closure_issue_scope_invalid", `Issue ${issue.id} must use every target-entry fact in injected order and cannot create authority links.`); + } + return { + ...issue, + factIds, + authorityIds, + recommendations: stringArray(issue.recommendations), + }; +} + export async function authorityClosurePlan(workspaceRoot, options = {}) { const loaded = await readWorkspaceState(workspaceRoot); const validation = options.validationResult @@ -3527,6 +3901,26 @@ export function convergenceStateHash(result, workItems) { } function convergenceWorkProjection(workItems) { + if (workItems?.group === "issue-closure-propose" && workItems.proposal?.validationError) { + const proposal = { ...workItems.proposal }; + delete proposal.validationError; + proposal.repairRequired = true; + return { ...workItems, proposal }; + } + if (workItems?.group === "issue-closure-apply" && workItems.proposal?.validated === true) { + return { + ...workItems, + proposal: { + path: workItems.proposal.path, + expectedStateHash: workItems.proposal.expectedStateHash, + targetMatrixId: workItems.proposal.targetMatrixId, + targetEntryId: workItems.proposal.targetEntryId, + preparedSliceSha256: workItems.proposal.preparedSliceSha256, + proposalSha256: workItems.proposal.proposalSha256, + validated: true, + }, + }; + } if (workItems?.group === "authority-closure-propose" && workItems.proposal?.validationError) { const proposal = { ...workItems.proposal }; delete proposal.validationError; @@ -3605,6 +3999,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { const sourceRepairApply = sourceRepairApplyCommandFor(workItems, cliPath); const matrixSelectionApply = matrixSelectionApplyCommandFor(workItems, cliPath); const matrixProposalApply = matrixProposalApplyCommandFor(workItems, cliPath); + const issueClosureApply = issueClosureApplyCommandFor(workItems, cliPath); const authorityClosureApply = authorityClosureApplyCommandFor(workItems, cliPath); const mutationContract = phaseMutationContractFor(result, workItems); const milestone = milestoneName(result); @@ -3640,6 +4035,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { ...(sourceRepairApply ? { sourceMergeRepairApplyCommand: sourceRepairApply } : {}), ...(matrixSelectionApply ? { matrixSelectionApplyCommand: matrixSelectionApply } : {}), ...(matrixProposalApply ? { matrixProposalApplyCommand: matrixProposalApply } : {}), + ...(issueClosureApply ? { issueClosureApplyCommand: issueClosureApply } : {}), ...(authorityClosureApply ? { authorityClosureApplyCommand: authorityClosureApply } : {}), ...(reference ? { guidanceCommand: reference } : {}), ...(mutationContract ? { mutationContract } : {}), @@ -3656,6 +4052,7 @@ export function milestoneEnvelopeFor(result, cliPath, workItems) { sourceRepairApply, matrixSelectionApply, matrixProposalApply, + issueClosureApply, authorityClosureApply, workItems, ), @@ -3719,6 +4116,13 @@ function matrixProposalApplyCommandFor(workItems, cliPath) { + `--proposal-sha256 ${workItems.proposal.proposalSha256}`; } +function issueClosureApplyCommandFor(workItems, cliPath) { + if (workItems?.group !== "issue-closure-apply" || workItems.proposal?.validated !== true) return undefined; + return `node ${JSON.stringify(cliPath)} issue-closure-apply --workspace "$PWD" ` + + `--input-file ${JSON.stringify(workItems.proposal.path)} ` + + `--proposal-sha256 ${workItems.proposal.proposalSha256}`; +} + function authorityClosureApplyCommandFor(workItems, cliPath) { if (workItems?.group !== "authority-closure-apply" || workItems.proposal?.validated !== true) return undefined; return `node ${JSON.stringify(cliPath)} authority-closure-apply --workspace "$PWD" ` @@ -3754,6 +4158,43 @@ function workBatchFor(phase) { function phaseMutationContractFor(result, workItems) { const first = result.errors[0]; + if (first?.phase === "issues" && first.code === "risk_signal_orphaned" + && typeof workItems?.group === "string" && workItems.group.startsWith("issue-closure-")) { + return { + schemaVersion: 1, + phase: "issues", + writer: "main-agent-only", + strategy: "state-bound-proposal-apply", + canonicalPaths: [ + `${STATE_DIRECTORY}/${STATE_FILES.issues}`, + `${STATE_DIRECTORY}/${STATE_FILES.matrices}`, + ], + target: { + errorCode: first.code, + matrixId: workItems.target.matrixId, + matrixIndex: workItems.target.matrixIndex, + entryId: workItems.target.entryId, + entryIndex: workItems.target.entryIndex, + }, + limits: { + maxChangedRecords: workItems.limits.maxRecords + 1, + maxFacts: workItems.limits.maxFacts, + maxSerializedBytes: workItems.limits.maxSerializedBytes, + preserveUnchangedRecords: true, + validateAfterWrite: true, + }, + prerequisites: [ + "Use only the injected target entry, allowed issue rules, facts, and proposal template.", + "Create one fact-grounded issue for each target risk signal through legal judgment.", + "Write only the proposal path, then use the injected apply command after validation.", + ], + interface: { + kind: "state-bound-proposal", + phaseApplyCommandAvailable: workItems.group === "issue-closure-apply", + instruction: "Never edit issues.json or matrices.json directly for this closure.", + }, + }; + } if (first?.phase === "authorities" && typeof workItems?.group === "string" && workItems.group.startsWith("authority-closure-")) { return { @@ -3871,6 +4312,7 @@ function nextActionFor( sourceRepairApply, matrixSelectionApply, matrixProposalApply, + issueClosureApply, authorityClosureApply, workItems, ) { @@ -4001,6 +4443,25 @@ function nextActionFor( return `The one-matrix proposal is valid and state-bound. Execute matrixProposalApplyCommand exactly as the next tool call: ${matrixProposalApply}. ` + `Do not inspect or edit any canonical ledger manually. The command replaces only the pending target matrix atomically and immediately runs the unchanged validator.`; } + if (first?.phase === "issues" && first.code === "risk_signal_orphaned" + && workItems?.group === "issue-closure-propose") { + if (workItems.proposal?.validationError) { + return `The issue closure proposal at ${JSON.stringify(workItems.proposal.path)} was rejected with ` + + `${workItems.proposal.validationError.code}: ${workItems.proposal.validationError.message}. ` + + `Rewrite only that proposal from workItems.proposal.template and workItems.preparedSlice. ` + + `Do not read canonical ledgers, source files, plugin source, references, or CLI help; the plugin will validate the rewritten state-bound transaction.`; + } + return `The orphaned risk-signal matrix entry and its complete bounded facts are already injected in workItems.preparedSlice. ` + + `As the next tool call, write exactly one proposal to ${JSON.stringify(workItems.proposal.path)} using workItems.proposal.template. ` + + `Replace every placeholder with fact-grounded legal judgment, create exactly one matching issue per allowed target risk signal, and preserve the injected fact IDs exactly. ` + + `Do not read issues.json, matrices.json, facts.json, source files, plugin source, references, or CLI help. ` + + `Do not edit a canonical ledger directly; the Legal Plugin will validate the proposal before exposing issue-closure-apply.`; + } + if (first?.phase === "issues" && first.code === "risk_signal_orphaned" + && workItems?.group === "issue-closure-apply") { + return `The bounded issue-matrix closure is valid and state-bound. Execute issueClosureApplyCommand exactly as the next tool call: ${issueClosureApply}. ` + + `Do not inspect or edit the proposal or any canonical ledger. The command applies reciprocal issue links as one logical transaction and immediately runs the unchanged validator.`; + } if (first?.phase === "authorities" && first.code === "legal_authority_links_missing" && workItems?.group === "authority-closure-propose") { if (workItems.proposal?.validationError) { diff --git a/tests/agent/legal-coverage-plugin-runtime.spec.ts b/tests/agent/legal-coverage-plugin-runtime.spec.ts index 6da37769..9ccd7381 100644 --- a/tests/agent/legal-coverage-plugin-runtime.spec.ts +++ b/tests/agent/legal-coverage-plugin-runtime.spec.ts @@ -202,6 +202,105 @@ test("real gateway executes the state-bound legal authority closure with complet } }); +test("real gateway executes the state-bound legal issue closure with complete O1 evidence", async () => { + const root = await mkdtemp(join(tmpdir(), "pilotdeck-legal-issue-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"), ISSUE_CLOSURE_TEST_CONFIG); + await writeIssueClosureState(projectRoot); + + const runtime = createLocalGateway({ + projectRoot, + fallbackProjectRoot: projectRoot, + pilotHome, + env: { ...process.env, PILOT_HOME: pilotHome, PILOTDECK_BUILD_SHA: "issue-closure-test" }, + __testModelFactory: () => issueClosureModelRuntime(requests), + }); + try { + const events = []; + for await (const event of runtime.gateway.submitTurn({ + sessionKey: "legal-issue-closure-session", + channelKey: "test", + projectKey: projectRoot, + message: "Continue the configured legal due diligence review.", + canPrompt: false, + timeoutMs: 60_000, + })) { + events.push(event); + } + + const agentRequests = requests.filter((request) => !isCompactionRequest(request)); + assert.equal(agentRequests.length, 3, JSON.stringify(events)); + assert.match(messageText(agentRequests[0]?.messages ?? []), /"group": "issue-closure-propose"/u); + assert.match(messageText(agentRequests[0]?.messages ?? []), /"factId": "F-001"/u); + assert.match(messageText(agentRequests[1]?.messages ?? []), /"group": "issue-closure-apply"/u); + assert.match(messageText(agentRequests[1]?.messages ?? []), /issue-closure-apply/u); + assert.match(messageText(agentRequests[2]?.messages ?? []), /"milestone": "COMPLETE"/u); + assert.equal(events.some((event) => event.type === "turn_completed" && event.finishReason === "completed"), true); + + const decisions = events.flatMap((event) => + event.type === "agent_status" && event.event === "progress_lease_evaluated" + ? [[event.detail?.decision, event.detail?.progressOrdinal, event.detail?.handoffOrdinal]] + : [] + ); + assert.deepEqual(decisions, [ + ["baseline", 0, 0], + ["handoff_grace", 0, 1], + ["completed", 1, 1], + ]); + + const stateRoot = join(projectRoot, STATE_ROOT); + const proof = JSON.parse(await readFile(join(stateRoot, "completion-proof.json"), "utf8")) as { stateHash: string }; + assert.match(proof.stateHash, /^[a-f0-9]{64}$/u); + const issues = JSON.parse(await readFile(join(stateRoot, "issues.json"), "utf8")) as { + issues: Array<{ id: string; critical: boolean; authorityIds: string[] }>; + }; + const matrices = JSON.parse(await readFile(join(stateRoot, "matrices.json"), "utf8")) as { + matrices: Array<{ id: string; entries: Array<{ issueIds: string[] }> }>; + }; + assert.deepEqual(issues.issues, [{ + id: "I-RISK", + ruleId: "rights-governance-conflict", + status: "open", + severity: "high", + critical: false, + factIds: ["F-001", "F-002"], + authorityIds: [], + analysis: "The synthetic ownership record conflicts with the governance control.", + conclusion: "The governance conflict requires a documented control before closing.", + recommendations: ["Require a verified governance remediation before closing."], + }]); + assert.deepEqual(matrices.matrices[0]?.entries[0]?.issueIds, ["I-RISK"]); + + const storage = createAgentProjectSessionStorage({ + projectRoot, + pilotHome, + sessionId: "legal-issue-closure-session", + }); + const observationPath = join(storage.observabilityDir, "observations.jsonl"); + const observations = await readObservationEvents(observationPath); + const rawObservations = await readFile(observationPath, "utf8"); + 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); + assert.doesNotMatch(rawObservations, /synthetic ownership record conflicts/u); + assert.doesNotMatch(rawObservations, /test-key/u); + } finally { + await runtime.dispose(); + await rm(root, { recursive: true, force: true }); + } +}); + test("real gateway hands off a validated ordinary source proposal before renewing from its durable receipt", async () => { const root = await mkdtemp(join(tmpdir(), "pilotdeck-legal-source-apply-gateway-")); const projectRoot = join(root, "project"); @@ -865,6 +964,66 @@ function authorityClosureModelRuntime(requests: CanonicalModelRequest[]): ModelR }; } +function issueClosureModelRuntime(requests: CanonicalModelRequest[]): ModelRuntime { + return { + async *stream(request) { + if (isCompactionRequest(request)) { + yield { type: "message_start", role: "assistant" }; + yield { type: "text_delta", text: "Continue the state-bound issue closure." }; + 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 === "issue-closure-propose") { + const proposal = validIssueClosureProposal(envelope.workItems.proposal.template); + const proposalPath = envelope.workItems.proposal.path; + const script = [ + 'const { mkdirSync, writeFileSync } = require("node:fs");', + 'const { dirname } = require("node:path");', + `const path = ${JSON.stringify(proposalPath)};`, + "mkdirSync(dirname(path), { recursive: true });", + `writeFileSync(path, ${JSON.stringify(`${JSON.stringify(proposal, null, 2)}\n`)});`, + ].join("\n"); + const toolCall = { + id: "issue-proposal-write", + name: "bash", + input: { + command: "node -e 'eval(Buffer.from(process.argv[1],\"base64\").toString(\"utf8\"))' " + + Buffer.from(script).toString("base64"), + }, + }; + 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 === "issue-closure-apply") { + const toolCall = { + id: "issue-proposal-apply", + name: "bash", + input: { command: envelope.issueClosureApplyCommand }, + }; + 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; + } + yield { type: "text_delta", text: "The legal issue closure is validated." }; + yield { type: "usage", usage: { inputTokens: 20, outputTokens: 6, totalTokens: 26 } }; + yield { type: "message_end", finishReason: "stop" }; + }, + async complete() { + return { role: "assistant", content: [{ type: "text", text: '{"title":"Issue closure QA"}' }], finishReason: "stop" }; + }, + getCapabilities: () => ({ ...DEFAULT_MODEL_CAPABILITIES, maxContextTokens: 1_048_576 }), + getMultimodal: () => DEFAULT_MULTIMODAL_CONSTRAINTS, + getProviderProtocol: () => "openai", + getProviderBaseUrl: () => "https://example.invalid", + }; +} + function legalEnvelopeFromMessages(messages: readonly CanonicalMessage[]): any { const match = messageText(messages).match(/\n([\s\S]*?)\n<\/legal_coverage_state>/u); return match ? JSON.parse(match[1]!) : undefined; @@ -900,6 +1059,152 @@ function validAuthorityClosureProposal(template: Record): Recor }; } +function validIssueClosureProposal(template: Record): Record { + return { + ...template, + issueUpserts: [{ + id: "I-RISK", + ruleId: "rights-governance-conflict", + status: "open", + severity: "high", + critical: false, + factIds: ["F-001", "F-002"], + authorityIds: [], + analysis: "The synthetic ownership record conflicts with the governance control.", + conclusion: "The governance conflict requires a documented control before closing.", + recommendations: ["Require a verified governance remediation before closing."], + }], + matrixEntryLinks: { issueIds: ["I-RISK"] }, + }; +} + +async function writeIssueClosureState(workspace: string): Promise { + const root = join(workspace, STATE_ROOT); + const source = "Synthetic ownership and governance record.\n"; + const opinion = [ + "# Legal Opinion", + "Synthetic ownership does not align with governance controls.", + "Synthetic appointment control remains with a different governance actor.", + "The governance conflict requires a documented control before closing and verified remediation.", + "", + ].join("\n"); + await mkdir(join(workspace, "source-room"), { recursive: true }); + await mkdir(join(workspace, "deliverables"), { recursive: true }); + await mkdir(root, { recursive: true }); + await writeFile(join(workspace, "source-room", "record.txt"), source); + await writeFile(join(workspace, "deliverables", "opinion.md"), opinion); + await writeJson(join(root, "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(root, "sources.json"), { + schemaVersion: 1, + sources: [{ + id: "S-001", + path: "source-room/record.txt", + sha256: sha256(source), + status: "reviewed", + extractionMethod: "plain-text inspection", + evidenceClass: "official-record", + factIds: ["F-001", "F-002"], + unresolvedItems: [], + }], + }); + await writeJson(join(root, "facts.json"), { + schemaVersion: 1, + facts: [ + { + id: "F-001", + subject: "Synthetic ownership", + predicate: "governance alignment", + value: "does not align", + dateOrPeriod: "Synthetic review date", + sourceRefs: [{ sourceId: "S-001", locator: "line 1" }], + evidenceClass: "official-record", + verificationStatus: "verified", + conflictStatus: "none", + material: true, + critical: false, + }, + { + id: "F-002", + subject: "Synthetic appointment control", + predicate: "governance actor", + value: "different actor", + dateOrPeriod: "Synthetic review date", + sourceRefs: [{ sourceId: "S-001", locator: "line 1" }], + evidenceClass: "official-record", + verificationStatus: "verified", + conflictStatus: "none", + material: true, + critical: false, + }, + ], + }); + await writeJson(join(root, "matrices.json"), { + schemaVersion: 1, + matrices: [ + { + id: "equity-capital-timeline", + status: "complete", + entries: [{ + id: "ECT-001", + summary: "Synthetic ownership and governance controls do not align.", + factIds: ["F-001", "F-002"], + riskSignals: ["rights_governance_conflict"], + issueIds: [], + authorityIds: [], + }], + }, + ...REQUIRED_MATRIX_IDS.slice(1).map((id) => ({ + id, + status: "not-applicable", + entries: [], + notApplicableReason: "No separate synthetic relationship is required.", + })), + ], + }); + await writeJson(join(root, "issues.json"), { schemaVersion: 1, issues: [] }); + await writeJson(join(root, "authorities.json"), { schemaVersion: 1, authorities: [] }); + await writeJson(join(root, "coverage.json"), { + schemaVersion: 1, + deliverables: [{ path: "deliverables/opinion.md", sha256: sha256(opinion) }], + sources: [], + facts: [{ + factId: "F-001", + status: "covered", + deliverablePath: "deliverables/opinion.md", + section: "Legal Opinion", + locator: "paragraph 1", + claim: "The ownership fact is material.", + quote: "Synthetic ownership does not align with governance controls.", + }, { + factId: "F-002", + status: "covered", + deliverablePath: "deliverables/opinion.md", + section: "Legal Opinion", + locator: "paragraph 2", + claim: "The appointment control remains separate.", + quote: "Synthetic appointment control remains with a different governance actor.", + }], + issues: [{ + issueId: "I-RISK", + status: "covered", + deliverablePath: "deliverables/opinion.md", + section: "Legal Opinion", + locator: "paragraph 3", + claim: "The governance conflict requires remediation.", + quote: "The governance conflict requires a documented control before closing and verified remediation.", + }], + authorities: [], + }); +} + async function writeAuthorityClosureState(workspace: string): Promise { const root = join(workspace, STATE_ROOT); const source = "Synthetic company record.\n"; @@ -1167,6 +1472,11 @@ observability: queueCapacity: 4096 `; +const ISSUE_CLOSURE_TEST_CONFIG = AUTHORITY_CLOSURE_TEST_CONFIG.replace( + "campaignId: authority-closure-qa", + "campaignId: issue-closure-qa", +); + const SOURCE_REPAIR_TEST_CONFIG = `schemaVersion: 1 agent: model: test/test-model diff --git a/tests/fixtures/convergence/case-09-issue-closure-replay.json b/tests/fixtures/convergence/case-09-issue-closure-replay.json new file mode 100644 index 00000000..eb1d65d9 --- /dev/null +++ b/tests/fixtures/convergence/case-09-issue-closure-replay.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "description": "Sanitized Case 09 shape after one matrix applies and a risk signal has no issue link.", + "target": { + "matrixId": "equity-capital-timeline", + "entryId": "M-SYNTHETIC-001", + "summary": "Synthetic ownership and governance facts require cross-fact analysis.", + "factIds": ["F-SYNTHETIC-001", "F-SYNTHETIC-002", "F-SYNTHETIC-003"], + "riskSignals": ["rights_governance_conflict"], + "issueIds": [], + "authorityIds": [] + }, + "expected": { + "phase": "issues", + "blockingCode": "risk_signal_orphaned", + "workGroup": "issue-closure-propose", + "applyCommand": "issue-closure-apply" + } +} diff --git a/tests/products/legal-coverage.spec.ts b/tests/products/legal-coverage.spec.ts index 981510ee..7e701a9a 100644 --- a/tests/products/legal-coverage.spec.ts +++ b/tests/products/legal-coverage.spec.ts @@ -3592,6 +3592,337 @@ test("legal coverage authority closure uses one bounded state-bound transaction" } }); +test("legal coverage risk-signal issue closure exposes one bounded state-bound transaction", async () => { + const workspace = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-issue-closure-")); + try { + await writeIssueClosureFixture(workspace); + const replay = JSON.parse(await readFile( + resolve("tests/fixtures/convergence/case-09-issue-closure-replay.json"), + "utf8", + )) as { + target: { matrixId: string; entryId: string; summary: string; factIds: string[]; riskSignals: string[]; issueIds: string[]; authorityIds: string[] }; + expected: { phase: string; blockingCode: string; workGroup: string; applyCommand: string }; + }; + + const firstHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + const envelope = legalEnvelope(firstHook) as { + knownGaps: Array<{ phase: string; code: string }>; + workItems: { + group: string; + returned: number; + limits: { maxRecords: number; maxFacts: number; maxSerializedBytes: number }; + preparedSlice: { + target: typeof replay.target & { matrixIndex: number; entryIndex: number }; + facts: Array<{ factId: string }>; + allowedIssueRules: Array<{ ruleId: string; riskSignal: string; whenToUse: string }>; + }; + proposal: { path: string; template: Record }; + }; + issueClosureApplyCommand?: string; + mutationContract: { + strategy: string; + canonicalPaths: string[]; + interface: { kind: string; phaseApplyCommandAvailable: boolean; instruction: string }; + }; + nextAction: string; + }; + + assert.equal(envelope.knownGaps[0]?.phase, replay.expected.phase); + assert.equal(envelope.knownGaps[0]?.code, replay.expected.blockingCode); + assert.equal(envelope.workItems.group, replay.expected.workGroup); + assert.equal(envelope.workItems.returned, 1); + assert.deepEqual(envelope.workItems.limits, { maxRecords: 6, maxFacts: 12, maxSerializedBytes: 24576 }); + assert.deepEqual(envelope.workItems.preparedSlice.target, { + ...replay.target, + matrixIndex: 0, + entryIndex: 0, + }); + assert.deepEqual( + envelope.workItems.preparedSlice.facts.map((fact) => fact.factId), + replay.target.factIds, + ); + assert.equal( + envelope.workItems.preparedSlice.allowedIssueRules.some((rule) => + rule.ruleId === "rights-governance-conflict" + && rule.riskSignal === "rights_governance_conflict" + && rule.whenToUse.includes("ownership") + ), + true, + ); + assert.equal( + (envelope.workItems.proposal.template.issueUpserts as Array<{ critical: boolean }>)[0]?.critical, + true, + ); + assert.match(envelope.workItems.proposal.path, /issue-transactions\/issue-closure-[a-f0-9]{12}\.json$/u); + assert.equal(envelope.issueClosureApplyCommand, undefined); + assert.equal(envelope.mutationContract.strategy, "state-bound-proposal-apply"); + assert.deepEqual(envelope.mutationContract.canonicalPaths, [ + `${STATE_ROOT}/issues.json`, + `${STATE_ROOT}/matrices.json`, + ]); + assert.equal(envelope.mutationContract.interface.kind, "state-bound-proposal"); + assert.equal(envelope.mutationContract.interface.phaseApplyCommandAvailable, false); + assert.match(envelope.mutationContract.interface.instruction, /Never edit issues\.json or matrices\.json directly/u); + assert.match(envelope.nextAction, /write exactly one proposal/u); + assert.match(envelope.nextAction, new RegExp(replay.expected.applyCommand, "u")); + const initialConvergence = firstHook.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + handoffOrdinal: number; + }; + + const root = join(workspace, STATE_ROOT); + const canonicalPaths = ["issues.json", "matrices.json"].map((name) => join(root, name)); + const unchanged = await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))); + const invalid = issueClosureProposal(envelope.workItems.proposal.template); + invalid.issueUpserts[0]!.factIds = ["F-OUT-OF-SCOPE"]; + await writeJson(join(workspace, envelope.workItems.proposal.path), invalid); + const invalidHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + const invalidEnvelope = legalEnvelope(invalidHook) as { + workItems: { group: string; proposal: { validationError: { code: string } } }; + }; + assert.equal(invalidEnvelope.workItems.group, "issue-closure-propose"); + assert.equal(invalidEnvelope.workItems.proposal.validationError.code, "issue_closure_issue_scope_invalid"); + assert.deepEqual(await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))), unchanged); + + const downgraded = issueClosureProposal(envelope.workItems.proposal.template); + downgraded.issueUpserts[0]!.critical = false; + await writeJson(join(workspace, envelope.workItems.proposal.path), downgraded); + const downgradedHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (legalEnvelope(downgradedHook) as { workItems: { proposal: { validationError: { code: string } } } }) + .workItems.proposal.validationError.code, + "issue_closure_criticality_invalid", + ); + assert.deepEqual(await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))), unchanged); + + const duplicate = issueClosureProposal(envelope.workItems.proposal.template); + const existingIssues = JSON.parse(await readFile(join(root, "issues.json"), "utf8")) as { + issues: Array>; + }; + existingIssues.issues.push({ ...duplicate.issueUpserts[0] }); + await writeJson(join(root, "issues.json"), existingIssues); + const duplicatePreparation = legalEnvelope(await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + })) as { workItems: { proposal: { path: string; template: Record } } }; + const duplicateProposal = issueClosureProposal(duplicatePreparation.workItems.proposal.template); + await writeJson(join(workspace, duplicatePreparation.workItems.proposal.path), duplicateProposal); + const duplicateHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (legalEnvelope(duplicateHook) as { workItems: { proposal: { validationError: { code: string } } } }) + .workItems.proposal.validationError.code, + "issue_closure_duplicate_id", + ); + await writeJson(join(root, "issues.json"), { schemaVersion: 1, issues: [] }); + + const extraLink = issueClosureProposal(envelope.workItems.proposal.template); + extraLink.matrixEntryLinks.issueIds.push("I-UNPROPOSED"); + await writeJson(join(workspace, envelope.workItems.proposal.path), extraLink); + const extraLinkHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (legalEnvelope(extraLinkHook) as { workItems: { proposal: { validationError: { code: string } } } }) + .workItems.proposal.validationError.code, + "issue_closure_links_invalid", + ); + + const valid = issueClosureProposal(envelope.workItems.proposal.template); + await writeJson(join(workspace, envelope.workItems.proposal.path), valid); + const validHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + const validEnvelope = legalEnvelope(validHook) as { + workItems: { + group: string; + preparedSlice?: unknown; + proposal: { + path: string; + proposalSha256: string; + validated: boolean; + template?: unknown; + transaction?: unknown; + }; + }; + issueClosureApplyCommand: string; + mutationContract: { interface: { phaseApplyCommandAvailable: boolean } }; + nextAction: string; + }; + assert.equal(validEnvelope.workItems.group, "issue-closure-apply"); + assert.equal(validEnvelope.workItems.preparedSlice, undefined); + assert.equal(validEnvelope.workItems.proposal.validated, true); + assert.equal(validEnvelope.workItems.proposal.template, undefined); + assert.equal(validEnvelope.workItems.proposal.transaction, undefined); + assert.equal(validEnvelope.mutationContract.interface.phaseApplyCommandAvailable, true); + assert.match(validEnvelope.issueClosureApplyCommand, /issue-closure-apply/u); + assert.match(validEnvelope.issueClosureApplyCommand, new RegExp(validEnvelope.workItems.proposal.proposalSha256, "u")); + assert.match(validEnvelope.nextAction, /Execute issueClosureApplyCommand exactly/u); + const validConvergence = validHook.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { + handoffOrdinal: number; + }; + assert.equal(validConvergence.handoffOrdinal, initialConvergence.handoffOrdinal + 1); + const replayedHook = await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure", + transcriptPath: "", + cwd: workspace, + }); + assert.equal( + (replayedHook.hookSpecificOutput.modelRequestPatch?.metadata?.pilotdeckConvergence as { handoffOrdinal: number }) + .handoffOrdinal, + validConvergence.handoffOrdinal, + ); + + const applied = await runCli( + workspace, + "issue-closure-apply", + "--input-file", validEnvelope.workItems.proposal.path, + "--proposal-sha256", validEnvelope.workItems.proposal.proposalSha256, + ); + assert.equal(applied.exitCode, 0, applied.stderr); + const appliedResult = JSON.parse(applied.stdout) as { + applied: boolean; matrixId: string; entryId: string; issuesUpserted: number; + }; + assert.deepEqual( + { + applied: appliedResult.applied, + matrixId: appliedResult.matrixId, + entryId: appliedResult.entryId, + issuesUpserted: appliedResult.issuesUpserted, + }, + { + applied: true, + matrixId: replay.target.matrixId, + entryId: replay.target.entryId, + issuesUpserted: 1, + }, + ); + const appliedIssues = JSON.parse(await readFile(join(root, "issues.json"), "utf8")) as { + issues: Array>; + }; + const appliedMatrices = JSON.parse(await readFile(join(root, "matrices.json"), "utf8")) as { + matrices: Array<{ id: string; entries: Array<{ id: string; issueIds: string[] }> }>; + }; + assert.deepEqual(appliedIssues.issues, valid.issueUpserts); + assert.deepEqual( + appliedMatrices.matrices[0]!.entries[0]!.issueIds, + valid.matrixEntryLinks.issueIds, + ); + assert.deepEqual( + JSON.parse(await readFile(join(root, "authorities.json"), "utf8")), + { schemaVersion: 1, authorities: [] }, + ); + + const replayApply = await runCli( + workspace, + "issue-closure-apply", + "--input-file", validEnvelope.workItems.proposal.path, + "--proposal-sha256", validEnvelope.workItems.proposal.proposalSha256, + ); + assert.equal(replayApply.exitCode, 1); + assert.equal(JSON.parse(replayApply.stderr).error.code, "issue_closure_out_of_scope"); + } finally { + await rm(workspace, { recursive: true, force: true }); + } +}); + +test("legal coverage rejects changed and symlinked issue closure proposals without ledger mutation", async () => { + const workspace = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-issue-closure-reject-")); + const outside = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-issue-closure-outside-")); + try { + await writeIssueClosureFixture(workspace); + const root = join(workspace, STATE_ROOT); + const first = legalEnvelope(await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure-reject", + transcriptPath: "", + cwd: workspace, + })) as { workItems: { proposal: { path: string; template: Record } } }; + const proposal = issueClosureProposal(first.workItems.proposal.template); + await writeJson(join(workspace, first.workItems.proposal.path), proposal); + const validated = legalEnvelope(await runHook({ + hookEventName: "PreModelRequest", + sessionId: "issue-closure-reject", + transcriptPath: "", + cwd: workspace, + })) as { workItems: { proposal: { path: string; proposalSha256: string } } }; + const canonicalPaths = ["issues.json", "matrices.json"].map((name) => join(root, name)); + const unchanged = await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))); + + proposal.issueUpserts[0]!.analysis = "Changed after validation."; + await writeJson(join(workspace, validated.workItems.proposal.path), proposal); + const changed = await runCli( + workspace, + "issue-closure-apply", + "--input-file", validated.workItems.proposal.path, + "--proposal-sha256", validated.workItems.proposal.proposalSha256, + ); + assert.equal(changed.exitCode, 1); + assert.equal(JSON.parse(changed.stderr).error.code, "issue_closure_changed"); + assert.deepEqual(await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))), unchanged); + + await writeJson(join(workspace, validated.workItems.proposal.path), issueClosureProposal(first.workItems.proposal.template)); + const factsPath = join(root, "facts.json"); + const facts = JSON.parse(await readFile(factsPath, "utf8")) as { facts: Array> }; + facts.facts[0]!.verificationStatus = "partially-verified"; + await writeJson(factsPath, facts); + const stale = await runCli( + workspace, + "issue-closure-apply", + "--input-file", validated.workItems.proposal.path, + "--proposal-sha256", sha256(await readFile(join(workspace, validated.workItems.proposal.path))), + ); + assert.equal(stale.exitCode, 1); + assert.equal(JSON.parse(stale.stderr).error.code, "issue_closure_out_of_scope"); + assert.deepEqual(await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))), unchanged); + + await rm(join(root, "issue-transactions"), { recursive: true, force: true }); + await symlink(outside, join(root, "issue-transactions")); + const outsideProposal = join(outside, validated.workItems.proposal.path.split("/").at(-1)!); + await writeJson(outsideProposal, issueClosureProposal(first.workItems.proposal.template)); + const outsideBytes = await readFile(outsideProposal); + const symlinked = await runCli( + workspace, + "issue-closure-apply", + "--input-file", validated.workItems.proposal.path, + "--proposal-sha256", sha256(outsideBytes), + ); + assert.equal(symlinked.exitCode, 1); + assert.equal(JSON.parse(symlinked.stderr).error.code, "issue_closure_apply_failed"); + assert.deepEqual(await Promise.all(canonicalPaths.map((path) => readFile(path, "utf8"))), unchanged); + } finally { + await rm(workspace, { recursive: true, force: true }); + await rm(outside, { recursive: true, force: true }); + } +}); + test("legal coverage rejects changed and symlinked authority closure proposals without ledger mutation", async () => { const workspace = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-authority-closure-reject-")); const outside = await mkdtemp(join(tmpdir(), "pilotdeck-legal-coverage-authority-closure-outside-")); @@ -3928,6 +4259,104 @@ async function writeAuthorityClosureFixture(workspace: string): Promise { await writeJson(join(root, "authorities.json"), { schemaVersion: 1, authorities: [] }); } +async function writeIssueClosureFixture(workspace: string): Promise { + await writeCompleteFixture(workspace); + const root = join(workspace, STATE_ROOT); + const sourcePath = join(root, "sources.json"); + const sources = JSON.parse(await readFile(sourcePath, "utf8")) as { + sources: Array<{ factIds: string[] }>; + }; + sources.sources[0]!.factIds = ["F-SYNTHETIC-001", "F-SYNTHETIC-002", "F-SYNTHETIC-003"]; + await writeJson(sourcePath, sources); + await writeJson(join(root, "facts.json"), { + schemaVersion: 1, + facts: [1, 2, 3].map((index) => ({ + id: `F-SYNTHETIC-00${index}`, + subject: `Synthetic governance subject ${index}`, + predicate: `synthetic governance predicate ${index}`, + value: `Synthetic governance value ${index}`, + missingTimeReason: "The synthetic source has no relevant date.", + sourceRefs: [{ sourceId: "S-001", locator: `line ${index}` }], + evidenceClass: "official-record", + verificationStatus: "verified", + conflictStatus: "none", + material: true, + critical: index === 1, + })), + }); + await writeJson(join(root, "matrices.json"), { + schemaVersion: 1, + matrices: [ + { + id: "equity-capital-timeline", + status: "complete", + entries: [{ + id: "M-SYNTHETIC-001", + summary: "Synthetic ownership and governance facts require cross-fact analysis.", + factIds: ["F-SYNTHETIC-001", "F-SYNTHETIC-002", "F-SYNTHETIC-003"], + riskSignals: ["rights_governance_conflict"], + issueIds: [], + authorityIds: [], + }], + }, + ...[ + "holding-platform-special-rights", + "governance-personnel-timeline", + "contract-key-terms", + "debt-collateral-liquidity", + "employment-ip-timeline", + "legal-authority", + ].map((id) => ({ + id, + status: "not-applicable", + entries: [], + notApplicableReason: "No additional synthetic relation is required for this fixture.", + })), + ], + }); + await writeJson(join(root, "issues.json"), { schemaVersion: 1, issues: [] }); + await writeJson(join(root, "authorities.json"), { schemaVersion: 1, authorities: [] }); + await mkdir(join(root, "issue-transactions"), { recursive: true }); +} + +function issueClosureProposal(template: Record): { + schemaVersion: number; + phase: string; + group: string; + expectedStateHash: string; + targetMatrixId: string; + targetEntryId: string; + preparedSliceSha256: string; + issueUpserts: Array & { factIds: string[] }>; + matrixEntryLinks: { issueIds: string[] }; +} { + const envelope = structuredClone(template) as { + schemaVersion: number; + phase: string; + group: string; + expectedStateHash: string; + targetMatrixId: string; + targetEntryId: string; + preparedSliceSha256: string; + }; + return { + ...envelope, + issueUpserts: [{ + id: "I-SYNTHETIC-001", + ruleId: "rights-governance-conflict", + status: "open", + severity: "high", + critical: true, + factIds: ["F-SYNTHETIC-001", "F-SYNTHETIC-002", "F-SYNTHETIC-003"], + authorityIds: [], + analysis: "The synthetic ownership and governance facts do not align.", + conclusion: "The governance conflict requires a documented transaction control.", + recommendations: ["Require a verified governance remediation before closing."], + }], + matrixEntryLinks: { issueIds: ["I-SYNTHETIC-001"] }, + }; +} + function authorityClosureProposal(template: Record): { schemaVersion: number; phase: string; From d10779eaab8b141de2568241fc90a49fcedfb7b0 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 13:36:54 +0800 Subject: [PATCH 2/6] docs(legal): direct orphaned issue transactions --- .../legal-coverage/skills/conduct-legal-due-diligence/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/legal/plugins/legal-coverage/skills/conduct-legal-due-diligence/SKILL.md b/products/legal/plugins/legal-coverage/skills/conduct-legal-due-diligence/SKILL.md index 784d34c3..64760177 100644 --- a/products/legal/plugins/legal-coverage/skills/conduct-legal-due-diligence/SKILL.md +++ b/products/legal/plugins/legal-coverage/skills/conduct-legal-due-diligence/SKILL.md @@ -45,7 +45,7 @@ Create the legal analysis; let the bundled validator enforce structure and cover 1. Complete every required matrix in `matrices.json`, or record a fact-grounded not-applicable reason. Link every entry to facts. While a matrix is `pending`, use the injected bounded evidence-index selection, proposal, and apply protocol; never read the full `facts.json` or edit `matrices.json` directly. The selection checkpoint records your legal judgment, and the plugin only validates and applies the one-matrix transaction. 2. Link every material or critical fact into at least one matrix. Never mark all matrices not-applicable merely to obtain a structural pass. -3. Apply the cross-fact rules. Create an issue for every timeline collision, threshold breach, rights or governance conflict, liquidity relationship, employment or IP ownership risk, and unresolved source contradiction. +3. Apply the cross-fact rules. Create an issue for every timeline collision, threshold breach, rights or governance conflict, liquidity relationship, employment or IP ownership risk, and unresolved source contradiction. When the issue milestone exposes `issue-closure-propose`, use only its injected target matrix entry, fact slice, allowed rules, and proposal template. Write only the injected proposal path; after validation, execute `issueClosureApplyCommand` exactly. Do not directly edit `issues.json` or `matrices.json` for an orphaned matrix risk signal. 4. Separate facts, assumptions, analysis, unresolved matters, conclusions, and recommendations. Preserve uncertainty instead of choosing an unsupported version. 5. Translate each material risk into concrete controls such as conditions precedent, remediation, representations, warranties, indemnities, price or structure changes, covenants, or post-closing monitoring. 6. Record every relied-on legal authority in `authorities.json`. Every critical issue requires at least one authority. For verified authorities, retain the name, article, effective version and date, source locator, and supported conclusion. Mark unverifiable citations pending instead of fabricating them. From 4b9464b6600e3b4117d395d10f92a7320e72dd95 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 13:37:03 +0800 Subject: [PATCH 3/6] docs(convergence): define V24R14 issue closure gate --- ...RGENCE_V24R14_ISSUE_CLOSURE_TRANSACTION.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/PILOTDECK_CONVERGENCE_V24R14_ISSUE_CLOSURE_TRANSACTION.md diff --git a/docs/PILOTDECK_CONVERGENCE_V24R14_ISSUE_CLOSURE_TRANSACTION.md b/docs/PILOTDECK_CONVERGENCE_V24R14_ISSUE_CLOSURE_TRANSACTION.md new file mode 100644 index 00000000..1aaab7e7 --- /dev/null +++ b/docs/PILOTDECK_CONVERGENCE_V24R14_ISSUE_CLOSURE_TRANSACTION.md @@ -0,0 +1,93 @@ +# PilotDeck V24R14 issue closure transaction + +## Problem contract + +V24R13 proves that capacity-aware full compaction resolves the Case 09 Core +context-lifecycle blocker. The preserved V24R13 run reaches 24 reviewed sources, +100 facts, and one applied matrix before failing closed on +`issues/risk_signal_orphaned`. + +The failure is an executable-interface gap in the Legal Coverage plugin: + +- dynamic context identifies the validator error and injects issue-rule + guidance; +- the CLI provides state-bound source, matrix, and authority transactions; +- no issue proposal/apply transaction exists; +- the generic issue instruction does not provide a bounded evidence slice, + exact document schema, reciprocal matrix-link update, or apply command. + +The model therefore loads issue guidance, validates, and inspects CLI help but +cannot form a durable issue mutation before Progress Lease correctly fails +closed. + +## Scope + +V24R14 changes only the Legal Coverage plugin and its tests, docs, and QA +evidence. It does not change: + +- Core compaction or context thresholds; +- Progress Lease limits or decisions; +- validator rules, phase ordering, or completion authority; +- source, matrix, authority, coverage, or report contracts; +- Router, Memory, model, Skills, corpus, runner, or deadlines. + +Only the first blocking error `issues/risk_signal_orphaned` is supported. Other +issue error codes remain fail closed and retain the existing interface. + +## Transaction contract + +The plugin exposes one deterministic transaction for one matrix entry: + +1. Locate the first matrix entry with risk signals and no linked issue IDs. +2. Inject only the target entry, its complete bounded fact slice, allowed + signal-to-rule mappings, and an exact proposal template. +3. Require a proposal at + `.pilotdeck/work/legal-coverage/issue-transactions/issue-closure-.json`. +4. Validate the proposal without mutating canonical state. +5. Expose `issue-closure-apply` only after the exact proposal bytes pass. +6. Recheck path, SHA-256, state hash, target identity, and proposal scope at + apply time. +7. Atomically append the new issues and replace only the target matrix entry's + `issueIds`; roll back both ledgers if either write fails. +8. Run the unchanged validator immediately after apply. + +The bounded interface is at most one matrix entry, 12 target facts, one issue +per unique risk signal, and 24 KiB serialized proposal/prepared context. + +## Validation invariants + +- Proposal keys, phase, group, state hash, target entry, and prepared-slice + hash must exactly match the injected template. +- Every proposed issue ID is new, unique, non-placeholder, and canonical. +- Every target risk signal has exactly one issue with its mapped rule ID. +- No issue rule outside the target signals is accepted. +- Every issue uses exactly the target entry's fact IDs and contains status, + severity, critical flag, analysis, conclusion, recommendations, and an + authority ID array. +- Every issue's critical flag equals whether the target fact slice contains a + critical fact; the transaction cannot downgrade critical evidence to avoid + the unchanged authority requirement. +- Matrix links equal the proposed issue IDs exactly; existing authority links + and all unrelated records are preserved. +- Invalid, changed, stale, replayed, oversized, outside-workspace, or symlinked + proposals fail without canonical mutation. +- A validated proposal advances only the opaque handoff signal. Canonical + state and semantic progress advance only after apply. + +## Counterexample + +The regression fixture is synthetic and contains no case text. It preserves +only the failure shape: a complete matrix entry with three bounded facts, one +`rights_governance_conflict` signal, no issue links, empty issue ledger, and a +`risk_signal_orphaned` first error. The red assertion requires the dynamic +milestone to provide a state-bound issue proposal/apply path instead of generic +repair prose. + +## Gate + +The code gate requires focused Legal Coverage tests, convergence tests, and the +full repository suite. The product gate requires a new immutable campaign with +the same runner, baseline, model, Skills, corpus, Router/Memory controls, +O1 profile, deadline, and Lease `8/2`. V25 and the 85-case campaign remain +blocked until Case 09 completes validator, completion proof, and substantive +report contracts. From 0e3bb688c663cd4b1729cb58e2d32e62d16a11a3 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 13:37:25 +0800 Subject: [PATCH 4/6] test(legal): record V24R14 code gate evidence --- .../preserved-state-probe.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/preserved-state-probe.json diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/preserved-state-probe.json b/.omo/evidence/20260727-v24r14-issue-closure-transaction/preserved-state-probe.json new file mode 100644 index 00000000..3e1dbcc1 --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/preserved-state-probe.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "surface": "legal-coverage PreModelRequest hook process", + "sourceCampaignFrozenAndUnchanged": true, + "copiedCanonicalLedgersUnchanged": true, + "blockingCode": "risk_signal_orphaned", + "workGroup": "issue-closure-propose", + "returned": 1, + "hasMore": false, + "target": { + "matrixIndex": 0, + "entryIndex": 1 + }, + "limits": { + "maxRecords": 6, + "maxFacts": 12, + "maxSerializedBytes": 24576 + }, + "factCount": 5, + "criticalFactCount": 4, + "riskSignalCount": 1, + "allowedRuleCount": 1, + "issueTemplateCount": 1, + "templateCriticalityPreserved": true, + "applyCommandExposedBeforeValidation": false, + "proposalPathIsDeterministic": true, + "convergence": { + "phase": "issues", + "blockingCode": "risk_signal_orphaned", + "remainingCount": 358, + "nextBatchGroup": "issue-closure-propose" + }, + "omitted": [ + "source text", + "fact text", + "prompt content", + "model reasoning", + "credentials" + ] +} From 936f01ce832d51fc2c5991c44ac8fa0ab0da6b86 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 13:37:41 +0800 Subject: [PATCH 5/6] test(legal): preserve V24R14 test logs --- .../cross-layer-focused.log | 293 +++ .../diff-check.log | 0 .../focused-issue-closure.log | 22 + .../full-test.log | 1947 +++++++++++++++++ .../real-gateway.log | 24 + 5 files changed, 2286 insertions(+) create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/cross-layer-focused.log create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/diff-check.log create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/focused-issue-closure.log create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/full-test.log create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/real-gateway.log diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/cross-layer-focused.log b/.omo/evidence/20260727-v24r14-issue-closure-transaction/cross-layer-focused.log new file mode 100644 index 00000000..08a9e582 --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/cross-layer-focused.log @@ -0,0 +1,293 @@ +TAP version 13 +# (node:71703) 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 1 - local gateway applies project hook context and enforces artifact correction + --- + duration_ms: 574.442458 + type: 'test' + ... +# Subtest: progress lease stays inert unless explicitly enabled +ok 2 - progress lease stays inert unless explicitly enabled + --- + duration_ms: 0.78325 + type: 'test' + ... +# Subtest: opaque hash churn is stagnant while a smaller remaining count renews the lease +ok 3 - opaque hash churn is stagnant while a smaller remaining count renews the lease + --- + duration_ms: 0.331375 + type: 'test' + ... +# Subtest: only a strictly increasing domain progress ordinal renews the lease +ok 4 - only a strictly increasing domain progress ordinal renews the lease + --- + duration_ms: 0.136292 + type: 'test' + ... +# Subtest: a lower remaining count cannot roll the stored progress ordinal backward +ok 5 - a lower remaining count cannot roll the stored progress ordinal backward + --- + duration_ms: 0.174166 + type: 'test' + ... +# Subtest: two stagnant observations require a boundary and allow exactly one post-boundary turn +ok 6 - two stagnant observations require a boundary and allow exactly one post-boundary turn + --- + duration_ms: 0.133125 + type: 'test' + ... +# Subtest: new repair feedback after a boundary gets one delivery turn without renewing progress +ok 7 - new repair feedback after a boundary gets one delivery turn without renewing progress + --- + duration_ms: 0.296959 + type: 'test' + ... +# Subtest: genuine progress after repair feedback renews the lease +ok 8 - genuine progress after repair feedback renews the lease + --- + duration_ms: 0.091708 + type: 'test' + ... +# Subtest: a newly prepared repair target gets one non-progress request after feedback +ok 9 - a newly prepared repair target gets one non-progress request after feedback + --- + duration_ms: 0.106917 + type: 'test' + ... +# Subtest: genuine progress immediately after repair preparation renews the lease +ok 10 - genuine progress immediately after repair preparation renews the lease + --- + duration_ms: 0.329292 + type: 'test' + ... +# Subtest: repair feedback co-delivered by a boundary preserves the later preparation turn +ok 11 - repair feedback co-delivered by a boundary preserves the later preparation turn + --- + duration_ms: 0.467083 + type: 'test' + ... +# Subtest: a boundary co-delivering repair feedback and preparation grants no replay turn +ok 12 - a boundary co-delivering repair feedback and preparation grants no replay turn + --- + duration_ms: 0.282541 + type: 'test' + ... +# Subtest: preparation observed before feedback cannot be replayed after the boundary +ok 13 - preparation observed before feedback cannot be replayed after the boundary + --- + duration_ms: 0.101042 + type: 'test' + ... +# Subtest: a second repair revision cannot replace missing progress after feedback +ok 14 - a second repair revision cannot replace missing progress after feedback + --- + duration_ms: 0.080875 + type: 'test' + ... +# Subtest: repair feedback already delivered before a boundary cannot be replayed as grace +ok 15 - repair feedback already delivered before a boundary cannot be replayed as grace + --- + duration_ms: 0.065709 + type: 'test' + ... +# Subtest: a new operational handoff gets one bounded request without renewing progress +ok 16 - a new operational handoff gets one bounded request without renewing progress + --- + duration_ms: 0.106625 + type: 'test' + ... +# Subtest: a post-boundary handoff is bounded and its replay still fails closed +ok 17 - a post-boundary handoff is bounded and its replay still fails closed + --- + duration_ms: 0.069584 + type: 'test' + ... +# Subtest: simultaneous repair and handoff revisions cannot be redeemed on separate turns +ok 18 - simultaneous repair and handoff revisions cannot be redeemed on separate turns + --- + duration_ms: 0.077542 + type: 'test' + ... +# Subtest: a lower handoff ordinal cannot be replayed as grace +ok 19 - a lower handoff ordinal cannot be replayed as grace + --- + duration_ms: 0.05475 + type: 'test' + ... +# Subtest: handoff allowance has a hard per-progress-epoch limit and resets only on progress +ok 20 - handoff allowance has a hard per-progress-epoch limit and resets only on progress + --- + duration_ms: 0.077375 + type: 'test' + ... +# Subtest: handoff cannot bypass an unavailable required boundary +ok 21 - handoff cannot bypass an unavailable required boundary + --- + duration_ms: 0.061792 + type: 'test' + ... +# Subtest: sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint +ok 22 - sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint + --- + duration_ms: 2.063167 + type: 'test' + ... +# Subtest: cold-start allowance expires after the first explicit domain progress +ok 23 - cold-start allowance expires after the first explicit domain progress + --- + duration_ms: 0.1035 + type: 'test' + ... +# Subtest: evaluation mode fails closed when the required boundary is rejected or unavailable +ok 24 - evaluation mode fails closed when the required boundary is rejected or unavailable + --- + duration_ms: 0.076 + type: 'test' + ... +# Subtest: a completed report releases the tracked scope +ok 25 - a completed report releases the tracked scope + --- + duration_ms: 0.059167 + type: 'test' + ... +# Subtest: convergence metadata parser rejects malformed and oversized reports +ok 26 - convergence metadata parser rejects malformed and oversized reports + --- + duration_ms: 0.176084 + type: 'test' + ... +# Subtest: auto compaction reports summary success separately from rejected oversized output +ok 27 - auto compaction reports summary success separately from rejected oversized output + --- + duration_ms: 3.424417 + type: 'test' + ... +# Subtest: auto compaction marks a budget-clearing summary as applied +ok 28 - auto compaction marks a budget-clearing summary as applied + --- + duration_ms: 0.1015 + type: 'test' + ... +# Subtest: progress policy can force a full boundary while the token budget is still healthy +ok 29 - progress policy can force a full boundary while the token budget is still healthy + --- + duration_ms: 0.101708 + type: 'test' + ... +# Subtest: full compaction distinguishes a fully retained trajectory from model failure +ok 30 - full compaction distinguishes a fully retained trajectory from model failure + --- + duration_ms: 302.25775 + type: 'test' + ... +# Subtest: auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt +ok 31 - auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt + --- + duration_ms: 0.129292 + type: 'test' + ... +# Subtest: bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair +ok 32 - bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair + --- + duration_ms: 1.288542 + type: 'test' + ... +# Subtest: full compaction shares one token budget across the exact tail and protected prefix +ok 33 - full compaction shares one token budget across the exact tail and protected prefix + --- + duration_ms: 44.176417 + type: 'test' + ... +# Subtest: full compaction keeps one oversized newest atomic frame intact +ok 34 - full compaction keeps one oversized newest atomic frame intact + --- + duration_ms: 0.494541 + type: 'test' + ... +# Subtest: full compaction summarizes a real-shaped single-prompt Agent trajectory +ok 35 - full compaction summarizes a real-shaped single-prompt Agent trajectory + --- + duration_ms: 0.965167 + type: 'test' + ... +# Subtest: full compaction aligns a token-budget tail boundary to complete tool turns +ok 36 - full compaction aligns a token-budget tail boundary to complete tool turns + --- + duration_ms: 0.453583 + type: 'test' + ... +# Subtest: sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions +ok 37 - sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions + --- + duration_ms: 5.53225 + type: 'test' + ... +# Subtest: lifecycle context is injected as a transient model-only message +ok 38 - lifecycle context is injected as a transient model-only message + --- + duration_ms: 11.480459 + type: 'test' + ... +# Subtest: lifecycle without a dynamic store preserves legacy hook messages +ok 39 - lifecycle without a dynamic store preserves legacy hook messages + --- + duration_ms: 0.108125 + type: 'test' + ... +# Subtest: SessionEnd clears session-scoped runtime state even when a hook throws +ok 40 - SessionEnd clears session-scoped runtime state even when a hook throws + --- + duration_ms: 0.6845 + type: 'test' + ... +# (node:71707) 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 41 - an active domain plugin contributes only generic runtime contracts + --- + duration_ms: 2.568042 + type: 'test' + ... +# Subtest: internal prompts do not reactivate domain detection +ok 42 - internal prompts do not reactivate domain detection + --- + duration_ms: 0.160166 + type: 'test' + ... +# Subtest: parses only the supported model request patch fields +ok 43 - parses only the supported model request patch fields + --- + duration_ms: 1.684542 + type: 'test' + ... +# Subtest: parses bounded dynamic context controls for hook-driven injection +ok 44 - parses bounded dynamic context controls for hook-driven injection + --- + duration_ms: 0.168416 + type: 'test' + ... +# Subtest: Gateway propagates its absolute turn deadline into the Agent session +ok 45 - Gateway propagates its absolute turn deadline into the Agent session + --- + duration_ms: 2.729583 + type: 'test' + ... +# (node:71710) 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 46 - local Gateway records a bounded handoff through boundary to progress + --- + duration_ms: 769.81625 + type: 'test' + ... +1..46 +# tests 46 +# suites 0 +# pass 46 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 1222.821583 diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/diff-check.log b/.omo/evidence/20260727-v24r14-issue-closure-transaction/diff-check.log new file mode 100644 index 00000000..e69de29b diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/focused-issue-closure.log b/.omo/evidence/20260727-v24r14-issue-closure-transaction/focused-issue-closure.log new file mode 100644 index 00000000..ab46d69a --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/focused-issue-closure.log @@ -0,0 +1,22 @@ +TAP version 13 +# Subtest: legal coverage risk-signal issue closure exposes one bounded state-bound transaction +ok 1 - legal coverage risk-signal issue closure exposes one bounded state-bound transaction + --- + duration_ms: 880.304334 + type: 'test' + ... +# Subtest: legal coverage rejects changed and symlinked issue closure proposals without ledger mutation +ok 2 - legal coverage rejects changed and symlinked issue closure proposals without ledger mutation + --- + duration_ms: 327.928834 + type: 'test' + ... +1..2 +# tests 2 +# suites 0 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 1327.569458 diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/full-test.log b/.omo/evidence/20260727-v24r14-issue-closure-transaction/full-test.log new file mode 100644 index 00000000..416e1550 --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/full-test.log @@ -0,0 +1,1947 @@ + +> pilotdeck@0.1.0 test /Users/da/Documents/PilotDeck-worktrees/20260727-fix-convergence-v24r14-issue-closure-transaction-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.948291 + 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.067208 + 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.0585 + type: 'test' + ... +# (node:73180) 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: 21.317333 + type: 'test' + ... +# Subtest: AgentLoop exposes explicit subagent identity to lifecycle hooks +ok 5 - AgentLoop exposes explicit subagent identity to lifecycle hooks + --- + duration_ms: 1.057 + 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: 4.321792 + 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.248375 + 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: 6.492167 + 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: 4.781541 + 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: 15.638542 + 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: 2.528666 + 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.786792 + 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.104 + type: 'test' + ... +# (node:73182) 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: 1643.476875 + 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: 919.714792 + 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: 788.411042 + type: 'test' + ... +# Subtest: real gateway executes the state-bound legal issue closure with complete O1 evidence +ok 17 - real gateway executes the state-bound legal issue closure with complete O1 evidence + --- + duration_ms: 591.608166 + type: 'test' + ... +# Subtest: real gateway hands off a validated ordinary source proposal before renewing from its durable receipt +ok 18 - real gateway hands off a validated ordinary source proposal before renewing from its durable receipt + --- + duration_ms: 791.314917 + type: 'test' + ... +# Subtest: real gateway applies an immutable source repair before renewing legal progress +ok 19 - real gateway applies an immutable source repair before renewing legal progress + --- + duration_ms: 853.578291 + type: 'test' + ... +# (node:73183) 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 20 - local gateway applies project hook context and enforces artifact correction + --- + duration_ms: 1086.898459 + type: 'test' + ... +# Subtest: ordinary edit failures after a successful write do not enter large-file repair +ok 21 - ordinary edit failures after a successful write do not enter large-file repair + --- + duration_ms: 0.754208 + type: 'test' + ... +# Subtest: an explicit post-draft large-file failure starts bounded recovery +ok 22 - an explicit post-draft large-file failure starts bounded recovery + --- + duration_ms: 0.16675 + type: 'test' + ... +# Subtest: successful focused write clears an active large-file repair episode +ok 23 - successful focused write clears an active large-file repair episode + --- + duration_ms: 0.142916 + type: 'test' + ... +# Subtest: permission failures are never reclassified as large-file recovery +ok 24 - permission failures are never reclassified as large-file recovery + --- + duration_ms: 0.043084 + type: 'test' + ... +# Subtest: progress lease stays inert unless explicitly enabled +ok 25 - progress lease stays inert unless explicitly enabled + --- + duration_ms: 0.667125 + type: 'test' + ... +# Subtest: opaque hash churn is stagnant while a smaller remaining count renews the lease +ok 26 - opaque hash churn is stagnant while a smaller remaining count renews the lease + --- + duration_ms: 0.138459 + type: 'test' + ... +# Subtest: only a strictly increasing domain progress ordinal renews the lease +ok 27 - only a strictly increasing domain progress ordinal renews the lease + --- + duration_ms: 0.059 + type: 'test' + ... +# Subtest: a lower remaining count cannot roll the stored progress ordinal backward +ok 28 - a lower remaining count cannot roll the stored progress ordinal backward + --- + duration_ms: 0.080792 + type: 'test' + ... +# Subtest: two stagnant observations require a boundary and allow exactly one post-boundary turn +ok 29 - two stagnant observations require a boundary and allow exactly one post-boundary turn + --- + duration_ms: 0.05275 + type: 'test' + ... +# Subtest: new repair feedback after a boundary gets one delivery turn without renewing progress +ok 30 - new repair feedback after a boundary gets one delivery turn without renewing progress + --- + duration_ms: 0.1475 + type: 'test' + ... +# Subtest: genuine progress after repair feedback renews the lease +ok 31 - genuine progress after repair feedback renews the lease + --- + duration_ms: 0.0445 + type: 'test' + ... +# Subtest: a newly prepared repair target gets one non-progress request after feedback +ok 32 - a newly prepared repair target gets one non-progress request after feedback + --- + duration_ms: 0.052292 + type: 'test' + ... +# Subtest: genuine progress immediately after repair preparation renews the lease +ok 33 - genuine progress immediately after repair preparation renews the lease + --- + duration_ms: 0.171459 + type: 'test' + ... +# Subtest: repair feedback co-delivered by a boundary preserves the later preparation turn +ok 34 - repair feedback co-delivered by a boundary preserves the later preparation turn + --- + duration_ms: 0.229166 + type: 'test' + ... +# Subtest: a boundary co-delivering repair feedback and preparation grants no replay turn +ok 35 - a boundary co-delivering repair feedback and preparation grants no replay turn + --- + duration_ms: 0.073709 + type: 'test' + ... +# Subtest: preparation observed before feedback cannot be replayed after the boundary +ok 36 - preparation observed before feedback cannot be replayed after the boundary + --- + duration_ms: 0.036042 + type: 'test' + ... +# Subtest: a second repair revision cannot replace missing progress after feedback +ok 37 - a second repair revision cannot replace missing progress after feedback + --- + duration_ms: 0.032625 + type: 'test' + ... +# Subtest: repair feedback already delivered before a boundary cannot be replayed as grace +ok 38 - repair feedback already delivered before a boundary cannot be replayed as grace + --- + duration_ms: 0.026834 + type: 'test' + ... +# Subtest: a new operational handoff gets one bounded request without renewing progress +ok 39 - a new operational handoff gets one bounded request without renewing progress + --- + duration_ms: 0.046042 + type: 'test' + ... +# Subtest: a post-boundary handoff is bounded and its replay still fails closed +ok 40 - a post-boundary handoff is bounded and its replay still fails closed + --- + duration_ms: 0.028959 + type: 'test' + ... +# Subtest: simultaneous repair and handoff revisions cannot be redeemed on separate turns +ok 41 - simultaneous repair and handoff revisions cannot be redeemed on separate turns + --- + duration_ms: 0.034875 + type: 'test' + ... +# Subtest: a lower handoff ordinal cannot be replayed as grace +ok 42 - a lower handoff ordinal cannot be replayed as grace + --- + duration_ms: 0.023 + type: 'test' + ... +# Subtest: handoff allowance has a hard per-progress-epoch limit and resets only on progress +ok 43 - 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 44 - handoff cannot bypass an unavailable required boundary + --- + duration_ms: 0.0255 + type: 'test' + ... +# Subtest: sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint +ok 45 - sanitized Case 09 matrix handoff trajectory reaches the next semantic checkpoint + --- + duration_ms: 0.754875 + type: 'test' + ... +# Subtest: cold-start allowance expires after the first explicit domain progress +ok 46 - cold-start allowance expires after the first explicit domain progress + --- + duration_ms: 0.041917 + type: 'test' + ... +# Subtest: evaluation mode fails closed when the required boundary is rejected or unavailable +ok 47 - evaluation mode fails closed when the required boundary is rejected or unavailable + --- + duration_ms: 0.035 + type: 'test' + ... +# Subtest: a completed report releases the tracked scope +ok 48 - a completed report releases the tracked scope + --- + duration_ms: 0.025167 + type: 'test' + ... +# Subtest: convergence metadata parser rejects malformed and oversized reports +ok 49 - convergence metadata parser rejects malformed and oversized reports + --- + duration_ms: 0.086083 + type: 'test' + ... +# Subtest: explore subagent does not probe tool safety before execution +ok 50 - explore subagent does not probe tool safety before execution + --- + duration_ms: 15.712292 + type: 'test' + ... +# Subtest: explore registry ignores an unallowed dynamic execute_code tool without probing it +ok 51 - explore registry ignores an unallowed dynamic execute_code tool without probing it + --- + duration_ms: 0.355834 + type: 'test' + ... +# Subtest: read-only subagent evaluates bash safety from the real command +ok 52 - read-only subagent evaluates bash safety from the real command + --- + duration_ms: 4.535834 + type: 'test' + ... +# Subtest: read-only execute_code checks the real code instead of crashing on registry setup +ok 53 - read-only execute_code checks the real code instead of crashing on registry setup + --- + duration_ms: 0.522375 + type: 'test' + ... +# Subtest: subagent budget defaults to ten minutes without a parent deadline +ok 54 - subagent budget defaults to ten minutes without a parent deadline + --- + duration_ms: 0.964291 + type: 'test' + ... +# Subtest: subagent budget preserves the parent handoff window +ok 55 - subagent budget preserves the parent handoff window + --- + duration_ms: 0.140708 + type: 'test' + ... +# Subtest: subagent budget directive exposes the effective bound and diminishing-return rule +ok 56 - subagent budget directive exposes the effective bound and diminishing-return rule + --- + duration_ms: 0.088375 + type: 'test' + ... +# Subtest: composed subagent timeout aborts with a typed reason +ok 57 - composed subagent timeout aborts with a typed reason + --- + duration_ms: 6.782666 + type: 'test' + ... +# Subtest: subagent operation returns control even when the callee ignores abort +ok 58 - subagent operation returns control even when the callee ignores abort + --- + duration_ms: 8.886416 + type: 'test' + ... +# Subtest: parent abort wins without being misclassified as a child timeout +ok 59 - parent abort wins without being misclassified as a child timeout + --- + duration_ms: 0.778583 + type: 'test' + ... +# Subtest: filterIncompleteToolCalls tolerates malformed messages without content +ok 60 - filterIncompleteToolCalls tolerates malformed messages without content + --- + duration_ms: 3.250583 + type: 'test' + ... +# Subtest: fork timeout closes child lifecycle and lets the parent finish +ok 61 - fork timeout closes child lifecycle and lets the parent finish + --- + duration_ms: 34.3525 + type: 'test' + ... +# Subtest: turn environment provides an isolated PilotDeck-owned work directory +ok 62 - turn environment provides an isolated PilotDeck-owned work directory + --- + duration_ms: 0.599916 + type: 'test' + ... +# Subtest: turn environment inherits the process environment when no override is configured +ok 63 - turn environment inherits the process environment when no override is configured + --- + duration_ms: 0.14425 + type: 'test' + ... +# Subtest: validates a required artifact and keeps contracts isolated by session +ok 64 - validates a required artifact and keeps contracts isolated by session + --- + duration_ms: 7.545 + type: 'test' + ... +# Subtest: missing or wrong-format artifacts fail with reviewer-readable issues +ok 65 - missing or wrong-format artifacts fail with reviewer-readable issues + --- + duration_ms: 3.174209 + type: 'test' + ... +# Subtest: empty required artifacts fail validation +ok 66 - empty required artifacts fail validation + --- + duration_ms: 3.91175 + type: 'test' + ... +# Subtest: rejects traversal and symlink escapes before a domain validator runs +ok 67 - rejects traversal and symlink escapes before a domain validator runs + --- + duration_ms: 12.297125 + type: 'test' + ... +# Subtest: legal-specific validator data stays in the plugin validator +ok 68 - legal-specific validator data stays in the plugin validator + --- + duration_ms: 8.038208 + type: 'test' + ... +# Subtest: validator exceptions become structured failures instead of escaping the runtime +ok 69 - validator exceptions become structured failures instead of escaping the runtime + --- + duration_ms: 1.977458 + type: 'test' + ... +# Subtest: duplicate validator ids cannot override an existing validator +ok 70 - duplicate validator ids cannot override an existing validator + --- + duration_ms: 0.583167 + type: 'test' + ... +# Subtest: contract registration is atomic when one contract is invalid +ok 71 - contract registration is atomic when one contract is invalid + --- + duration_ms: 0.062792 + type: 'test' + ... +# Subtest: extension watcher ignores generated Python and Office runtime files +ok 72 - extension watcher ignores generated Python and Office runtime files + --- + duration_ms: 0.540541 + type: 'test' + ... +# Subtest: config bootstrap does not copy bundled skills into user storage +ok 73 - config bootstrap does not copy bundled skills into user storage + --- + duration_ms: 74.234667 + type: 'test' + ... +# Subtest: Office attachments are reported unsupported before size checks +ok 74 - Office attachments are reported unsupported before size checks + --- + duration_ms: 4.074625 + type: 'test' + ... +# Subtest: auto compaction reports summary success separately from rejected oversized output +ok 75 - auto compaction reports summary success separately from rejected oversized output + --- + duration_ms: 1.43875 + type: 'test' + ... +# Subtest: auto compaction marks a budget-clearing summary as applied +ok 76 - auto compaction marks a budget-clearing summary as applied + --- + duration_ms: 0.080167 + type: 'test' + ... +# Subtest: progress policy can force a full boundary while the token budget is still healthy +ok 77 - progress policy can force a full boundary while the token budget is still healthy + --- + duration_ms: 0.091041 + type: 'test' + ... +# Subtest: full compaction distinguishes a fully retained trajectory from model failure +ok 78 - full compaction distinguishes a fully retained trajectory from model failure + --- + duration_ms: 491.401167 + type: 'test' + ... +# Subtest: auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt +ok 79 - auto compaction propagates no-summarizable as a stable rejection reason without claiming a summary attempt + --- + duration_ms: 0.4655 + type: 'test' + ... +# Subtest: bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair +ok 80 - bounded protected-prefix retention summarizes old agent turns and preserves every kept tool pair + --- + duration_ms: 6.905 + type: 'test' + ... +# Subtest: full compaction shares one token budget across the exact tail and protected prefix +ok 81 - full compaction shares one token budget across the exact tail and protected prefix + --- + duration_ms: 78.710458 + type: 'test' + ... +# Subtest: full compaction keeps one oversized newest atomic frame intact +ok 82 - full compaction keeps one oversized newest atomic frame intact + --- + duration_ms: 0.499 + type: 'test' + ... +# Subtest: full compaction summarizes a real-shaped single-prompt Agent trajectory +ok 83 - full compaction summarizes a real-shaped single-prompt Agent trajectory + --- + duration_ms: 0.909791 + type: 'test' + ... +# Subtest: full compaction aligns a token-budget tail boundary to complete tool turns +ok 84 - full compaction aligns a token-budget tail boundary to complete tool turns + --- + duration_ms: 0.432917 + type: 'test' + ... +# Subtest: sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions +ok 85 - sanitized Case 09 replay retains enough evidence to detect rejected blocking compactions + --- + duration_ms: 9.253917 + type: 'test' + ... +# Subtest: lifecycle context is injected as a transient model-only message +ok 86 - lifecycle context is injected as a transient model-only message + --- + duration_ms: 7.369833 + type: 'test' + ... +# Subtest: lifecycle without a dynamic store preserves legacy hook messages +ok 87 - lifecycle without a dynamic store preserves legacy hook messages + --- + duration_ms: 0.094042 + type: 'test' + ... +# Subtest: SessionEnd clears session-scoped runtime state even when a hook throws +ok 88 - SessionEnd clears session-scoped runtime state even when a hook throws + --- + duration_ms: 0.278958 + type: 'test' + ... +# Subtest: merges pending context by priority and consumes it once +ok 89 - merges pending context by priority and consumes it once + --- + duration_ms: 1.367208 + type: 'test' + ... +# Subtest: re-registering the same source and id replaces stale content without changing order +ok 90 - re-registering the same source and id replaces stale content without changing order + --- + duration_ms: 0.113459 + type: 'test' + ... +# Subtest: expired context is pruned without affecting another session +ok 91 - expired context is pruned without affecting another session + --- + duration_ms: 0.054084 + type: 'test' + ... +# Subtest: blank context is ignored +ok 92 - blank context is ignored + --- + duration_ms: 0.039625 + type: 'test' + ... +# Subtest: clearing a session does not disturb another session with a shared prefix +ok 93 - clearing a session does not disturb another session with a shared prefix + --- + duration_ms: 0.087209 + type: 'test' + ... +# Subtest: context budgets retain higher-priority entries and cap merged prompt size +ok 94 - context budgets retain higher-priority entries and cap merged prompt size + --- + duration_ms: 0.467791 + type: 'test' + ... +# Subtest: source and id tuples cannot collide through delimiter characters +ok 95 - source and id tuples cannot collide through delimiter characters + --- + duration_ms: 0.076166 + type: 'test' + ... +# Subtest: default prompt does not require Markdown links for generated files +ok 96 - default prompt does not require Markdown links for generated files + --- + duration_ms: 3.340792 + type: 'test' + ... +# Subtest: default prompt does not advertise web search when the tool is unavailable +ok 97 - default prompt does not advertise web search when the tool is unavailable + --- + duration_ms: 0.222625 + type: 'test' + ... +# Subtest: available skills include the resolved SKILL.md path and bounded lookup guidance +ok 98 - available skills include the resolved SKILL.md path and bounded lookup guidance + --- + duration_ms: 1.96125 + type: 'test' + ... +# Subtest: budget snapshot uses conservative budget tokens for ratio while preserving real tokens +ok 99 - budget snapshot uses conservative budget tokens for ratio while preserving real tokens + --- + duration_ms: 0.508834 + type: 'test' + ... +# Subtest: tool text under token budget remains inline even when over legacy byte threshold +ok 100 - tool text under token budget remains inline even when over legacy byte threshold + --- + duration_ms: 5840.295625 + type: 'test' + ... +# Subtest: tool text over token budget is persisted with expanded grep-first preview +ok 101 - tool text over token budget is persisted with expanded grep-first preview + --- + duration_ms: 6.90675 + type: 'test' + ... +# Subtest: large tool error references preserve error semantics for model replay +ok 102 - large tool error references preserve error semantics for model replay + --- + duration_ms: 2.244 + type: 'test' + ... +# Subtest: multibyte truncated tool result references advertise read_file access +ok 103 - multibyte truncated tool result references advertise read_file access + --- + duration_ms: 1.226917 + type: 'test' + ... +# Subtest: parses declarative artifact contracts without domain-specific knowledge +ok 104 - parses declarative artifact contracts without domain-specific knowledge + --- + duration_ms: 2.183084 + type: 'test' + ... +# (node:73238) 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 105 - an active domain plugin contributes only generic runtime contracts + --- + duration_ms: 2.902083 + type: 'test' + ... +# Subtest: internal prompts do not reactivate domain detection +ok 106 - internal prompts do not reactivate domain detection + --- + duration_ms: 0.097209 + type: 'test' + ... +# Subtest: parses only the supported model request patch fields +ok 107 - parses only the supported model request patch fields + --- + duration_ms: 1.074458 + type: 'test' + ... +# Subtest: parses bounded dynamic context controls for hook-driven injection +ok 108 - parses bounded dynamic context controls for hook-driven injection + --- + duration_ms: 0.113042 + type: 'test' + ... +# Subtest: standalone skills expose only their slug without a parent-directory namespace +ok 109 - standalone skills expose only their slug without a parent-directory namespace + --- + duration_ms: 7.913709 + type: 'test' + ... +# Subtest: a plugin skill directory used as the configured base never derives a parent namespace +ok 110 - a plugin skill directory used as the configured base never derives a parent namespace + --- + duration_ms: 0.271584 + type: 'test' + ... +# Subtest: standalone skill precedence is project > user > builtin without legacy aliases +ok 111 - standalone skill precedence is project > user > builtin without legacy aliases + --- + duration_ms: 17.811375 + type: 'test' + ... +# Subtest: legacy bundled migration backs up only byte-identical bootstrap copies +ok 112 - legacy bundled migration backs up only byte-identical bootstrap copies + --- + duration_ms: 23.896166 + type: 'test' + ... +# Subtest: legacy bundled migration leaves user skills untouched when the release bundle is missing +ok 113 - legacy bundled migration leaves user skills untouched when the release bundle is missing + --- + duration_ms: 6.403709 + type: 'test' + ... +# Subtest: SkillManager lists built-ins separately and describes override relationships +ok 114 - SkillManager lists built-ins separately and describes override relationships + --- + duration_ms: 34.04075 + type: 'test' + ... +# Subtest: SkillManager permits reading but rejects mutations of built-in skills +ok 115 - SkillManager permits reading but rejects mutations of built-in skills + --- + duration_ms: 10.079833 + type: 'test' + ... +# Subtest: registered plain-text attachments with non-whitelisted names are described as read_file inspectable +ok 116 - registered plain-text attachments with non-whitelisted names are described as read_file inspectable + --- + duration_ms: 8.358333 + type: 'test' + ... +# Subtest: registered Office attachments are still described as not directly inspectable +ok 117 - registered Office attachments are still described as not directly inspectable + --- + duration_ms: 2.318458 + type: 'test' + ... +# Subtest: startPilotDeckServer listens before a background channel finishes starting +ok 118 - startPilotDeckServer listens before a background channel finishes starting + --- + duration_ms: 22.508 + type: 'test' + ... +# (node:73251) 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 119 - browser-use args do not inherit generic proxy env by default + --- + duration_ms: 1.661791 + type: 'test' + ... +# Subtest: browser-use args use explicit browser proxy server +ok 120 - browser-use args use explicit browser proxy server + --- + duration_ms: 0.213583 + type: 'test' + ... +# Subtest: browser-use args only inherit generic proxy env when opted in +ok 121 - browser-use args only inherit generic proxy env when opted in + --- + duration_ms: 0.124875 + type: 'test' + ... +# Subtest: browser-use args use config proxy when browser env proxy is absent +ok 122 - browser-use args use config proxy when browser env proxy is absent + --- + duration_ms: 0.162333 + type: 'test' + ... +# Subtest: browser-use args allow explicit direct mode to disable config proxy +ok 123 - browser-use args allow explicit direct mode to disable config proxy + --- + duration_ms: 0.108959 + type: 'test' + ... +# Subtest: internal prompt dispatch waits for the user turn and fully drains the synthetic turn +ok 124 - internal prompt dispatch waits for the user turn and fully drains the synthetic turn + --- + duration_ms: 8.515083 + type: 'test' + ... +# Subtest: mapAgentEvent propagates runId to streaming lifecycle boundaries +ok 125 - mapAgentEvent propagates runId to streaming lifecycle boundaries + --- + duration_ms: 0.744416 + type: 'test' + ... +# Subtest: mapAgentEvent projects every convergence ordinal +ok 126 - mapAgentEvent projects every convergence ordinal + --- + duration_ms: 0.529833 + type: 'test' + ... +# Subtest: defer returns immediately while a user turn is busy +ok 127 - defer returns immediately while a user turn is busy + --- + duration_ms: 2.0535 + type: 'test' + ... +# Subtest: enqueue waits for idle and dispatches exactly once +ok 128 - enqueue waits for idle and dispatches exactly once + --- + duration_ms: 3.530208 + type: 'test' + ... +# Subtest: same semantic key coalesces in-flight and dedupes after completion +ok 129 - same semantic key coalesces in-flight and dedupes after completion + --- + duration_ms: 0.331125 + type: 'test' + ... +# Subtest: an idle-settle race retries when the gateway reports busy +ok 130 - an idle-settle race retries when the gateway reports busy + --- + duration_ms: 0.315209 + type: 'test' + ... +# Subtest: abort while queued does not run an internal turn +ok 131 - abort while queued does not run an internal turn + --- + duration_ms: 0.214333 + type: 'test' + ... +# Subtest: turn timeouts are reported distinctly from generic execution failures +ok 132 - turn timeouts are reported distinctly from generic execution failures + --- + duration_ms: 0.105209 + type: 'test' + ... +# Subtest: a zero recent-dedupe window disables completed-result deduplication +ok 133 - a zero recent-dedupe window disables completed-result deduplication + --- + duration_ms: 0.300375 + type: 'test' + ... +# Subtest: dispose prevents a queued prompt from starting after shutdown +ok 134 - dispose prevents a queued prompt from starting after shutdown + --- + duration_ms: 0.22675 + type: 'test' + ... +# Subtest: the public WebSocket boundary strips trusted in-process turn controls +ok 135 - the public WebSocket boundary strips trusted in-process turn controls + --- + duration_ms: 0.924666 + type: 'test' + ... +# Subtest: waitForIdle resolves only after the matching turn slot is released +ok 136 - waitForIdle resolves only after the matching turn slot is released + --- + duration_ms: 1.565334 + type: 'test' + ... +# Subtest: waitForIdle honors AbortSignal without releasing the turn +ok 137 - waitForIdle honors AbortSignal without releasing the turn + --- + duration_ms: 0.639542 + type: 'test' + ... +# Subtest: mapAgentEvent bounds live tool result previews at the gateway +ok 138 - mapAgentEvent bounds live tool result previews at the gateway + --- + duration_ms: 1.084375 + type: 'test' + ... +# Subtest: mapAgentEvent bounds large strings inside successful tool data +ok 139 - mapAgentEvent bounds large strings inside successful tool data + --- + duration_ms: 0.295583 + type: 'test' + ... +# Subtest: mapAgentEvent bounds subagent tool result content and preview +ok 140 - mapAgentEvent bounds subagent tool result content and preview + --- + duration_ms: 0.132208 + type: 'test' + ... +# Subtest: Gateway propagates its absolute turn deadline into the Agent session +ok 141 - Gateway propagates its absolute turn deadline into the Agent session + --- + duration_ms: 1.771791 + type: 'test' + ... +# Subtest: WeixinChannel.start returns while QR login is waiting +ok 142 - WeixinChannel.start returns while QR login is waiting + --- + duration_ms: 33.39725 + type: 'test' + ... +# Subtest: channel runtime status reporter persists the latest channel state +ok 143 - channel runtime status reporter persists the latest channel state + --- + duration_ms: 4.591042 + type: 'test' + ... +# Subtest: UI weixin QR route only reads runtime status +ok 144 - UI weixin QR route only reads runtime status + --- + duration_ms: 4.664083 + type: 'test' + ... +# Subtest: UI weixin QR begin route delegates to gateway prepare RPC +ok 145 - UI weixin QR begin route delegates to gateway prepare RPC + --- + duration_ms: 0.181958 + type: 'test' + ... +# Subtest: Gateway settings keeps existing status rendered during silent refresh +ok 146 - Gateway settings keeps existing status rendered during silent refresh + --- + duration_ms: 0.450708 + type: 'test' + ... +# Subtest: Gateway settings starts weixin QR by begin route and ignores stale runtime errors +ok 147 - Gateway settings starts weixin QR by begin route and ignores stale runtime errors + --- + duration_ms: 0.205791 + type: 'test' + ... +# Subtest: Gateway protocol exposes prepare_weixin_login RPC +ok 148 - Gateway protocol exposes prepare_weixin_login RPC + --- + duration_ms: 0.421584 + type: 'test' + ... +# Subtest: applies context, system messages, and restricted model request patches +ok 149 - applies context, system messages, and restricted model request patches + --- + duration_ms: 0.973292 + type: 'test' + ... +# Subtest: blocks before any request mutation is used +ok 150 - blocks before any request mutation is used + --- + duration_ms: 0.056458 + type: 'test' + ... +# Subtest: McpClient keeps stdio clients idle before connection +ok 151 - McpClient keeps stdio clients idle before connection + --- + duration_ms: 0.491208 + type: 'test' + ... +# Subtest: McpClient constructs streamable_http transport without requiring stdio fields +ok 152 - McpClient constructs streamable_http transport without requiring stdio fields + --- + duration_ms: 0.046625 + type: 'test' + ... +# Subtest: McpClient routes streamable_http fetches with bounded timeouts +ok 153 - McpClient routes streamable_http fetches with bounded timeouts + --- + duration_ms: 1.085958 + type: 'test' + ... +# Subtest: expandMcpString expands ${env:*} placeholders +ok 154 - expandMcpString expands ${env:*} placeholders + --- + duration_ms: 0.584875 + type: 'test' + ... +# Subtest: expandMcpString expands ${userHome} placeholder +ok 155 - expandMcpString expands ${userHome} placeholder + --- + duration_ms: 0.092041 + type: 'test' + ... +# Subtest: expandMcpString expands ~ prefix +ok 156 - expandMcpString expands ~ prefix + --- + duration_ms: 0.041291 + type: 'test' + ... +# Subtest: expandMcpString leaves unknown env vars as empty string +ok 157 - expandMcpString leaves unknown env vars as empty string + --- + duration_ms: 0.033334 + type: 'test' + ... +# Subtest: expandMcpString handles combined placeholders +ok 158 - expandMcpString handles combined placeholders + --- + duration_ms: 0.039125 + type: 'test' + ... +# Subtest: expandMcpConfig recursively expands objects and arrays +ok 159 - expandMcpConfig recursively expands objects and arrays + --- + duration_ms: 0.503417 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio env +ok 160 - parsePluginMcpServers expands ${env:*} in stdio env + --- + duration_ms: 0.138083 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in stdio command before transport detection +ok 161 - parsePluginMcpServers expands ${env:*} in stdio command before transport detection + --- + duration_ms: 0.041959 + type: 'test' + ... +# Subtest: parsePluginMcpServers drops empty expanded stdio command +ok 162 - parsePluginMcpServers drops empty expanded stdio command + --- + duration_ms: 0.185625 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${userHome} in stdio cwd +ok 163 - parsePluginMcpServers expands ${userHome} in stdio cwd + --- + duration_ms: 0.228 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ~ in stdio args (backward compat) +ok 164 - parsePluginMcpServers expands ~ in stdio args (backward compat) + --- + duration_ms: 0.10975 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http url +ok 165 - parsePluginMcpServers expands ${env:*} in streamable_http url + --- + duration_ms: 0.041625 + type: 'test' + ... +# Subtest: parsePluginMcpServers expands ${env:*} in streamable_http headers +ok 166 - parsePluginMcpServers expands ${env:*} in streamable_http headers + --- + duration_ms: 0.037834 + type: 'test' + ... +# Subtest: user config and plugin config resolve same placeholders consistently +ok 167 - user config and plugin config resolve same placeholders consistently + --- + duration_ms: 0.036708 + type: 'test' + ... +# Subtest: Anthropic-compatible terminated SSE errors remain retryable +ok 168 - Anthropic-compatible terminated SSE errors remain retryable + --- + duration_ms: 3.698541 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is omitted +ok 169 - catalog provider resolves api key from default env var when apiKey is omitted + --- + duration_ms: 1.688041 + type: 'test' + ... +# Subtest: catalog provider resolves api key from default env var when apiKey is blank +ok 170 - catalog provider resolves api key from default env var when apiKey is blank + --- + duration_ms: 0.180542 + type: 'test' + ... +# Subtest: normalizeModelError classifies common network failures +ok 171 - normalizeModelError classifies common network failures + --- + duration_ms: 2.625 + type: 'test' + ... +# Subtest: model request failure preserves provider raw message before action guidance +ok 172 - model request failure preserves provider raw message before action guidance + --- + duration_ms: 3.382333 + type: 'test' + ... +# Subtest: model_not_found guidance points users to local model settings +ok 173 - model_not_found guidance points users to local model settings + --- + duration_ms: 0.848792 + type: 'test' + ... +# Subtest: stream idle timeout is classified as timeout with network and timeoutMs guidance +ok 174 - stream idle timeout is classified as timeout with network and timeoutMs guidance + --- + duration_ms: 0.2815 + type: 'test' + ... +# Subtest: provider terminated stream is classified as a retryable connection reset +ok 175 - provider terminated stream is classified as a retryable connection reset + --- + duration_ms: 0.194834 + type: 'test' + ... +# Subtest: billing and rate limit guidance distinguish provider-side fixes +ok 176 - billing and rate limit guidance distinguish provider-side fixes + --- + duration_ms: 0.08775 + type: 'test' + ... +# Subtest: unknown provider errors still give actionable settings and provider checks +ok 177 - unknown provider errors still give actionable settings and provider checks + --- + duration_ms: 0.043458 + type: 'test' + ... +# Subtest: OpenAI Responses usage reads cached tokens from input token details +ok 178 - OpenAI Responses usage reads cached tokens from input token details + --- + duration_ms: 0.528625 + type: 'test' + ... +# Subtest: Gemini usage counts thoughts tokens as output consumption +ok 179 - Gemini usage counts thoughts tokens as output consumption + --- + duration_ms: 0.080084 + type: 'test' + ... +# Subtest: Ollama provider uses catalog defaults and does not require apiKey +ok 180 - Ollama provider uses catalog defaults and does not require apiKey + --- + duration_ms: 5.071583 + type: 'test' + ... +# Subtest: Ollama provider builds OpenAI-compatible chat completions body +ok 181 - Ollama provider builds OpenAI-compatible chat completions body + --- + duration_ms: 1.853125 + type: 'test' + ... +# Subtest: model request builders tolerate malformed messages with missing content +ok 182 - model request builders tolerate malformed messages with missing content + --- + duration_ms: 0.907541 + type: 'test' + ... +# Subtest: cloneMessages normalizes malformed messages with missing content +ok 183 - cloneMessages normalizes malformed messages with missing content + --- + duration_ms: 0.495125 + type: 'test' + ... +# Subtest: streamModel retries Anthropic-compatible terminated SSE errors +ok 184 - streamModel retries Anthropic-compatible terminated SSE errors + --- + duration_ms: 10.626041 + type: 'test' + ... +# Subtest: networkFetch retries retryable status responses and then succeeds +ok 185 - networkFetch retries retryable status responses and then succeeds + --- + duration_ms: 3.204541 + type: 'test' + ... +# Subtest: networkFetch uses retry-after when calculating retry delay +ok 186 - networkFetch uses retry-after when calculating retry delay + --- + duration_ms: 0.068875 + type: 'test' + ... +# Subtest: networkFetch caps retry-after delays with maxDelayMs +ok 187 - networkFetch caps retry-after delays with maxDelayMs + --- + duration_ms: 0.034458 + type: 'test' + ... +# Subtest: networkFetch normalizes DNS and reset errors +ok 188 - networkFetch normalizes DNS and reset errors + --- + duration_ms: 0.088375 + type: 'test' + ... +# Subtest: networkFetch times out requests +ok 189 - networkFetch times out requests + --- + duration_ms: 6.181916 + type: 'test' + ... +# Subtest: networkFetch honors init.signal abort reasons without options.signal +ok 190 - networkFetch honors init.signal abort reasons without options.signal + --- + duration_ms: 0.833167 + type: 'test' + ... +# Subtest: networkFetch preserves parent NetworkFetchError reasons passed through options.signal +ok 191 - networkFetch preserves parent NetworkFetchError reasons passed through options.signal + --- + duration_ms: 0.469583 + type: 'test' + ... +# (node:73293) 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 192 - local gateway writes a complete shadow bundle linked to the final request + --- + duration_ms: 704.889542 + 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 193 - enabling O1 does not change Agent-visible model input + --- + duration_ms: 524.624375 + type: 'test' + ... +# (node:73296) 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 194 - local Gateway records a bounded handoff through boundary to progress + --- + duration_ms: 1215.979167 + type: 'test' + ... +# (node:73297) 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 195 - local Gateway records a complete O1 trajectory when a bounded child times out + --- + duration_ms: 1566.945333 + type: 'test' + ... +# Subtest: subagent tool detection and result pair at the model projection layer +ok 196 - subagent tool detection and result pair at the model projection layer + --- + duration_ms: 6.779333 + type: 'test' + ... +# Subtest: main tool detection and result produce one exact O1 pair +ok 197 - main tool detection and result produce one exact O1 pair + --- + duration_ms: 0.612583 + type: 'test' + ... +# Subtest: repair preparation grace is observable without becoming progress +ok 198 - repair preparation grace is observable without becoming progress + --- + duration_ms: 0.073458 + type: 'test' + ... +# Subtest: recorder finalizes a complete hash-only trajectory +ok 199 - recorder finalizes a complete hash-only trajectory + --- + duration_ms: 21.573625 + type: 'test' + ... +# Subtest: queue overflow is explicit and makes integrity partial +ok 200 - queue overflow is explicit and makes integrity partial + --- + duration_ms: 9.693834 + type: 'test' + ... +# Subtest: verifier rejects reused model request identity and duplicate terminals +ok 201 - verifier rejects reused model request identity and duplicate terminals + --- + duration_ms: 24.195875 + type: 'test' + ... +# Subtest: verifier rejects duplicate identity and secret-bearing keys +ok 202 - verifier rejects duplicate identity and secret-bearing keys + --- + duration_ms: 0.289458 + type: 'test' + ... +# [PilotDeck] transientRetry: rate_limit (attempt 1/1, delay=1ms) +# Subtest: router observation closes every fallback and retry attempt +ok 203 - router observation closes every fallback and retry attempt + --- + duration_ms: 26.919125 + type: 'test' + ... +# Subtest: router request identity separates sessions sharing a turn id +ok 204 - router request identity separates sessions sharing a turn id + --- + duration_ms: 7.37275 + type: 'test' + ... +# Subtest: router request identity advances across sequential calls in one turn +ok 205 - router request identity advances across sequential calls in one turn + --- + duration_ms: 7.386833 + type: 'test' + ... +# Subtest: observability is disabled by default and preserves the O1 diagnostic profile +ok 206 - observability is disabled by default and preserves the O1 diagnostic profile + --- + duration_ms: 29.400125 + type: 'test' + ... +# Subtest: O1 rejects unsupported profiles and unsafe labels +ok 207 - O1 rejects unsupported profiles and unsafe labels + --- + duration_ms: 11.227083 + type: 'test' + ... +# Subtest: web search can be explicitly disabled without discarding provider config +ok 208 - web search can be explicitly disabled without discarding provider config + --- + duration_ms: 0.979084 + type: 'test' + ... +# Subtest: web search enabled remains optional for backwards compatibility +ok 209 - web search enabled remains optional for backwards compatibility + --- + duration_ms: 0.048875 + type: 'test' + ... +# Subtest: web search enabled must be a boolean +ok 210 - web search enabled must be a boolean + --- + duration_ms: 0.047292 + type: 'test' + ... +# Subtest: progress lease is disabled by default and opt-in evaluation config is preserved +ok 211 - progress lease is disabled by default and opt-in evaluation config is preserved + --- + duration_ms: 17.751917 + type: 'test' + ... +# Subtest: progress lease preserves an explicit cold-start allowance +ok 212 - progress lease preserves an explicit cold-start allowance + --- + duration_ms: 2.800291 + type: 'test' + ... +# Subtest: progress lease rejects non-evaluation modes +ok 213 - progress lease rejects non-evaluation modes + --- + duration_ms: 2.580875 + type: 'test' + ... +# Subtest: progress lease rejects a cold-start allowance below the steady-state limit +ok 214 - progress lease rejects a cold-start allowance below the steady-state limit + --- + duration_ms: 2.350166 + type: 'test' + ... +# Subtest: legal coverage validator creates a current proof and removes it when the deliverable changes +ok 215 - legal coverage validator creates a current proof and removes it when the deliverable changes + --- + duration_ms: 276.061041 + type: 'test' + ... +# Subtest: legal coverage initializer creates a text skeleton before source review and remains idempotent +ok 216 - legal coverage initializer creates a text skeleton before source review and remains idempotent + --- + duration_ms: 394.074208 + type: 'test' + ... +# Subtest: legal coverage initializer binds trusted manifests to original inputs +ok 217 - legal coverage initializer binds trusted manifests to original inputs + --- + duration_ms: 442.970167 + type: 'test' + ... +# Subtest: legal coverage source bootstrap creates only deterministic pending manifest rows +ok 218 - legal coverage source bootstrap creates only deterministic pending manifest rows + --- + duration_ms: 445.939666 + type: 'test' + ... +# Subtest: legal coverage injects deterministic disjoint worker batches for large pending source rooms +ok 219 - legal coverage injects deterministic disjoint worker batches for large pending source rooms + --- + duration_ms: 5081.6445 + type: 'test' + ... +# Subtest: legal coverage CLI exposes bundled guidance through stable named references +ok 220 - legal coverage CLI exposes bundled guidance through stable named references + --- + duration_ms: 134.579291 + type: 'test' + ... +# Subtest: legal coverage initializer creates only explicit text formats and preserves existing content +ok 221 - legal coverage initializer creates only explicit text formats and preserves existing content + --- + duration_ms: 54.337333 + type: 'test' + ... +# Subtest: legal coverage initializer rejects traversal and symlink ancestors without external writes +ok 222 - legal coverage initializer rejects traversal and symlink ancestors without external writes + --- + duration_ms: 146.747042 + type: 'test' + ... +# Subtest: legal coverage validator binds runner originals and derivations into its proof +ok 223 - legal coverage validator binds runner originals and derivations into its proof + --- + duration_ms: 213.384791 + type: 'test' + ... +# Subtest: legal coverage validator rejects missing lineage and mutable or derived input roots +ok 224 - legal coverage validator rejects missing lineage and mutable or derived input roots + --- + duration_ms: 268.766167 + type: 'test' + ... +# Subtest: legal coverage next-batch exposes one bounded deterministic repair slice +ok 225 - legal coverage next-batch exposes one bounded deterministic repair slice + --- + duration_ms: 210.35875 + type: 'test' + ... +# Subtest: legal coverage apply-batch atomically updates only the current bounded slice +ok 226 - legal coverage apply-batch atomically updates only the current bounded slice + --- + duration_ms: 314.956958 + type: 'test' + ... +# Subtest: legal coverage apply-batch rejects record and byte limit violations before writing +ok 227 - legal coverage apply-batch rejects record and byte limit violations before writing + --- + duration_ms: 202.725042 + type: 'test' + ... +# Subtest: legal coverage validator rejects orphaned conflicts and incomplete final disclosure +ok 228 - legal coverage validator rejects orphaned conflicts and incomplete final disclosure + --- + duration_ms: 104.348042 + type: 'test' + ... +# Subtest: legal coverage validator detects un-inventoried sources and cross-fact timeline collisions +ok 229 - legal coverage validator detects un-inventoried sources and cross-fact timeline collisions + --- + duration_ms: 110.993166 + type: 'test' + ... +# Subtest: legal coverage validator rejects reused generic quotes and unsupported fact coverage +ok 230 - legal coverage validator rejects reused generic quotes and unsupported fact coverage + --- + duration_ms: 105.42775 + type: 'test' + ... +# Subtest: legal coverage validator requires authority links for critical issues and legal-authority matrices +ok 231 - legal coverage validator requires authority links for critical issues and legal-authority matrices + --- + duration_ms: 105.165917 + type: 'test' + ... +# Subtest: legal coverage validator rejects non-object canonical JSON documents +ok 232 - legal coverage validator rejects non-object canonical JSON documents + --- + duration_ms: 726.594667 + type: 'test' + ... +# Subtest: legal coverage validator binds reviewed source bytes to the ledger and state hash +ok 233 - legal coverage validator binds reviewed source bytes to the ledger and state hash + --- + duration_ms: 207.479042 + type: 'test' + ... +# Subtest: legal coverage validator rejects ancestor symlinks and a symlinked proof +ok 234 - legal coverage validator rejects ancestor symlinks and a symlinked proof + --- + duration_ms: 154.057583 + type: 'test' + ... +# Subtest: legal coverage validator does not read or write through a symlinked state ancestor +ok 235 - legal coverage validator does not read or write through a symlinked state ancestor + --- + duration_ms: 99.307125 + type: 'test' + ... +# Subtest: legal coverage validator requires unresolved disclosure for unverified material facts +ok 236 - legal coverage validator requires unresolved disclosure for unverified material facts + --- + duration_ms: 103.509292 + type: 'test' + ... +# Subtest: legal coverage validator rejects unique but irrelevant issue and authority quotes +ok 237 - legal coverage validator rejects unique but irrelevant issue and authority quotes + --- + duration_ms: 104.861875 + type: 'test' + ... +# Subtest: legal coverage validator enforces reciprocal and same-entry ledger relationships +ok 238 - legal coverage validator enforces reciprocal and same-entry ledger relationships + --- + duration_ms: 106.681459 + type: 'test' + ... +# Subtest: legal coverage validator accepts null for an optional threshold assessment +ok 239 - legal coverage validator accepts null for an optional threshold assessment + --- + duration_ms: 109.070667 + type: 'test' + ... +# Subtest: legal coverage validator uses locators without quotes for binary deliverables +ok 240 - legal coverage validator uses locators without quotes for binary deliverables + --- + duration_ms: 111.433875 + type: 'test' + ... +# Subtest: legal coverage validator rejects empty-fact shortcuts and material facts outside matrices +ok 241 - legal coverage validator rejects empty-fact shortcuts and material facts outside matrices + --- + duration_ms: 209.958792 + type: 'test' + ... +# Subtest: legal coverage hook activates only legal work and injects one observable milestone +ok 242 - legal coverage hook activates only legal work and injects one observable milestone + --- + duration_ms: 557.204667 + type: 'test' + ... +# Subtest: legal coverage hook groups repeated validator errors into one bounded milestone +ok 243 - legal coverage hook groups repeated validator errors into one bounded milestone + --- + duration_ms: 551.654167 + type: 'test' + ... +# Subtest: legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress +ok 244 - legal coverage pending-matrix selection is bounded across pages and invalid revisions cannot manufacture progress + --- + duration_ms: 1027.091084 + type: 'test' + ... +# Subtest: legal coverage progress ordinal advances once for a new legal phase +ok 245 - legal coverage progress ordinal advances once for a new legal phase + --- + duration_ms: 223.003333 + type: 'test' + ... +# Subtest: legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals +ok 246 - legal coverage permits not-applicable only after exhaustive selection and rejects stale or changed matrix proposals + --- + duration_ms: 495.975625 + type: 'test' + ... +# Subtest: legal coverage rejects oversized matrix evidence before making a selection receipt immutable +ok 247 - legal coverage rejects oversized matrix evidence before making a selection receipt immutable + --- + duration_ms: 164.021083 + type: 'test' + ... +# Subtest: legal coverage fails closed when one matrix index item exceeds the bounded page +ok 248 - legal coverage fails closed when one matrix index item exceeds the bounded page + --- + duration_ms: 161.864583 + type: 'test' + ... +# Subtest: legal coverage injects a bounded deterministic matrix relation-closure batch +ok 249 - legal coverage injects a bounded deterministic matrix relation-closure batch + --- + duration_ms: 219.787709 + type: 'test' + ... +# Subtest: legal coverage authority closure uses one bounded state-bound transaction +ok 250 - legal coverage authority closure uses one bounded state-bound transaction + --- + duration_ms: 608.652667 + type: 'test' + ... +# Subtest: legal coverage risk-signal issue closure exposes one bounded state-bound transaction +ok 251 - legal coverage risk-signal issue closure exposes one bounded state-bound transaction + --- + duration_ms: 609.176666 + type: 'test' + ... +# Subtest: legal coverage rejects changed and symlinked issue closure proposals without ledger mutation +ok 252 - legal coverage rejects changed and symlinked issue closure proposals without ledger mutation + --- + duration_ms: 316.971708 + type: 'test' + ... +# Subtest: legal coverage rejects changed and symlinked authority closure proposals without ledger mutation +ok 253 - legal coverage rejects changed and symlinked authority closure proposals without ledger mutation + --- + duration_ms: 265.864458 + type: 'test' + ... +# Subtest: legal coverage milestone digest ignores opaque incomplete-state hash churn +ok 254 - legal coverage milestone digest ignores opaque incomplete-state hash churn + --- + duration_ms: 0.246167 + type: 'test' + ... +# Subtest: legal coverage hook activates a malformed configured workspace so the agent can repair it +ok 255 - legal coverage hook activates a malformed configured workspace so the agent can repair it + --- + duration_ms: 104.614291 + type: 'test' + ... +# Subtest: legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace +ok 256 - legal coverage hook rejects a symlinked session-state ancestor without writing outside the workspace + --- + duration_ms: 51.839667 + type: 'test' + ... +# Subtest: legal product plugin loads one skill and contains no benchmark-specific controls +ok 257 - legal product plugin loads one skill and contains no benchmark-specific controls + --- + duration_ms: 3.514959 + type: 'test' + ... +# Subtest: file artifacts include every meaningful workspace change without an extension allowlist +ok 258 - file artifacts include every meaningful workspace change without an extension allowlist + --- + duration_ms: 36.577958 + type: 'test' + ... +# Subtest: file artifact fingerprints are reused for unchanged files across scans and turns +ok 259 - file artifact fingerprints are reused for unchanged files across scans and turns + --- + duration_ms: 4.861292 + type: 'test' + ... +# Subtest: TurnRunner emits and persists file artifacts before completing the turn +ok 260 - TurnRunner emits and persists file artifacts before completing the turn + --- + duration_ms: 21.301417 + type: 'test' + ... +# Subtest: TurnRunner does not collect generated files when artifacts are disabled +ok 261 - TurnRunner does not collect generated files when artifacts are disabled + --- + duration_ms: 2.689375 + type: 'test' + ... +# Subtest: bash success result is formatted with assertions, stdout, and stderr +ok 262 - bash success result is formatted with assertions, stdout, and stderr + --- + duration_ms: 3.646875 + type: 'test' + ... +# Subtest: bash allows hyphenated commands whose names merely start with next +ok 263 - bash allows hyphenated commands whose names merely start with next + --- + duration_ms: 1.894209 + type: 'test' + ... +# Subtest: bash still rejects exact framework CLI tokens that start long-lived processes +ok 264 - bash still rejects exact framework CLI tokens that start long-lived processes + --- + duration_ms: 1.012208 + type: 'test' + ... +# Subtest: bash failure tool result includes raw stdout and stderr tail for UI and model +ok 265 - bash failure tool result includes raw stdout and stderr tail for UI and model + --- + duration_ms: 22.649416 + type: 'test' + ... +# Subtest: bash failure diagnostic extracts Python traceback location and exception +ok 266 - bash failure diagnostic extracts Python traceback location and exception + --- + duration_ms: 1.329208 + type: 'test' + ... +# Subtest: bash success keeps full output for ToolResultBudget persistence +ok 267 - bash success keeps full output for ToolResultBudget persistence + --- + duration_ms: 9.335834 + type: 'test' + ... +# Subtest: agent tool accepts explorer as an alias for explore +ok 268 - agent tool accepts explorer as an alias for explore + --- + duration_ms: 2.924625 + type: 'test' + ... +# Subtest: agent tool defaults general-purpose to explore in ask mode +ok 269 - agent tool defaults general-purpose to explore in ask mode + --- + duration_ms: 0.21725 + type: 'test' + ... +# Subtest: agent tool injects and forwards the parent-bounded timeout +ok 270 - agent tool injects and forwards the parent-bounded timeout + --- + duration_ms: 0.340208 + type: 'test' + ... +# Subtest: agent tool rejects a launch that would consume the parent handoff window +ok 271 - agent tool rejects a launch that would consume the parent handoff window + --- + duration_ms: 0.487167 + type: 'test' + ... +# Subtest: agent fallback path enforces the same timeout +ok 272 - agent fallback path enforces the same timeout + --- + duration_ms: 5.583333 + type: 'test' + ... +# Subtest: agent tool preserves unknown custom fallback subagent names +ok 273 - agent tool preserves unknown custom fallback subagent names + --- + duration_ms: 0.488958 + type: 'test' + ... +# Subtest: execute_code read-only probe handles missing input +ok 274 - execute_code read-only probe handles missing input + --- + duration_ms: 0.979083 + type: 'test' + ... +# Subtest: disabling web search removes it from the registry but keeps web fetch +ok 275 - disabling web search removes it from the registry but keeps web fetch + --- + duration_ms: 1.447667 + type: 'test' + ... +# Subtest: execute_code rejects nested web search calls when web search is disabled +ok 276 - execute_code rejects nested web search calls when web search is disabled + --- + duration_ms: 0.137458 + type: 'test' + ... +# Subtest: read_skill returns the resolved SKILL.md path with the skill body +ok 277 - read_skill returns the resolved SKILL.md path with the skill body + --- + duration_ms: 0.573041 + type: 'test' + ... +# Subtest: read_skill preserves legacy content-only loading when metadata is unavailable +ok 278 - read_skill preserves legacy content-only loading when metadata is unavailable + --- + duration_ms: 0.063417 + type: 'test' + ... +# Subtest: web_search retries transient provider failures +ok 279 - web_search retries transient provider failures + --- + duration_ms: 514.472167 + type: 'test' + ... +# Subtest: web_search turns request timeout into tool_timeout +ok 280 - web_search turns request timeout into tool_timeout + --- + duration_ms: 3.122875 + type: 'test' + ... +# Subtest: web_search turns network timeout errors into tool_timeout +ok 281 - web_search turns network timeout errors into tool_timeout + --- + duration_ms: 2.243459 + type: 'test' + ... +# Subtest: write_file freshness error tells the model to read the file first +ok 282 - write_file freshness error tells the model to read the file first + --- + duration_ms: 0.954208 + type: 'test' + ... +# Subtest: write_file missing content points at the missing field and chunked recovery +ok 283 - write_file missing content points at the missing field and chunked recovery + --- + duration_ms: 0.247917 + type: 'test' + ... +# Subtest: invalid tool input keeps a bounded original error block +ok 284 - invalid tool input keeps a bounded original error block + --- + duration_ms: 0.206583 + type: 'test' + ... +# Subtest: edit_file old_string miss tells the model to reread and copy exact text +ok 285 - edit_file old_string miss tells the model to reread and copy exact text + --- + duration_ms: 0.06125 + type: 'test' + ... +# Subtest: read_file invalid range preserves the concrete issue +ok 286 - read_file invalid range preserves the concrete issue + --- + duration_ms: 0.105542 + type: 'test' + ... +# Subtest: bash missing command reports the required command parameter +ok 287 - bash missing command reports the required command parameter + --- + duration_ms: 0.098083 + type: 'test' + ... +# Subtest: bash timeout over max keeps foreground and task_wait guidance +ok 288 - bash timeout over max keeps foreground and task_wait guidance + --- + duration_ms: 0.129625 + type: 'test' + ... +# Subtest: bash background rejection keeps background-specific guidance +ok 289 - bash background rejection keeps background-specific guidance + --- + duration_ms: 0.076875 + type: 'test' + ... +# Subtest: write_file can create a new file without a prior read_file call +ok 290 - write_file can create a new file without a prior read_file call + --- + duration_ms: 13.832291 + type: 'test' + ... +# Subtest: edit_file can create a new file with empty old_string without a prior read_file call +ok 291 - edit_file can create a new file with empty old_string without a prior read_file call + --- + duration_ms: 6.469 + type: 'test' + ... +# Subtest: read_file auto-pages large text files instead of failing +ok 292 - read_file auto-pages large text files instead of failing + --- + duration_ms: 2293.313375 + type: 'test' + ... +# Subtest: read_file rejects Office container files during validation +ok 293 - read_file rejects Office container files during validation + --- + duration_ms: 1.020208 + type: 'test' + ... +# Subtest: read_file explicit limit reads a large file range without auto paging +ok 294 - read_file explicit limit reads a large file range without auto paging + --- + duration_ms: 4.027958 + type: 'test' + ... +# Subtest: read_file auto-shrinks oversized persisted tool-result ref ranges +ok 295 - read_file auto-shrinks oversized persisted tool-result ref ranges + --- + duration_ms: 22553.803125 + type: 'test' + ... +# Subtest: read_file keeps explicit oversized ordinary file ranges strict +ok 296 - read_file keeps explicit oversized ordinary file ranges strict + --- + duration_ms: 2.611417 + type: 'test' + ... +# Subtest: read_file explicit limit records a ranged snapshot for follow-up edits +ok 297 - read_file explicit limit records a ranged snapshot for follow-up edits + --- + duration_ms: 2.030083 + type: 'test' + ... +# Subtest: read_file auto-paged large files record a ranged snapshot for follow-up edits +ok 298 - read_file auto-paged large files record a ranged snapshot for follow-up edits + --- + duration_ms: 1550.274166 + type: 'test' + ... +# Subtest: read_file returns a head-tail preview for a single oversized line +ok 299 - read_file returns a head-tail preview for a single oversized line + --- + duration_ms: 2.378125 + type: 'test' + ... +# Subtest: read_file classifies common Office formats as binary +ok 300 - read_file classifies common Office formats as binary + --- + duration_ms: 0.21 + type: 'test' + ... +# Subtest: read_file rejects ranged binary input instead of hanging +ok 301 - read_file rejects ranged binary input instead of hanging + --- + duration_ms: 1.041541 + type: 'test' + ... +# Subtest: read_file range honors an aborted signal +ok 302 - read_file range honors an aborted signal + --- + duration_ms: 0.69525 + type: 'test' + ... +# Subtest: glob result text tells the model when more files are available +ok 303 - glob result text tells the model when more files are available + --- + duration_ms: 36.317917 + type: 'test' + ... +# Subtest: grep result text includes next offset when paginated +ok 304 - grep result text includes next offset when paginated + --- + duration_ms: 16.238291 + type: 'test' + ... +# Subtest: todo_write returns the actual todo list in model-visible content +ok 305 - todo_write returns the actual todo list in model-visible content + --- + duration_ms: 1.414833 + type: 'test' + ... +# Subtest: task_list returns model-visible status and next action hints +ok 306 - task_list returns model-visible status and next action hints + --- + duration_ms: 1.029875 + type: 'test' + ... +# Subtest: applyResultSizeLimit keeps both head and tail when truncating text output +ok 307 - applyResultSizeLimit keeps both head and tail when truncating text output + --- + duration_ms: 1.096792 + type: 'test' + ... +# Subtest: large tool results are persisted under workspace .pilotdeck and readable by read_file +ok 308 - large tool results are persisted under workspace .pilotdeck and readable by read_file + --- + duration_ms: 15.08775 + type: 'test' + ... +# Subtest: large tool result read_file aliases are short and sequential +ok 309 - large tool result read_file aliases are short and sequential + --- + duration_ms: 7.397666 + type: 'test' + ... +# Subtest: history replay restores structured agent file artifacts +ok 310 - history replay restores structured agent file artifacts + --- + duration_ms: 15.89175 + type: 'test' + ... +# Subtest: history replay hides Agent file artifacts in general conversations +ok 311 - history replay hides Agent file artifacts in general conversations + --- + duration_ms: 4.147167 + type: 'test' + ... +# Subtest: history replay preserves agent status i18n metadata and user hint +ok 312 - history replay preserves agent status i18n metadata and user hint + --- + duration_ms: 8.195375 + type: 'test' + ... +# Subtest: history token usage restores latest non-empty turn past latest empty turn result +ok 313 - history token usage restores latest non-empty turn past latest empty turn result + --- + duration_ms: 4.023667 + type: 'test' + ... +# Subtest: history token usage prefers persisted context budget snapshot +ok 314 - history token usage prefers persisted context budget snapshot + --- + duration_ms: 3.365417 + type: 'test' + ... +# Subtest: web reducer merges persisted tool result detail path into existing tool result +ok 315 - web reducer merges persisted tool result detail path into existing tool result + --- + duration_ms: 1.448167 + type: 'test' + ... +# Subtest: web reducer bounds huge live tool result previews +ok 316 - web reducer bounds huge live tool result previews + --- + duration_ms: 0.202042 + type: 'test' + ... +1..316 +# tests 316 +# suites 0 +# pass 316 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 28949.214041 diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/real-gateway.log b/.omo/evidence/20260727-v24r14-issue-closure-transaction/real-gateway.log new file mode 100644 index 00000000..701321cd --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/real-gateway.log @@ -0,0 +1,24 @@ +TAP version 13 +# (node:73938) 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 executes the state-bound legal authority closure with complete O1 evidence +ok 1 - real gateway executes the state-bound legal authority closure with complete O1 evidence + --- + duration_ms: 810.123041 + type: 'test' + ... +# Subtest: real gateway executes the state-bound legal issue closure with complete O1 evidence +ok 2 - real gateway executes the state-bound legal issue closure with complete O1 evidence + --- + duration_ms: 507.504375 + type: 'test' + ... +1..2 +# tests 2 +# suites 0 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 1712.197708 From 6c8735c50cf90d4e32eee2448cbc87e1c7bc0f84 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 13:37:50 +0800 Subject: [PATCH 6/6] test(legal): summarize V24R14 QA --- .../QA.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .omo/evidence/20260727-v24r14-issue-closure-transaction/QA.md diff --git a/.omo/evidence/20260727-v24r14-issue-closure-transaction/QA.md b/.omo/evidence/20260727-v24r14-issue-closure-transaction/QA.md new file mode 100644 index 00000000..089a20b2 --- /dev/null +++ b/.omo/evidence/20260727-v24r14-issue-closure-transaction/QA.md @@ -0,0 +1,82 @@ +# V24R14 issue closure transaction QA + +## What was tested + +1. A sanitized `risk_signal_orphaned` counterexample exercised proposal + preparation, rejection, validation, apply, replay, stale-state, byte-change, + criticality-downgrade, path, and symlink controls. +2. A real local Gateway loaded the installed Legal Coverage plugin, received + the dynamic issue proposal and apply milestones, executed both through real + `bash` tool calls, enforced Progress Lease `8/2`, and recorded O1 diagnostic + evidence with Router and Memory disabled. +3. The preserved V24R13 Case 09 workspace was copied to a temporary directory + and passed through the current `PreModelRequest` hook process. The probe + emitted only counts, flags, and booleans and compared canonical hashes before + and after. +4. Focused Progress Lease, context compaction, dynamic context, domain plugin, + Gateway handoff, O1, and machine-deadline suites were run together. +5. The complete repository build and test suite plus `git diff --check` were + run against the unchanged locked dependency tree. + +## What was observed + +- Red criticality counterexample: the pre-fix template returned `false` for a + target containing critical facts; the new assertion failed as intended. +- Focused issue transaction tests: 2 passed, 0 failed. +- Real local Gateway issue/authority comparison: 2 passed, 0 failed. The issue + path observed `baseline(0,0) -> handoff_grace(0,1) -> completed(1,1)`, two + paired tool calls, a current completion proof, and zero O1 recorder drops. +- Cross-layer controls: 46 passed, 0 failed. +- Preserved Case 09 projection: `risk_signal_orphaned` produced one bounded + `issue-closure-propose` item with 5 facts, 4 critical facts, 1 allowed rule, + no premature apply command, and unchanged frozen/copy canonical ledgers. +- Full repository: 316 passed, 0 failed after a clean build. +- Patch hygiene: `git diff --check` passed; `pnpm-lock.yaml`, Core compaction, + Progress Lease, validator rules, Router, Memory, and runner files are + unchanged. + +Artifacts: + +- `focused-issue-closure.log` +- `real-gateway.log` +- `cross-layer-focused.log` +- `preserved-state-probe.json` +- `full-test.log` +- `diff-check.log` + +## Why it is enough for the code gate + +The state-bound tests cover all mutation inputs and prove that invalid or stale +revisions cannot mutate canonical ledgers or manufacture progress. The real +Gateway test proves the dynamic prompt, handoff ordinal, tool execution, +validator, completion proof, and O1 surfaces compose end to end. The preserved +state probe proves the exact V24R13 failure shape now receives the new bounded +interface without changing the frozen campaign. The full suite covers +unrelated product regressions. + +## Boundary preserved + +- Production changes are limited to the Legal Coverage plugin CLI, hook, + transaction library, and its domain Skill. +- The transaction handles only the first `issues/risk_signal_orphaned` entry, + at most 12 facts, one issue per known risk signal, and 24 KiB. +- Criticality is derived from the complete fact slice and cannot be downgraded + to avoid the unchanged authority requirement. +- Core, Lease `8/2`, validator rules, model, corpus, Skills snapshot, runner, + deadlines, Router, Memory, and O1 implementation are unchanged. + +## Remaining product risk + +Deterministic QA cannot prove that the real model will produce correct legal +analysis, finish all six remaining matrices, close authority and coverage +relationships, create a substantive report, or finish Case 09 within 2,100 +seconds. Those claims require a new immutable V24R14 Gateway/O1 campaign. +V25 and the 85-case campaign remain unauthorized until the Case 09 product +Gate passes in full. + +## What was omitted + +No API key, token, auth header, environment dump, private source text, prompt +body, legal report text, or model reasoning is stored in this evidence. The +preserved-state probe reads a temporary local copy and records only aggregate +metrics and booleans.