diff --git a/.adv/specs/advance-workflow/spec.json b/.adv/specs/advance-workflow/spec.json index 8d497092..9cb480a6 100644 --- a/.adv/specs/advance-workflow/spec.json +++ b/.adv/specs/advance-workflow/spec.json @@ -5679,6 +5679,71 @@ } ] }, + { + "id": "rq-reviewerEvidenceAuthority01", + "title": "Reviewer Evidence Authority Partitioned by Evidence Policy", + "body": "The stage-v2 evidence authority MUST partition verification requirements by evidence policy. For a completed task with evidence_policy \"review\", a persisted same-task adv-reviewer report IS the authoritative completion evidence; its aggregate tests_run command list neither creates nor substitutes for durable adv_run_test execution evidence, and the consumer_warning emitter MUST NOT emit verification_missing for it. For evidence_policy \"test\" or \"static_check\", reviewer aggregate command evidence MUST NOT satisfy the verification requirement; durable adv_run_test execution evidence remains required.", + "priority": "must", + "tags": [ + "workflow", + "verification", + "evidence", + "structural", + "review" + ], + "scenarios": [ + { + "id": "rq-reviewerEvidenceAuthority01.1", + "title": "Review-policy task with linked reviewer report — no verification block", + "given": [ + "A completed stage-v2 task has evidence_policy \"review\"", + "A persisted same-task adv-reviewer report is linked as review_evidence_ref" + ], + "when": "Acceptance or release readiness evaluates verification evidence", + "then": [ + "No VERIFICATION_EVIDENCE_MISSING blocker is emitted for the reviewer's tests_run" + ], + "warrant": "AC1" + }, + { + "id": "rq-reviewerEvidenceAuthority01.2", + "title": "Test/static_check-policy task with only reviewer evidence — block remains", + "given": [ + "A completed task has evidence_policy \"test\" or \"static_check\"", + "A persisted same-task adv-reviewer report carries aggregate tests_run", + "No durable adv_run_test run exists" + ], + "when": "Acceptance or release readiness evaluates verification evidence", + "then": [ + "A VERIFICATION_EVIDENCE_MISSING blocker is emitted" + ], + "warrant": "AC2" + }, + { + "id": "rq-reviewerEvidenceAuthority01.3", + "title": "Emitter policy-gates verification_missing for adv-reviewer reports", + "given": [ + "A task has evidence_policy \"review\"" + ], + "when": "An adv-reviewer report is submitted with aggregate tests_run", + "then": [ + "The consumer_warning emitter produces no verification_missing warnings" + ], + "warrant": "AC5" + }, + { + "id": "rq-reviewerEvidenceAuthority01.4", + "title": "Same-task ownership preserved — change-scoped reviewer report cannot satisfy task review_evidence_ref", + "given": [ + "A change-scoped adv-reviewer report has no task_id anchor" + ], + "when": "Task completion validation runs", + "then": [ + "The report cannot satisfy the task's review_evidence_ref" + ] + } + ] + }, { "id": "rq-reviewBadTestCleanup01", "title": "Reviewer-Owned Safe Local Cleanup of Evidence-Backed Bad Tests", diff --git a/docs/cli-surface-matrix.md b/docs/cli-surface-matrix.md index df0d1675..c5003f7d 100644 --- a/docs/cli-surface-matrix.md +++ b/docs/cli-surface-matrix.md @@ -149,6 +149,8 @@ | `adv_epic_reorder` | `no-cli-dangerous` | Epic mutation | | `adv_epic_retire` | `no-cli-dangerous` | Epic retirement mutation | | `adv_launcher_projection_rebuild` | `keep-mcp-only` | Producer-only aggregate launcher-projection rebuild (drift recovery); plugin/MCP-only, never bin/adv | +| `adv_change_set_worker_bundle_impact` | `keep-mcp-only` | Workflow-bound planning declaration of worker-bundle impact classification; agent/orchestrator use only | +| `adv_worker_bundle_provenance_record` | `keep-mcp-only` | Execution-time worker-bundle build+replay provenance receipt; agent/orchestrator use only | ## Deferred diff --git a/docs/tool-ownership.md b/docs/tool-ownership.md index 999d7e7d..856a6c31 100644 --- a/docs/tool-ownership.md +++ b/docs/tool-ownership.md @@ -104,6 +104,7 @@ class rather than operator-only. | `adv_change_archive` | orchestrator | Release-gate archive workflow | | `adv_change_update_issues` | orchestrator | Issue linkage update | | `adv_change_reenter` | orchestrator | Gate re-entry | +| `adv_change_set_worker_bundle_impact` | orchestrator | Typed worker-bundle impact declaration at planning | ### Lightweight change profile @@ -186,6 +187,7 @@ class rather than operator-only. | `adv_tool_catalog` | orchestrator | Bounded read-only catalog of canonical ADV tools with descriptive visibility metadata | | `adv_tool_describe` | orchestrator | Read-only single-tool schema/metadata projection; no handler invocation | | `adv_tool_invoke` | orchestrator | Strict in-process dispatcher through the canonical wrapped `ToolDefinition.execute`; preserves ToolContext, validation, authorization, approvals, recovery restrictions, and timeouts. Recursion-exclusion (`adv_tool_invoke`, `adv_tool_catalog`, `adv_tool_describe`, `execute`) is enforced before any lookup or dispatch (`addProviderToolSearch` AC1–AC4) | +| `adv_worker_bundle_provenance_record` | orchestrator | Record worker-bundle build+replay provenance on a change after both runs pass; execution-time evidence receipt | ## Removed Tools and Replacements diff --git a/plugin/src/adv-autonomy-quality-assets.test.ts b/plugin/src/adv-autonomy-quality-assets.test.ts index 02f77bbe..a1cc3799 100644 --- a/plugin/src/adv-autonomy-quality-assets.test.ts +++ b/plugin/src/adv-autonomy-quality-assets.test.ts @@ -748,3 +748,101 @@ describe("Opportunity scout phase and schema anchors", () => { expect(ids).toContain("rq-designOpportunityScout01"); }); }); + +// ============================================================================= +// 7. Reviewer Evidence Authority (rq-reviewerEvidenceAuthority01) +// ============================================================================= + +describe("Reviewer evidence authority requirement (rq-reviewerEvidenceAuthority01)", () => { + const specPath = join(REPO_ROOT, ".adv/specs/advance-workflow/spec.json"); + const spec: { + requirements: Array<{ + id: string; + title: string; + body: string; + priority: string; + tags: string[]; + scenarios: Array<{ + id: string; + title: string; + given: string[]; + when: string; + then: string[]; + warrant?: string; + }>; + }>; + } = JSON.parse(readAsset(specPath)); + const req = spec.requirements.find( + (r) => r.id === "rq-reviewerEvidenceAuthority01", + ); + + test("requirement exists as a MUST", () => { + expect(req).toBeDefined(); + expect(req?.priority).toBe("must"); + }); + + test("tags include relevant neighboring tags", () => { + expect(req?.tags).toContain("workflow"); + expect(req?.tags).toContain("verification"); + expect(req?.tags).toContain("evidence"); + expect(req?.tags).toContain("structural"); + expect(req?.tags).toContain("review"); + }); + + test("requirement has exactly four scenarios .1 through .4", () => { + const scenarioIds = req?.scenarios.map((s) => s.id); + expect(scenarioIds).toEqual([ + "rq-reviewerEvidenceAuthority01.1", + "rq-reviewerEvidenceAuthority01.2", + "rq-reviewerEvidenceAuthority01.3", + "rq-reviewerEvidenceAuthority01.4", + ]); + }); + + test("scenario .1 warrants AC1 and describes review-policy no-block", () => { + const s = req?.scenarios.find( + (x) => x.id === "rq-reviewerEvidenceAuthority01.1", + ); + expect(s).toBeDefined(); + expect(s?.warrant).toBe("AC1"); + expect(s?.title).toMatch(/review/i); + expect(s?.title).toMatch(/linked reviewer report|review_evidence_ref/i); + expect(s?.then.join(" ")).toMatch(/no VERIFICATION_EVIDENCE_MISSING/i); + }); + + test("scenario .2 warrants AC2 and preserves block for test/static_check policies", () => { + const s = req?.scenarios.find( + (x) => x.id === "rq-reviewerEvidenceAuthority01.2", + ); + expect(s).toBeDefined(); + expect(s?.warrant).toBe("AC2"); + expect(s?.title).toMatch(/test|static_check/i); + expect(s?.title).toMatch( + /reviewer evidence|only reviewer|aggregate tests_run/i, + ); + expect(s?.then.join(" ")).toMatch( + /VERIFICATION_EVIDENCE_MISSING blocker is emitted/i, + ); + }); + + test("scenario .3 warrants AC5 and gates verification_missing warnings", () => { + const s = req?.scenarios.find( + (x) => x.id === "rq-reviewerEvidenceAuthority01.3", + ); + expect(s).toBeDefined(); + expect(s?.warrant).toBe("AC5"); + expect(s?.title).toMatch(/emitter/i); + expect(s?.title).toMatch(/verification_missing/i); + expect(s?.then.join(" ")).toMatch(/no verification_missing warnings/i); + }); + + test("scenario .4 preserves same-task ownership", () => { + const s = req?.scenarios.find( + (x) => x.id === "rq-reviewerEvidenceAuthority01.4", + ); + expect(s).toBeDefined(); + expect(s?.title).toMatch(/same-task/i); + expect(s?.title).toMatch(/ownership|review_evidence_ref/i); + expect(s?.then.join(" ")).toMatch(/cannot satisfy|not satisfy/i); + }); +}); diff --git a/plugin/src/cli-bridge-contract.test.ts b/plugin/src/cli-bridge-contract.test.ts index 48d0be9e..b94cba9c 100644 --- a/plugin/src/cli-bridge-contract.test.ts +++ b/plugin/src/cli-bridge-contract.test.ts @@ -145,6 +145,8 @@ describe("REGISTRY NO-REMOVAL GUARD (AC6/DONT1)", () => { "adv_change_update_issues", "adv_change_repair_origin", "adv_change_reenter", + "adv_change_set_worker_bundle_impact", + "adv_worker_bundle_provenance_record", "adv_epic_create", "adv_epic_show", "adv_epic_list", diff --git a/plugin/src/temporal/__tests__/archive-phase9-splitbrain.itest.ts b/plugin/src/temporal/__tests__/archive-phase9-splitbrain.itest.ts index 31848676..f0e9524b 100644 --- a/plugin/src/temporal/__tests__/archive-phase9-splitbrain.itest.ts +++ b/plugin/src/temporal/__tests__/archive-phase9-splitbrain.itest.ts @@ -158,6 +158,12 @@ function makeChangeInput( gates: makeSeedGates(), contract: fixtureContract, reentry_history: [], + // Worker-bundle provenance is enforced for new release-gate completions; + // declare it not_applicable so the phase9 recovery path is not blocked. + worker_bundle_impact: { + kind: "not_applicable", + rationale: "Integration fixture bypasses worker-bundle provenance.", + }, }, }; } diff --git a/plugin/src/temporal/gate-readiness.test.ts b/plugin/src/temporal/gate-readiness.test.ts index 10f48164..01d81965 100644 --- a/plugin/src/temporal/gate-readiness.test.ts +++ b/plugin/src/temporal/gate-readiness.test.ts @@ -1528,6 +1528,56 @@ describe("checkUnresolvedVerificationEvidence — strengthenAgentEvidence AC1/AC }; } + function reviewerReport( + overrides: { + attempt?: number; + taskId?: string; + testsRun?: string[]; + scope?: "task" | "change"; + scopeKey?: string; + consumerWarnings?: { kind: string; message: string }[]; + } = {}, + ) { + const taskId = overrides.taskId ?? "tk-ver-1"; + const scope = + overrides.scope === "change" + ? { + kind: "change" as const, + scope_key: overrides.scopeKey ?? "review:acceptance", + } + : { kind: "task" as const, task_id: taskId }; + return { + schema_version: "1.0" as const, + change_id: "strengthenAgentEvidence", + ...(scope.kind === "task" ? { task_id: taskId } : {}), + scope, + attempt: overrides.attempt ?? 1, + agent: "adv-reviewer" as const, + phase: "review" as const, + verdict: "READY" as const, + blocking_findings: [], + nonblocking_findings: [], + changes_made: [], + wisdom_candidates: [], + verification: { + tests_run: overrides.testsRun ?? ["pnpm test"], + results: "pass" as const, + evidence: "review" as const, + }, + scope_drift: null, + risks: [], + required_main_agent_actions: [], + workdir_used: "/tmp/worktree", + context_update_for_adv: { + what_ads_needs_to_know: "x", + suggested_next_action: "y", + }, + ...(overrides.consumerWarnings + ? { consumer_warnings: overrides.consumerWarnings } + : {}), + }; + } + const missingWarning = { kind: "verification_missing" as const, message: "No adv_run_test evidence found for reported command: pnpm test", @@ -1682,6 +1732,67 @@ describe("checkUnresolvedVerificationEvidence — strengthenAgentEvidence AC1/AC ); }); + it("AC1: review-policy task with linked task-scoped adv-reviewer report -> no VERIFICATION_EVIDENCE_MISSING block", () => { + const state = makeState({ + tasks: [doneTask({ evidence_policy: "review" })], + subagent_reports: [ + reviewerReport({ + testsRun: ["pnpm test"], + consumerWarnings: [], + }), + ], + }); + expect(checkUnresolvedVerificationEvidence(state, "acceptance")).toEqual( + [], + ); + }); + + it("AC2: test-policy task with only adv-reviewer evidence -> still blocks", () => { + const state = makeState({ + tasks: [doneTask({ evidence_policy: "test" })], + subagent_reports: [ + reviewerReport({ + testsRun: ["pnpm test"], + consumerWarnings: [ + { + kind: "verification_missing", + message: + "Reviewer aggregate evidence is non-authoritative; no typed adv_run_test run ID proves command: pnpm test", + }, + ], + }), + ], + }); + expect( + checkUnresolvedVerificationEvidence(state, "acceptance").some( + (b) => b.code === "VERIFICATION_EVIDENCE_MISSING", + ), + ).toBe(true); + }); + + it.each(["source_audit", "rubric_review"] as const)( + "AC3: warn-first policy %s with warnings -> no VERIFICATION_EVIDENCE_MISSING block", + (policy) => { + const state = makeState({ + tasks: [doneTask({ evidence_policy: policy })], + subagent_reports: [ + reviewerReport({ + testsRun: ["pnpm test"], + consumerWarnings: [ + { + kind: "verification_missing", + message: "No adv_run_test evidence found", + }, + ], + }), + ], + }); + expect(checkUnresolvedVerificationEvidence(state, "acceptance")).toEqual( + [], + ); + }, + ); + it("is wired into evaluateGateReadiness for acceptance", () => { const state = makeState({ gates: acceptanceReadyGates(), @@ -1920,6 +2031,61 @@ describe("checkCompletedTaskEvidencePlan — resolved plan readiness (C2/C4/C5)" ).toBe(false); }); + it("AC4: change-scoped adv-reviewer report cannot satisfy review_evidence_ref", () => { + const state = makeState({ + gates: acceptanceReadyGates(), + contract: passingContract(), + documents: { + acceptance: + "# Acceptance\n\nSubstantive acceptance proof content here.", + }, + tasks: [ + doneTask({ + evidence_policy: "review", + evidence_plan: { + policy: "review", + proof_target: "Structured review conclusion", + rationale: "Peer review is sufficient.", + review_evidence_ref: { + report_key: "change-1|change:review:acceptance|adv-reviewer|1", + }, + provenance: "new", + stage: "stage-v2", + }, + }), + ], + }); + state.subagent_reports = [ + { + schema_version: "1.0", + change_id: "change-1", + attempt: 1, + workdir_used: "/tmp/test", + agent: "adv-reviewer", + scope: { kind: "change", scope_key: "review:acceptance" }, + phase: "review", + verdict: "READY", + blocking_findings: [], + nonblocking_findings: [], + changes_made: [], + wisdom_candidates: [], + verification: { tests_run: [], results: "n/a", evidence: "review" }, + scope_drift: null, + risks: [], + required_main_agent_actions: [], + }, + ] as any; + const result = evaluateGateReadiness(state, "acceptance"); + expect( + result.blockers.some((b) => b.code === "EVIDENCE_PLAN_INVALID"), + ).toBe(true); + expect( + result.blockers.some( + (b) => b.code === "EVIDENCE_PLAN_REVIEW_PROOF_MISSING", + ), + ).toBe(false); + }); + it("does not block non-acceptance/release gates", () => { const state = makeState({ tasks: [ diff --git a/plugin/src/temporal/gate-readiness.ts b/plugin/src/temporal/gate-readiness.ts index c561061a..c519ad96 100644 --- a/plugin/src/temporal/gate-readiness.ts +++ b/plugin/src/temporal/gate-readiness.ts @@ -46,6 +46,13 @@ export interface GateReadinessOptions { compatibilityReason?: string; enforceDiscoveryContract?: boolean; enforceWorkerBundleProvenance?: boolean; + /** + * When true (default), review-matrix rows whose contractId is not present in + * the contract are flagged as unknown. When false, rows for OOS/out-of-scope + * contract items (verificationRequired: false) are not flagged as unknown. + * This is replay-gated via the `review-matrix-oos-row-v1` patch marker. + */ + strictReviewMatrixUnknownRows?: boolean; } export interface GateReadinessWarning { @@ -347,6 +354,7 @@ function discoveryContractBlockers( function acceptanceContractBlockers( state: ChangeWorkflowState, gateId: GateId, + options?: Pick, ): GateReadinessBlocker[] { if (gateId !== "acceptance") return []; if (!state.contract) { @@ -373,7 +381,9 @@ function acceptanceContractBlockers( }), ]; } - const rowCoverage = validateReviewMatrixRowCoverage(state); + const rowCoverage = validateReviewMatrixRowCoverage(state, { + strictUnknownRows: options?.strictReviewMatrixUnknownRows, + }); const rowCoverageBlockers = rowCoverage.valid ? [] : [ @@ -1112,7 +1122,7 @@ export function evaluateGateReadiness( options.compatibilityReason, ); } else { - blockers.push(...acceptanceContractBlockers(state, gateId)); + blockers.push(...acceptanceContractBlockers(state, gateId, options)); } blockers.push(...checkUnresolvedDesignConcerns(state, gateId)); blockers.push(...checkUnresolvedVerificationEvidence(state, gateId)); @@ -1157,6 +1167,7 @@ import { GATE_CRITERIA_DEFINITIONS } from "../types"; */ function validateReviewMatrixRowCoverage( state: ChangeWorkflowState, + { strictUnknownRows = true }: { strictUnknownRows?: boolean } = {}, ): | { valid: true; rowCount: number; itemCount: number } | { valid: false; reason: string } { @@ -1168,7 +1179,6 @@ function validateReviewMatrixRowCoverage( const expectedItems = contract.items.filter( (item) => item.verificationRequired !== false, ); - const expectedIds = new Set(expectedItems.map((item) => item.id)); const seen = new Set(); const duplicates: string[] = []; for (const row of contract.reviewMatrix.rows) { @@ -1183,8 +1193,13 @@ function validateReviewMatrixRowCoverage( const missing = expectedItems .filter((item) => !seen.has(item.id)) .map((item) => item.id); + // Check unknown rows against either required items only (legacy behavior, + // pre-review-matrix-oos-row-v1 patch) or all contract IDs (current behavior). + const knownIds = strictUnknownRows + ? new Set(contract.items.map((item) => item.id)) + : new Set(expectedItems.map((item) => item.id)); const unknown = contract.reviewMatrix.rows - .filter((row) => !expectedIds.has(row.contractId)) + .filter((row) => !knownIds.has(row.contractId)) .map((row) => row.contractId); if (duplicates.length > 0 || missing.length > 0 || unknown.length > 0) { diff --git a/plugin/src/temporal/messages.test.ts b/plugin/src/temporal/messages.test.ts index cd9f0b2c..993d1728 100644 --- a/plugin/src/temporal/messages.test.ts +++ b/plugin/src/temporal/messages.test.ts @@ -62,6 +62,7 @@ import { LightweightProfileRequestedSignalPayloadSchema, LightweightProfileEvaluatedSignalPayloadSchema, WorkerBundleProvenanceRecordedSignalPayloadSchema, + WorkerBundleImpactSetSignalPayloadSchema, } from "../types"; const designSignalKeys = [ @@ -126,6 +127,7 @@ const designSignalKeys = [ "updateArtifactMetadata", "originRepaired", "workerBundleProvenanceRecorded", + "workerBundleImpactSet", "archiveChange", "closeChange", ] as const; @@ -146,7 +148,7 @@ describe("change workflow message contract", () => { const surfacedKeys = Object.keys(CHANGE_WORKFLOW_SIGNAL_NAMES); expect(surfacedKeys).toEqual([...designSignalKeys]); - expect(surfacedKeys).toHaveLength(63); + expect(surfacedKeys).toHaveLength(64); for (const key of designSignalKeys) { expect(CHANGE_WORKFLOW_SIGNAL_NAMES[key]).toBe(`adv.change.${key}`); @@ -683,6 +685,16 @@ describe("change workflow message contract", () => { recorded_at: timestamp, }, ], + [ + WorkerBundleImpactSetSignalPayloadSchema, + { + worker_bundle_impact: { + kind: "required", + rationale: "workflow code changed", + }, + set_at: timestamp, + }, + ], [ ChangeCancelledSignalPayloadSchema, { diff --git a/plugin/src/temporal/workflows.ts b/plugin/src/temporal/workflows.ts index f83b8da3..061c459f 100644 --- a/plugin/src/temporal/workflows.ts +++ b/plugin/src/temporal/workflows.ts @@ -1166,6 +1166,13 @@ export async function changeWorkflow( // replay the old no-provenance-check branch. New histories enforce the // worker_bundle_impact + provenance + typed test-run requirement. const WORKER_BUNDLE_FRESHNESS_PROVENANCE_PATCH = "worker-bundle-freshness-v1"; + // Patch rationale: review-matrix unknown-row validation previously flagged rows + // for out-of-scope (verificationRequired: false) contract items as unknown. + // Current behavior treats those rows as informational and checks unknown rows + // against all contract IDs. Gate this behind a patch so pre-change acceptance + // histories replay the legacy strictness and avoid branching into new activity + // commands. + const REVIEW_MATRIX_OOS_ROW_PATCH = "review-matrix-oos-row-v1"; const blockerText = (blockers: GateReadinessBlocker[]): string => blockers.map((b) => `${b.code}: ${b.message}`).join("; "); @@ -1225,6 +1232,9 @@ export async function changeWorkflow( const stateBackedAcceptanceProofActive = payload.gateId === "acceptance" && wf.patched(STATE_BACKED_ACCEPTANCE_PROOF_PATCH); + const strictReviewMatrixUnknownRows = + payload.gateId === "acceptance" && + wf.patched(REVIEW_MATRIX_OOS_ROW_PATCH); const capturedAcceptanceReadinessRevision = acceptanceReadinessFenceActive ? (state.acceptanceReadinessRevision ?? 0) : undefined; @@ -1237,6 +1247,7 @@ export async function changeWorkflow( enforceWorkerBundleProvenance: payload.gateId === "release" && wf.patched(WORKER_BUNDLE_FRESHNESS_PROVENANCE_PATCH), + strictReviewMatrixUnknownRows, }); if (!readiness.ready) { markGateStuckForBlockers(payload, readiness.blockers); diff --git a/plugin/src/tools/subagent-report.test.ts b/plugin/src/tools/subagent-report.test.ts index df08819b..2da5264e 100644 --- a/plugin/src/tools/subagent-report.test.ts +++ b/plugin/src/tools/subagent-report.test.ts @@ -299,6 +299,36 @@ function reviewerReport( return report; } +function taskReviewerReport( + overrides: Partial = {}, +): ReviewerSubagentReport { + const report: ReviewerSubagentReport = { + schema_version: "1.0", + change_id: "change-1", + task_id: "tk-1", + attempt: 1, + agent: "adv-reviewer", + scope: { kind: "task", task_id: "tk-1" }, + workdir_used: "/repo", + phase: "review", + verdict: "READY", + blocking_findings: [], + nonblocking_findings: [], + changes_made: [], + wisdom_candidates: [], + verification: { + tests_run: ["pnpm test"], + results: "pass", + evidence: "exit code 0", + }, + scope_drift: null, + risks: [], + required_main_agent_actions: [], + ...overrides, + }; + return report; +} + function scannerBundleReport( overrides: Partial = {}, ): ScannerBundleSubagentReport { @@ -1466,6 +1496,73 @@ describe("subagentReportTools", () => { ); }); + test("rq-reviewerEvidenceAuthority01: review-policy task suppresses verification_missing for adv-reviewer reports", async () => { + const store = storeFor( + change({ + tasks: [ + { + id: "tk-1", + title: "Task one", + status: "in_progress", + priority: 1, + created_at: "2026-05-23T00:00:00.000Z", + evidence_policy: "review", + }, + ], + }), + ); + const report = taskReviewerReport(); + + const output = parse( + await subagentReportTools.adv_subagent_report_submit.execute( + { report }, + store, + ), + ); + + expect(output.success).toBe(true); + expect(output.consumerResults.verification.warnings).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "verification_missing" }), + ]), + ); + }); + + test("rq-reviewerEvidenceAuthority01: test-policy task preserves verification_missing for adv-reviewer reports", async () => { + const store = storeFor( + change({ + tasks: [ + { + id: "tk-1", + title: "Task one", + status: "in_progress", + priority: 1, + created_at: "2026-05-23T00:00:00.000Z", + evidence_policy: "test", + }, + ], + }), + ); + const report = taskReviewerReport(); + + const output = parse( + await subagentReportTools.adv_subagent_report_submit.execute( + { report }, + store, + ), + ); + + expect(output.success).toBe(true); + expect(output.consumerResults.verification.warnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + kind: "verification_missing", + message: expect.stringContaining("pnpm test"), + }), + ]), + ); + }); + test("accepts verification triage bundle reports as change-scoped sidecar evidence", async () => { const store = storeFor(change()); const report = verificationTriageBundleReport(); diff --git a/plugin/src/tools/subagent-report.ts b/plugin/src/tools/subagent-report.ts index 8d0678a2..207dffb3 100644 --- a/plugin/src/tools/subagent-report.ts +++ b/plugin/src/tools/subagent-report.ts @@ -25,6 +25,7 @@ import { getProjectId } from "../utils/project-id"; import { formatToolOutput } from "../utils/tool-output"; import { fireSignalAndRefresh, getChangeHandle } from "./_adapters"; import { saveRecoveredSubagentReport } from "./_recovery-writers"; +import { resolveTaskEvidence } from "../validator/task-classifier"; import { isWorkflowCompletedError } from "../temporal/recovery-classification"; import { formatTargetProjectContext, @@ -473,6 +474,15 @@ function verificationWarnings( durableRecords?: readonly DurableTestRunLike[], ): ConsumerWarning[] { if (report.agent === "adv-reviewer") { + // rq-reviewerEvidenceAuthority01: for review-policy tasks, the persisted + // same-task adv-reviewer report IS the authoritative completion evidence. + // Its aggregate tests_run list neither creates nor substitutes for durable + // execution evidence. Suppress verification_missing for review policy only; + // test/static_check still require durable adv_run_test evidence. + const policy = task ? resolveTaskEvidence(task).policy : undefined; + if (policy === "review") { + return []; + } return report.verification.tests_run.map((command) => ({ kind: "verification_missing" as const, message: `Reviewer aggregate evidence is non-authoritative; no typed adv_run_test run ID proves command: ${command}`,