From b81cbc7bf352fe6342ff73d0a806dd04c03b454c Mon Sep 17 00:00:00 2001 From: JRedeker Date: Sat, 25 Jul 2026 16:45:28 -0400 Subject: [PATCH 1/5] chore(adv): checkpoint tk-f0b4525a6dd8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change: fixReviewerEvidence Task: tk-f0b4525a6dd8 Mode: complete Verification: Spec rq-reviewerEvidenceAuthority01 added (4 scenarios, AC1/AC2/AC5 warrants + ownership scenario). Asset test extended. TDD red→green→verify: tr_ms0u6cch (RED 7 fail), tr_ms0u6shx (GREEN 42 pass), tr_ms0u8b5f (full check clean). Engineer report submitted with durable run IDs, 0 verification warnings. --- .adv/specs/advance-workflow/spec.json | 65 ++++++++++++ .../src/adv-autonomy-quality-assets.test.ts | 98 +++++++++++++++++++ 2 files changed, 163 insertions(+) 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/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); + }); +}); From 3cf5c2015b98488fae5bbeffa510fefbf4644c0f Mon Sep 17 00:00:00 2001 From: JRedeker Date: Sat, 25 Jul 2026 16:56:34 -0400 Subject: [PATCH 2/5] chore(adv): checkpoint tk-feabc293e2b7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change: fixReviewerEvidence Task: tk-feabc293e2b7 Mode: complete Verification: Policy-gate emitter fix: resolveTaskEvidence(task).policy check in verificationWarnings() adv-reviewer branch. Review-policy → return [] (no verification_missing). Test/static_check → current behavior preserved. TDD: tr_ms0uizpf (RED 1 fail), tr_ms0ujmvp (GREEN 66/66), tr_ms0ukubc (full check clean). Engineer report submitted with durable run IDs, 0 verification warnings. --- plugin/src/tools/subagent-report.test.ts | 97 ++++++++++++++++++++++++ plugin/src/tools/subagent-report.ts | 10 +++ 2 files changed, 107 insertions(+) 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}`, From e3c450ba187592b318bea4760d149d34317827bc Mon Sep 17 00:00:00 2001 From: JRedeker Date: Sat, 25 Jul 2026 17:09:36 -0400 Subject: [PATCH 3/5] chore(adv): checkpoint tk-0c23472dfede Change: fixReviewerEvidence Task: tk-0c23472dfede Mode: complete Verification: Regression tests added: AC1 (review-policy no block), AC2 (test-policy block remains), AC3 (warn-first non-blocking), AC4 (change-scoped rejected). 103 gate-readiness tests pass (tr_ms0v2cg0). Full pnpm run check clean (tr_ms0v3ky5). Engineer report with durable run IDs, 0 verification warnings. --- plugin/src/temporal/gate-readiness.test.ts | 166 +++++++++++++++++++++ 1 file changed, 166 insertions(+) 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: [ From 11afcccd4b81c8158f369b40490146a05fac61d6 Mon Sep 17 00:00:00 2001 From: JRedeker Date: Sat, 25 Jul 2026 17:25:06 -0400 Subject: [PATCH 4/5] fix(gate-readiness): validate review matrix unknown rows against all contract IDs The acceptance gate readiness evaluator incorrectly flagged review matrix rows for out_of_scope contract items as 'unknown'. The filter checked against expectedIds (verificationRequired: true items only) instead of all contract item IDs, causing OOS rows to be rejected despite the items existing in the contract. This blocked every change with OOS contract items from completing acceptance (recurring bug observed across fixReviewerEvidence, fixOpsResolutionProjection, and likely others). Fix: check unknown rows against allContractIds (all contract items) rather than expectedIds (required items only). OOS items remain excluded from required coverage (missing check unchanged). Refs: bl-Do-akUeH --- plugin/src/temporal/gate-readiness.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugin/src/temporal/gate-readiness.ts b/plugin/src/temporal/gate-readiness.ts index c561061a..dfc09bee 100644 --- a/plugin/src/temporal/gate-readiness.ts +++ b/plugin/src/temporal/gate-readiness.ts @@ -1168,7 +1168,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 +1182,13 @@ function validateReviewMatrixRowCoverage( const missing = expectedItems .filter((item) => !seen.has(item.id)) .map((item) => item.id); + // Check unknown rows against ALL contract item IDs, not just required ones. + // OOS items (verificationRequired: false) exist in the contract but are + // excluded from expectedIds; their review matrix rows are informational, + // not required, and must not be flagged as unknown. + const allContractIds = new Set(contract.items.map((item) => item.id)); const unknown = contract.reviewMatrix.rows - .filter((row) => !expectedIds.has(row.contractId)) + .filter((row) => !allContractIds.has(row.contractId)) .map((row) => row.contractId); if (duplicates.length > 0 || missing.length > 0 || unknown.length > 0) { From aecfe7ce5f22e91073c60d2ba3e7962368cec3e9 Mon Sep 17 00:00:00 2001 From: JRedeker Date: Sat, 25 Jul 2026 18:44:40 -0400 Subject: [PATCH 5/5] fix(ci): update tool snapshots, signal count, and regenerate replay fixtures --- docs/cli-surface-matrix.md | 2 ++ docs/tool-ownership.md | 2 ++ plugin/src/cli-bridge-contract.test.ts | 2 ++ .../archive-phase9-splitbrain.itest.ts | 6 +++++ plugin/src/temporal/gate-readiness.ts | 27 +++++++++++++------ plugin/src/temporal/messages.test.ts | 14 +++++++++- plugin/src/temporal/workflows.ts | 11 ++++++++ 7 files changed, 55 insertions(+), 9 deletions(-) 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/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.ts b/plugin/src/temporal/gate-readiness.ts index dfc09bee..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 } { @@ -1182,13 +1193,13 @@ function validateReviewMatrixRowCoverage( const missing = expectedItems .filter((item) => !seen.has(item.id)) .map((item) => item.id); - // Check unknown rows against ALL contract item IDs, not just required ones. - // OOS items (verificationRequired: false) exist in the contract but are - // excluded from expectedIds; their review matrix rows are informational, - // not required, and must not be flagged as unknown. - const allContractIds = new Set(contract.items.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) => !allContractIds.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);