",
+ "test": "src/components/RunDetail.test.tsx",
+ "selectedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "failureType": "Error",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E49130B4E62582123A5A48CB10342B79856DE973F40CE8169B55E78820890AB2",
+ "evidencePrefix": ".handoff/s8b-ui-held_detail"
+ },
+ {
+ "id": "ui/held_checkpoint",
+ "file": "spire-ui/src/components/RunDetail.tsx",
+ "before": "['Checkpoint', run.publication.checkpointHead]",
+ "after": "['Checkpoint', null]",
+ "test": "src/components/RunDetail.test.tsx",
+ "selectedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "failureType": "Error",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E49130B4E62582123A5A48CB10342B79856DE973F40CE8169B55E78820890AB2",
+ "evidencePrefix": ".handoff/s8b-ui-held_checkpoint"
+ },
+ {
+ "id": "ui/held_time",
+ "file": "spire-ui/src/components/RunPhases.tsx",
+ "before": "run.publication?.readyAt ?",
+ "after": "false ?",
+ "test": "src/components/RunDetail.test.tsx",
+ "selectedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "failureType": "TestingLibraryElementError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E3DC739E85401550905B781B9DEF7A6361297A2121AC773E22B71CE6E268D163",
+ "evidencePrefix": ".handoff/s8b-ui-held_time"
+ },
+ {
+ "id": "ui/held_link",
+ "file": "spire-ui/src/components/RunDetail.tsx",
+ "before": "`/work-items/${encodeURIComponent(run.publication.workItemId)}`",
+ "after": "`/work-items/TEST-wrong`",
+ "test": "src/components/RunDetail.test.tsx",
+ "selectedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "failureType": "Error",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E49130B4E62582123A5A48CB10342B79856DE973F40CE8169B55E78820890AB2",
+ "evidencePrefix": ".handoff/s8b-ui-held_link"
+ },
+ {
+ "id": "ui/held_duration",
+ "file": "spire-ui/src/components/RunDetail.tsx",
+ "before": "`${run.publication.activeWallSeconds} seconds`",
+ "after": "`0 seconds`",
+ "test": "src/components/RunDetail.test.tsx",
+ "selectedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows the retained checkpoint and stopped compute while publication waits",
+ "failureType": "Error",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E49130B4E62582123A5A48CB10342B79856DE973F40CE8169B55E78820890AB2",
+ "evidencePrefix": ".handoff/s8b-ui-held_duration"
+ },
+ {
+ "id": "ui/journey_evidence",
+ "file": "spire-ui/src/components/work-items/WorkItemJourney.tsx",
+ "before": "{execution &&
",
+ "after": "{true &&
",
+ "test": "src/components/work-items/WorkItemJourney.test.tsx",
+ "selectedMethod": "shows build evidence and the observed delivery state",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "shows build evidence and the observed delivery state",
+ "failureType": "AssertionError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "17D60CED7509D5BC3999F33A22911D42A06546B5417082F8B7A6DBE0C779A2D9",
+ "evidencePrefix": ".handoff/s8b-ui-journey_review"
+ },
+ {
+ "id": "ui/criterion_one_same_status",
+ "file": "spire-ui/src/components/work-items/WorkItems.tsx",
+ "before": ">{state.label}",
+ "after": ">Same journey",
+ "test": "src/components/work-items/WorkItemJourney.test.tsx",
+ "selectedMethod": "renders distinct suggest assisted and autonomous journeys",
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "renders distinct suggest assisted and autonomous journeys",
+ "failureType": "TestingLibraryElementError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "E97BCB6502DB2E55D43F7AAA6819DB6F94B8C45F8EBF8208D630DD70A596F879",
+ "evidencePrefix": ".handoff/s8b-ui-criterion_one_same_status"
+ },
+ {
+ "id": "control/review_head",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!execution.head().equals(rs.getString(\"commit_sha\"))",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.aReviewOfAnotherHeadCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aReviewOfAnotherHeadCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_head"
+ },
+ {
+ "id": "control/review_posted",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!execution.head().equals(rs.getString(\"last_posted_commit\"))",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.anUnpostedHeadCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnpostedHeadCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_posted"
+ },
+ {
+ "id": "control/review_open",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!\"OPEN\".equals(rs.getString(\"pr_state\"))",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.aClosedPullRequestCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aClosedPullRequestCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_open"
+ },
+ {
+ "id": "control/review_archived",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "rs.getTimestamp(\"archived_at\")!=null",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.anArchivedPullRequestCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anArchivedPullRequestCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_archived"
+ },
+ {
+ "id": "control/review_findings",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!readable(rs.getString(\"findings_json\"),review,false)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.unreadableEvidenceCannotMasqueradeAsZeroFindings",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unreadableEvidenceCannotMasqueradeAsZeroFindings()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_findings"
+ },
+ {
+ "id": "control/review_open_findings",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!readable(rs.getString(\"open_findings_json\"),review,false)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.missingFindingEvidenceCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "missingFindingEvidenceCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_open_findings"
+ },
+ {
+ "id": "control/review_reconciliation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "!readable(rs.getString(\"reconciliation_json\"),review,true)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.unknownReconciliationVerdictsCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unknownReconciliationVerdictsCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_reconciliation"
+ },
+ {
+ "id": "control/review_null",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "if(stored==null)return optional;",
+ "after": "if(stored==null)return true;",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.missingFindingEvidenceCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "missingFindingEvidenceCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_null"
+ },
+ {
+ "id": "control/review_array",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "if(!array.isArray())return false;",
+ "after": "if(false)return false;",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.anObjectCannotMasqueradeAsAnEmptyFindingArray",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anObjectCannotMasqueradeAsAnEmptyFindingArray()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_array"
+ },
+ {
+ "id": "control/review_entry",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "if(!entry.isObject() || !entry.path(\"loc\").isTextual() || !entry.path(\"msg\").isTextual()\n || !Set.of(\"critical\",\"warning\",\"suggestion\",\"nit\").contains(entry.path(\"sev\").asText()))return false;",
+ "after": "if(false)return false;",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.malformedFindingEntriesCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "malformedFindingEntriesCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_entry"
+ },
+ {
+ "id": "control/review_verdict",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "if(optional && !Set.of(\"RESOLVED\",\"STILL_OPEN\",\"ACKNOWLEDGED\",\"SUPERSEDED\",\"UNCHANGED\").contains(entry.path(\"status\").asText()))return false;",
+ "after": "if(false)return false;",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.unknownReconciliationVerdictsCannotAuthorizeLand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unknownReconciliationVerdictsCannotAuthorizeLand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_verdict"
+ },
+ {
+ "id": "control/review_blockers",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": "if(detail.openBlockers()>0)",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.openBlockersKeepLandWaiting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "openBlockersKeepLandWaiting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_blockers"
+ },
+ {
+ "id": "control/review_wait_dedup",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkReview.java",
+ "before": " || reason.equals(item.reason())",
+ "after": "",
+ "test": "dev.codespire.orchestrator.work.WorkReviewIT.aMissingReviewDoesNotBecomeAPassingPhase",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aMissingReviewDoesNotBecomeAPassingPhase()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0D4702E0C6A9AB2E9F80A5E004F10EE23A43FBCD2B2DDEDAC682B3DF770199C4",
+ "evidencePrefix": ".handoff/s8b-control-review_wait_dedup"
+ },
+ {
+ "id": "control/delivery_current_policy",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "if(!transitions.select(observed,item).equals(admitted.policy()) || observed.policy().revision()!=admitted.policyRevision())return \"policy_changed_before_delivery\";",
+ "after": "if(false)return \"policy_changed_before_delivery\";",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aChangedCeilingPreventsTheFirstPermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aChangedCeilingPreventsTheFirstPermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-control-delivery_current_policy"
+ },
+ {
+ "id": "control/delivery_outstanding_policy",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "QuarkusTransaction.requiringNew().call(()->claim(waiting,authority));",
+ "after": "// TEST mutation removes the outstanding-authority check.",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aChangedCeilingCancelsAnOutstandingPermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aChangedCeilingCancelsAnOutstandingPermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-control-delivery_outstanding_policy"
+ },
+ {
+ "id": "control/delivery_policy_cancel",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "if(\"refused\".equals(read(id).state()))transport.cancel(effect.run());",
+ "after": "if(false)transport.cancel(effect.run());",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aChangedCeilingCancelsAnOutstandingPermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aChangedCeilingCancelsAnOutstandingPermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-control-delivery_policy_cancel"
+ },
+ {
+ "id": "control/delivery_expiry",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!clock.now().isBefore(effect.expires())",
+ "after": "clock.now().isAfter(effect.expires())",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anExpiredUnobservedPermitStopsDeliveryAndRequestsCancellation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anExpiredUnobservedPermitStopsDeliveryAndRequestsCancellation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-control-delivery_expiry"
+ },
+ {
+ "id": "control/delivery_expiry_cancel",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "stop(effect,\"publication_permit_expired\");transport.cancel(effect.run());return;",
+ "after": "stop(effect,\"publication_permit_expired\");return;",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anExpiredUnobservedPermitStopsDeliveryAndRequestsCancellation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anExpiredUnobservedPermitStopsDeliveryAndRequestsCancellation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-control-delivery_expiry_cancel"
+ },
+ {
+ "id": "control/verify_evidence",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "case \"verify\" -> previous!=null && proof.equals(previous.verified(result.attemptId()));",
+ "after": "case \"verify\" -> true;",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.verificationMustNameTheRetainedCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "verificationMustNameTheRetainedCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-control-verify_evidence"
+ },
+ {
+ "id": "control/verify_missing_evidence",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(proof==null)return !result.successful() || previous==null || !Set.of(\"verify\",\"deliver\",\"review\").contains(item.phase());",
+ "after": "if(proof==null)return true;",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aSuccessfulFlagWithoutVerificationEvidenceCannotAdvance",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aSuccessfulFlagWithoutVerificationEvidenceCannotAdvance()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-control-verify_missing_evidence"
+ },
+ {
+ "id": "worker-decisions/worker_duplicate",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(!claimed){flushResults();return CompletableFuture.completedFuture(null);}",
+ "after": "if(false){flushResults();return CompletableFuture.completedFuture(null);}",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anExecuteRedeliveryCannotRunTheHarnessAgain",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anExecuteRedeliveryCannotRunTheHarnessAgain()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_duplicate"
+ },
+ {
+ "id": "worker-decisions/worker_claim_before_ack",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "boolean claimed=store.claim(command); // No ack until the command and shared M2 execute slot commit together.\n message.ack().toCompletableFuture().join();",
+ "after": "message.ack().toCompletableFuture().join();\n boolean claimed=store.claim(command);",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aFailedClaimCannotAcknowledgeTheCommand",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aFailedClaimCannotAcknowledgeTheCommand()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_claim_before_ack"
+ },
+ {
+ "id": "worker-decisions/worker_pre_cancel",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(claims.taken(id,RunDispatcher.CANCEL_SLOT))",
+ "after": "if(false)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.cancellationBeforeBuildBuysNoHarnessCall",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "cancellationBeforeBuildBuysNoHarnessCall()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_pre_cancel"
+ },
+ {
+ "id": "worker-decisions/worker_lease",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "else if(!leases.take(id))",
+ "after": "else if(false)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aFailedLeaseBuysNoHarnessCall",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aFailedLeaseBuysNoHarnessCall()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_lease"
+ },
+ {
+ "id": "worker-decisions/worker_build_cancel",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(cancelled(id)) result=cancelledResult(command,result);",
+ "after": "if(false) result=cancelledResult(command,result);",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.cancellationAfterTheBuildRetainsItsMeasuredUsage",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "cancellationAfterTheBuildRetainsItsMeasuredUsage()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_build_cancel"
+ },
+ {
+ "id": "worker-decisions/worker_outbox_ack",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(results.report(result))store.acknowledged(result);",
+ "after": "results.report(result);store.acknowledged(result);",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aBrokerRefusalKeepsTheResultPending",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aBrokerRefusalKeepsTheResultPending()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_outbox_ack"
+ },
+ {
+ "id": "worker-decisions/worker_local_unit",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(unit.isEmpty() || !publication.publicationHeld(unit.orElseThrow()))return;",
+ "after": "if(unit.isPresent() && !publication.publicationHeld(unit.orElseThrow()))return;",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anotherDaemonCannotClaimPublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anotherDaemonCannotClaimPublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_local_unit"
+ },
+ {
+ "id": "worker-decisions/worker_hold",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(unit.isEmpty() || !publication.publicationHeld(unit.orElseThrow()))return;",
+ "after": "if(unit.isEmpty())return;",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anUnheldUnitCannotClaimPublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnheldUnitCannotClaimPublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_hold"
+ },
+ {
+ "id": "worker-decisions/worker_permit_claim",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(!store.claimPublication(request))",
+ "after": "if(false)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aRefusedPermitCannotReachThePublisher",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aRefusedPermitCannotReachThePublisher()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_permit_claim"
+ },
+ {
+ "id": "worker-decisions/worker_unknown_publication",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "else if(!end.salvaged())",
+ "after": "else if(false)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anUnobservedPublisherRemainsRecoverable",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnobservedPublisherRemainsRecoverable()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_unknown_publication"
+ },
+ {
+ "id": "worker-decisions/worker_late_push",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(result instanceof RunResult.RunFinished finished && finished.pushedRef()!=null)return result;",
+ "after": "if(false)return result;",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aLateCancelCannotEraseAnObservedPush",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aLateCancelCannotEraseAnObservedPush()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_late_push"
+ },
+ {
+ "id": "worker-decisions/worker_cancel_authority",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "try {return !cancelled(id);}",
+ "after": "try {return true;}",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aCancelledPermitRetainsTheWorkspaceAndUsage",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aCancelledPermitRetainsTheWorkspaceAndUsage()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_cancel_authority"
+ },
+ {
+ "id": "worker-decisions/worker_refusal",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "else if(outcome.refused())",
+ "after": "else if(false)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aRefusedPublisherRetainsItsBlockedPaths",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aRefusedPublisherRetainsItsBlockedPaths()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_refusal"
+ },
+ {
+ "id": "worker-decisions/worker_stop_cancelled",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "unit.ifPresent(publication::cancel);\n store.terminal(cancelledResult(held.execution(),held.ready()));",
+ "after": "store.terminal(cancelledResult(held.execution(),held.ready()));",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aCancelledReadyRunIsRecoveredWithoutPublishing",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aCancelledReadyRunIsRecoveredWithoutPublishing()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_stop_cancelled"
+ },
+ {
+ "id": "worker-decisions/worker_live_recovery",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "if(registry.isExecuting(id) || !active.add(id))continue;",
+ "after": "if(!active.add(id))continue;",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anExecutingRunIsNotRecoveredAsAnOrphan",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anExecutingRunIsNotRecoveredAsAnOrphan()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_live_recovery"
+ },
+ {
+ "id": "worker-decisions/worker_stop_abandoned",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "unit.ifPresent(publication::cancel);\n store.abandonBuild",
+ "after": "store.abandonBuild",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.anAbandonedBuildIsNeverRebuilt",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anAbandonedBuildIsNeverRebuilt()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_stop_abandoned"
+ },
+ {
+ "id": "worker-decisions/worker_cleanup_after_commit",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "store.terminal(result); // Paid usage and publication evidence are durable before any deletion or send.\n releasePublished(store.find(id).orElseThrow(),publication);",
+ "after": "releasePublished(store.find(id).orElseThrow(),publication);\n store.terminal(result);",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aSuccessfulPublicationPersistsBeforeDeletingAndReporting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aSuccessfulPublicationPersistsBeforeDeletingAndReporting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_cleanup_after_commit"
+ },
+ {
+ "id": "worker-decisions/worker_cleanup_marker",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "store.released(id);",
+ "after": "// TEST mutation drops durable cleanup acknowledgement.",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aSuccessfulPublicationPersistsBeforeDeletingAndReporting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aSuccessfulPublicationPersistsBeforeDeletingAndReporting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_cleanup_marker"
+ },
+ {
+ "id": "worker-decisions/worker_cleanup_lease",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "leases.release(id);",
+ "after": "// TEST mutation keeps a completed lease.",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.aSuccessfulPublicationPersistsBeforeDeletingAndReporting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aSuccessfulPublicationPersistsBeforeDeletingAndReporting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "18D68C9633278EA2F5F581CC3A7C39A6188093B2B5F4A3902F673806EC2C0EA4",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-worker_cleanup_lease"
+ },
+ {
+ "id": "worker-decisions/store_topology_identity",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "if (!runId.equals(unit.runId()))",
+ "after": "if (false)",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.topologyMustNameItsClaimedRun",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "topologyMustNameItsClaimedRun()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_topology_identity"
+ },
+ {
+ "id": "worker-decisions/store_topology_once",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND state='building' AND unit_spec IS NULL",
+ "after": "WHERE run_id=? AND state='building'",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.topologyIsRetainedOnceWithItsOriginalLimitsAndEnvironment",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "topologyIsRetainedOnceWithItsOriginalLimitsAndEnvironment()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_topology_once"
+ },
+ {
+ "id": "worker-decisions/store_ready_topology",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND binding=? AND state='building' AND unit_spec IS NOT NULL",
+ "after": "WHERE run_id=? AND binding=? AND state='building'",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.readinessCannotPrecedeTheRetainedTopology",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readinessCannotPrecedeTheRetainedTopology()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_ready_topology"
+ },
+ {
+ "id": "worker-decisions/store_ready_once",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND binding=? AND state='building' AND unit_spec IS NOT NULL",
+ "after": "WHERE run_id=? AND binding=? AND unit_spec IS NOT NULL",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.readinessIsWrittenOnlyOnce",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readinessIsWrittenOnlyOnce()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_ready_once"
+ },
+ {
+ "id": "worker-decisions/store_ready_binding",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND binding=? AND state='building' AND unit_spec IS NOT NULL",
+ "after": "WHERE run_id=? AND ? IS NOT NULL AND state='building' AND unit_spec IS NOT NULL",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.aReadyResultCannotClaimAnotherPreparation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aReadyResultCannotClaimAnotherPreparation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_ready_binding"
+ },
+ {
+ "id": "worker-decisions/store_final_once",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND final_result IS NULL",
+ "after": "WHERE run_id=?",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.readyAndFinalResultsHaveSeparateRecoverableAcknowledgments",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readyAndFinalResultsHaveSeparateRecoverableAcknowledgments()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_final_once"
+ },
+ {
+ "id": "worker-decisions/store_claim_work",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "|| !held.execution().work().equals(request.permit().work())",
+ "after": "|| false",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.aPermitNeedsTheMatchingGenerationBuildAndHead",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aPermitNeedsTheMatchingGenerationBuildAndHead()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_claim_work"
+ },
+ {
+ "id": "worker-decisions/store_claim_head",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "|| !held.ready().head().equals(request.permit().head())",
+ "after": "|| false",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.aPermitNeedsTheMatchingGenerationBuildAndHead",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aPermitNeedsTheMatchingGenerationBuildAndHead()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_claim_head"
+ },
+ {
+ "id": "worker-decisions/store_claim_time",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "|| !request.permit().validAt(now)",
+ "after": "|| false",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.expiredOrFuturePermitsCannotClaimPublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "expiredOrFuturePermitsCannotClaimPublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_claim_time"
+ },
+ {
+ "id": "worker-decisions/store_claim_ready",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "!\"ready\".equals(held.state())",
+ "after": "false",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.publicationClaimCommitsThePermitBeforeAnyIo",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationClaimCommitsThePermitBeforeAnyIo()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_claim_ready"
+ },
+ {
+ "id": "worker-decisions/store_claim_cancel",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "slot='cancel')\n \"\"\")) {",
+ "after": "slot='TEST-not-cancel')\n \"\"\")) {",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.aDurableCancellationBlocksAnOtherwiseValidPermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aDurableCancellationBlocksAnOtherwiseValidPermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_claim_cancel"
+ },
+ {
+ "id": "worker-decisions/store_recovery_fresh",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE runworker.run_lease.preserved_at IS NOT NULL OR runworker.run_lease.heartbeat_at < ?",
+ "after": "WHERE runworker.run_lease.preserved_at IS NOT NULL OR runworker.run_lease.heartbeat_at > ?",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.recoveryCannotTakeAFreshPublicationOwner",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "recoveryCannotTakeAFreshPublicationOwner()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_recovery_fresh"
+ },
+ {
+ "id": "worker-decisions/store_recovery_preserved",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE runworker.run_lease.preserved_at IS NOT NULL OR runworker.run_lease.heartbeat_at < ?",
+ "after": "WHERE runworker.run_lease.heartbeat_at < ?",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.recoveryCannotTakeAFreshPublicationOwner",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "recoveryCannotTakeAFreshPublicationOwner()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_recovery_preserved"
+ },
+ {
+ "id": "worker-decisions/store_recovery_stale",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE runworker.run_lease.preserved_at IS NOT NULL OR runworker.run_lease.heartbeat_at < ?",
+ "after": "WHERE runworker.run_lease.preserved_at IS NOT NULL AND runworker.run_lease.heartbeat_at < ?",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.recoveryCanTakeADeadPublicationOwner",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "recoveryCanTakeADeadPublicationOwner()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_recovery_stale"
+ },
+ {
+ "id": "worker-decisions/store_recovery_state",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "FROM runworker.work_run WHERE run_id=? AND state='publishing'",
+ "after": "FROM runworker.work_run WHERE run_id=?",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.anUnclaimedReadyBuildCannotBecomePublicationRecovery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnclaimedReadyBuildCannotBecomePublicationRecovery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_recovery_state"
+ },
+ {
+ "id": "worker-decisions/store_abandon_state",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "WHERE run_id=? AND state='building'\n \"\"\",encode(failure.runId()",
+ "after": "WHERE run_id=?\n \"\"\",encode(failure.runId()",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.staleBuildRecoveryCannotOverwriteCommittedReadiness",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "staleBuildRecoveryCannotOverwriteCommittedReadiness()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_abandon_state"
+ },
+ {
+ "id": "worker-decisions/store_release_push",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "result instanceof RunResult.RunFinished finished && finished.pushedRef()!=null",
+ "after": "result instanceof RunResult.RunFinished finished",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.onlyObservedSuccessfulPublicationSchedulesDeletion",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "onlyObservedSuccessfulPublicationSchedulesDeletion()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_release_push"
+ },
+ {
+ "id": "worker-decisions/store_release_observed",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "&& !finished.agentUnobserved(), result.runId());",
+ "after": ", result.runId());",
+ "test": "dev.codespire.runworker.WorkRunStoreTest.onlyObservedSuccessfulPublicationSchedulesDeletion",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "onlyObservedSuccessfulPublicationSchedulesDeletion()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "0B6C6143CE90A7B39E1A88B61F36C5AB25B4C1B36C1CCE8D435A2E17EBFAF1F2",
+ "evidencePrefix": ".handoff/s8b-worker-decisions-store_release_observed"
+ },
+ {
+ "id": "evidence/builder_unit",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "!held.runId().equals(request.runId())",
+ "after": "false",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationRequiresTheRetainedUnitIdentity",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationRequiresTheRetainedUnitIdentity()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_unit"
+ },
+ {
+ "id": "evidence/builder_command",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "!original.runId().equals(request.runId())",
+ "after": "false",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationRequiresTheOriginalCommandIdentity",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationRequiresTheOriginalCommandIdentity()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_command"
+ },
+ {
+ "id": "evidence/builder_binding",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "!original.work().equals(request.permit().work())",
+ "after": "false",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationRequiresTheOriginalWorkBinding",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationRequiresTheOriginalWorkBinding()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_binding"
+ },
+ {
+ "id": "evidence/builder_fresh_scm",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "credentials.scm(request.runId(), request.scmCredential())",
+ "after": "credentials.scm(original.runId(), original.scmCredential())",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationDecryptsOnlyTheFreshScmCredential",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationDecryptsOnlyTheFreshScmCredential()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_fresh_scm"
+ },
+ {
+ "id": "evidence/builder_no_harness",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "Credentials.Scm scm = credentials.scm(request.runId(), request.scmCredential());",
+ "after": "credentials.harnessEnv(original.runId(), original.harnessCredential());\n Credentials.Scm scm = credentials.scm(request.runId(), request.scmCredential());",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationDecryptsOnlyTheFreshScmCredential",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationDecryptsOnlyTheFreshScmCredential()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_no_harness"
+ },
+ {
+ "id": "evidence/builder_original_floor",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "new java.util.LinkedHashSet<>(original.execution().protectedPaths())",
+ "after": "new java.util.LinkedHashSet()",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationPreservesBothProtectedPathFloors",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationPreservesBothProtectedPathFloors()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_original_floor"
+ },
+ {
+ "id": "evidence/builder_current_floor",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "paths.addAll(request.permit().protectedPaths());",
+ "after": "// TEST mutation omits the current floor.",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationPreservesBothProtectedPathFloors",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationPreservesBothProtectedPathFloors()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_current_floor"
+ },
+ {
+ "id": "evidence/builder_permitted_head",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "env.put(\"SPIRE_PERMITTED_HEAD\", request.permit().head());",
+ "after": "// TEST mutation omits the head.",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.theTrustedPublisherReceivesTheExactHeadAndLeaseWindow",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theTrustedPublisherReceivesTheExactHeadAndLeaseWindow()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_permitted_head"
+ },
+ {
+ "id": "evidence/builder_permit_start",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "env.put(\"SPIRE_PERMIT_ISSUED_AT\", request.permit().issuedAt().toString());",
+ "after": "// TEST mutation omits the issue boundary.",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.theTrustedPublisherReceivesTheExactHeadAndLeaseWindow",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theTrustedPublisherReceivesTheExactHeadAndLeaseWindow()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_permit_start"
+ },
+ {
+ "id": "evidence/builder_permit_end",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "env.put(\"SPIRE_PERMIT_EXPIRES_AT\", request.permit().expiresAt().toString());",
+ "after": "// TEST mutation omits the expiry boundary.",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.theTrustedPublisherReceivesTheExactHeadAndLeaseWindow",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theTrustedPublisherReceivesTheExactHeadAndLeaseWindow()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_permit_end"
+ },
+ {
+ "id": "evidence/builder_permitted_binary",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "held.publisher().image(), List.of(\"spire-publish-permitted\")",
+ "after": "held.publisher().image(), List.of(\"spire-publish\")",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.theTrustedPublisherReceivesTheExactHeadAndLeaseWindow",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theTrustedPublisherReceivesTheExactHeadAndLeaseWindow()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_permitted_binary"
+ },
+ {
+ "id": "evidence/builder_original_image",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunUnitBuilder.java",
+ "before": "new ContainerSpec(held.publisher().image(), List.of(\"spire-publish-permitted\")",
+ "after": "new ContainerSpec(publisherImage, List.of(\"spire-publish-permitted\")",
+ "test": "dev.codespire.runworker.HeldRunUnitBuilderTest.publicationKeepsTheOriginalImageTopologyAndBudgets",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationKeepsTheOriginalImageTopologyAndBudgets()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "A1A469B2DB622676CD61FE4CBBDE7A335346C06412C66F8A710A9D05A071511B",
+ "evidencePrefix": ".handoff/s8b-evidence-builder_original_image"
+ },
+ {
+ "id": "evidence/checkpoint_full_head",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/PublisherOutcome.java",
+ "before": "if (!head.matches(\"[0-9a-f]{40}\"))",
+ "after": "if (false)",
+ "test": "dev.codespire.runworker.HeldPublisherOutcomeTest.malformedCheckpointCannotLeaveAnEarlierHeadReady",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "malformedCheckpointCannotLeaveAnEarlierHeadReady()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "11889B10C596B5A513B1784C7ED38B937CA075C8336F0D1FCD3F208CCC64341D",
+ "evidencePrefix": ".handoff/s8b-evidence-checkpoint_full_head"
+ },
+ {
+ "id": "evidence/checkpoint_terminal",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/PublisherOutcome.java",
+ "before": "failedAfterCheckpoint = checkpointHead != null && !NON_TERMINAL_CAUSES.contains(failureCause);",
+ "after": "failedAfterCheckpoint = false;",
+ "test": "dev.codespire.runworker.HeldPublisherOutcomeTest.terminalPublisherFailureOutranksTheHeldCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "terminalPublisherFailureOutranksTheHeldCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "11889B10C596B5A513B1784C7ED38B937CA075C8336F0D1FCD3F208CCC64341D",
+ "evidencePrefix": ".handoff/s8b-evidence-checkpoint_terminal"
+ },
+ {
+ "id": "evidence/checkpoint_nonterminal",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/PublisherOutcome.java",
+ "before": "failedAfterCheckpoint = checkpointHead != null && !NON_TERMINAL_CAUSES.contains(failureCause);",
+ "after": "failedAfterCheckpoint = checkpointHead != null;",
+ "test": "dev.codespire.runworker.HeldPublisherOutcomeTest.unreadableBundleKeepsTheLastObservedCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unreadableBundleKeepsTheLastObservedCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "11889B10C596B5A513B1784C7ED38B937CA075C8336F0D1FCD3F208CCC64341D",
+ "evidencePrefix": ".handoff/s8b-evidence-checkpoint_nonterminal"
+ },
+ {
+ "id": "evidence/checkpoint_refused",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/PublisherOutcome.java",
+ "before": "return refused() || failedAfterCheckpoint ? Optional.empty() : Optional.ofNullable(checkpointHead);",
+ "after": "return failedAfterCheckpoint ? Optional.empty() : Optional.ofNullable(checkpointHead);",
+ "test": "dev.codespire.runworker.HeldPublisherOutcomeTest.pathRefusalOutranksTheHeldCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "pathRefusalOutranksTheHeldCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "11889B10C596B5A513B1784C7ED38B937CA075C8336F0D1FCD3F208CCC64341D",
+ "evidencePrefix": ".handoff/s8b-evidence-checkpoint_refused"
+ },
+ {
+ "id": "evidence/orphan_hold",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/OrphanWatchdog.java",
+ "before": "publication.publicationHeld(unit)",
+ "after": "false",
+ "test": "dev.codespire.runworker.OrphanWatchdogTest.aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "9ED5CA5623CAA073E1F45952B84EA9BD037A0D40887837231A639691E4D46027",
+ "evidencePrefix": ".handoff/s8b-evidence-orphan_hold"
+ },
+ {
+ "id": "evidence/orphan_stop",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/OrphanWatchdog.java",
+ "before": "stop(unit);\n LOG.warn(\"publication is held; stopped the abandoned processes and retained their workspace\");",
+ "after": "LOG.warn(\"publication is held; stopped the abandoned processes and retained their workspace\");",
+ "test": "dev.codespire.runworker.OrphanWatchdogTest.aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "9ED5CA5623CAA073E1F45952B84EA9BD037A0D40887837231A639691E4D46027",
+ "evidencePrefix": ".handoff/s8b-evidence-orphan_stop"
+ },
+ {
+ "id": "evidence/orphan_no_fallthrough",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/OrphanWatchdog.java",
+ "before": "LOG.warn(\"publication is held; stopped the abandoned processes and retained their workspace\");\n return;",
+ "after": "LOG.warn(\"publication is held; stopped the abandoned processes and retained their workspace\");",
+ "test": "dev.codespire.runworker.OrphanWatchdogTest.aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aHeldWorkspaceWithNoLeaseIsStoppedAndRetainedWithoutInventingATerminalResult()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "9ED5CA5623CAA073E1F45952B84EA9BD037A0D40887837231A639691E4D46027",
+ "evidencePrefix": ".handoff/s8b-evidence-orphan_no_fallthrough"
+ },
+ {
+ "id": "evidence/ready_run",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "if (runId == null || runId.isBlank()) throw new IllegalArgumentException(\"A ready run needs its identity\");",
+ "after": "if (false) throw new IllegalArgumentException(\"A ready run needs its identity\");",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.readyNeedsItsRunWorkAndCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readyNeedsItsRunWorkAndCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_run"
+ },
+ {
+ "id": "evidence/ready_work",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "Objects.requireNonNull(work, \"A ready run needs its work binding\");",
+ "after": "",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.readyNeedsItsRunWorkAndCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readyNeedsItsRunWorkAndCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_work"
+ },
+ {
+ "id": "evidence/ready_head",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "if (head == null || !head.matches(\"[0-9a-f]{40}\"))",
+ "after": "if (false)",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.readyNeedsItsRunWorkAndCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "readyNeedsItsRunWorkAndCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_head"
+ },
+ {
+ "id": "evidence/ready_wall",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "if (activeWallSeconds < 0)",
+ "after": "if (false)",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.wallTimeCannotBeNegative",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "wallTimeCannotBeNegative()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_wall"
+ },
+ {
+ "id": "evidence/ready_empty_usage",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "if (tokenUsage.isEmpty()) throw new IllegalArgumentException(\"An empty usage map is not a measurement\");",
+ "after": "if (false) throw new IllegalArgumentException(\"An empty usage map is not a measurement\");",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.usageAndPathsStayImmutableAndUnknownStaysUnknown",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "usageAndPathsStayImmutableAndUnknownStaysUnknown()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_empty_usage"
+ },
+ {
+ "id": "evidence/ready_usage_copy",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunResult.java",
+ "before": "tokenUsage = Map.copyOf(tokenUsage);\n }\n if (activeWallSeconds",
+ "after": "tokenUsage = tokenUsage;\n }\n if (activeWallSeconds",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.usageAndPathsStayImmutableAndUnknownStaysUnknown",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "usageAndPathsStayImmutableAndUnknownStaysUnknown()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "60B43355693ED906BE34891DB22D1EB905CA32D0BE785A80700DDBB1D12CC756",
+ "evidencePrefix": ".handoff/s8b-evidence-ready_usage_copy"
+ },
+ {
+ "id": "evidence/binding_length_prefix",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkRunBinding.java",
+ "before": "digest.update(java.nio.ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array());",
+ "after": "",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.runtimeBindingCoversEveryIdentityComponent",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "runtimeBindingCoversEveryIdentityComponent()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "4F04BD18C41733570612B476F2226169C4E1ABAD537AF86D20F3180DBD852B31",
+ "evidencePrefix": ".handoff/s8b-evidence-binding_length_prefix"
+ },
+ {
+ "id": "evidence/binding_content",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkRunBinding.java",
+ "before": "digest.update(bytes);",
+ "after": "",
+ "test": "dev.codespire.contract.work.WorkReadyProtocolTest.runtimeBindingCoversEveryIdentityComponent",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "runtimeBindingCoversEveryIdentityComponent()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "4F04BD18C41733570612B476F2226169C4E1ABAD537AF86D20F3180DBD852B31",
+ "evidencePrefix": ".handoff/s8b-evidence-binding_content"
+ },
+ {
+ "id": "evidence/execution_run",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "if(runId==null || runId.isBlank())",
+ "after": "if(false)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aBlankRunCannotNameAnExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aBlankRunCannotNameAnExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_run"
+ },
+ {
+ "id": "evidence/execution_binding",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "Objects.requireNonNull(build,\"build\");",
+ "after": "",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aMissingBindingIsRejected",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aMissingBindingIsRejected()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_binding"
+ },
+ {
+ "id": "evidence/execution_head",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "if(head==null || !head.matches(\"[0-9a-f]{40}\"))",
+ "after": "if(false)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aPartialHeadCannotNameVerifiedWork",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aPartialHeadCannotNameVerifiedWork()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_head"
+ },
+ {
+ "id": "evidence/execution_review",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "if(reviewId!=null && reviewId.isBlank())",
+ "after": "if(false)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aBlankReviewCannotNameEvidence",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aBlankReviewCannotNameEvidence()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_review"
+ },
+ {
+ "id": "evidence/execution_verify_nonnull",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "Objects.requireNonNull(attempt)",
+ "after": "attempt",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aVerificationWitherRequiresItsAttempt",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aVerificationWitherRequiresItsAttempt()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_verify_nonnull"
+ },
+ {
+ "id": "evidence/execution_delivery_nonnull",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "Objects.requireNonNull(ref)",
+ "after": "ref",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aDeliveryWitherRequiresItsPullRequest",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aDeliveryWitherRequiresItsPullRequest()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_delivery_nonnull"
+ },
+ {
+ "id": "evidence/execution_review_nonnull",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkExecution.java",
+ "before": "Objects.requireNonNull(id)",
+ "after": "id",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aReviewWitherRequiresItsReview",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aReviewWitherRequiresItsReview()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D6D4826648DD04295756FD41A7B0E517E0A88429A963B5A26FB650ED405AB00A",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_review_nonnull"
+ },
+ {
+ "id": "evidence/execution_new_build",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "usageUnknown,build?null:execution",
+ "after": "usageUnknown,execution",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.aNewBuildCannotReuseEarlierVerification",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aNewBuildCannotReuseEarlierVerification()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_new_build"
+ },
+ {
+ "id": "evidence/execution_keep_reservation",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "attemptState,value,startedAt,runs,steps,wallSeconds,costMillicents,calls,usageUnknown,execution",
+ "after": "attemptState,value,startedAt,runs,steps,wallSeconds,costMillicents,calls,usageUnknown,null",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.reservationAndUsageChangesPreserveExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "reservationAndUsageChangesPreserveExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_keep_reservation"
+ },
+ {
+ "id": "evidence/execution_keep_unknown",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "costMillicents,calls,true,execution",
+ "after": "costMillicents,calls,true,null",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.reservationAndUsageChangesPreserveExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "reservationAndUsageChangesPreserveExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_keep_unknown"
+ },
+ {
+ "id": "evidence/execution_keep_finish",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "Math.addExact(calls,callCount),usageUnknown,execution);\n }\n /** Late spend",
+ "after": "Math.addExact(calls,callCount),usageUnknown,null);\n }\n /** Late spend",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.reservationAndUsageChangesPreserveExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "reservationAndUsageChangesPreserveExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_keep_finish"
+ },
+ {
+ "id": "evidence/execution_keep_phase",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "usageUnknown,build?null:execution",
+ "after": "usageUnknown,null",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.reservationAndUsageChangesPreserveExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "reservationAndUsageChangesPreserveExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-evidence-execution_keep_phase"
+ },
+ {
+ "id": "replay/approval_bypass",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkItemLifecycle.java",
+ "before": "if(\"approve\".equals(mode) && !approved) {",
+ "after": "if(false && \"approve\".equals(mode) && !approved) {",
+ "test": "dev.codespire.orchestrator.work.WorkItemJourneyIT.threeProfilesProduceDifferentVisibleJourneys",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "threeProfilesProduceDifferentVisibleJourneys()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "124C80ECD6068D4043B66F764BD9D8D79AD8AC35261A523613AB551F0E254F06",
+ "evidencePrefix": ".handoff/s8b-replay-approval_bypass"
+ },
+ {
+ "id": "replay/standalone_proposal",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryPullRequests.java",
+ "before": "plan.workItemId()!=null || ",
+ "after": "",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.itemRunCannotUseStandaloneAutomaticProposal",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "itemRunCannotUseStandaloneAutomaticProposal()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "38B26635B9600AA4FC8A2ACA2B640F5C7B6F06184F260CA318C6E62B1F7164D5",
+ "evidencePrefix": ".handoff/s8b-replay-standalone_proposal"
+ },
+ {
+ "id": "inbox/late_association",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(!rs.next())return new Outcome(409,\"build_attempt_missing\",item);",
+ "after": "if(!rs.next())return new Outcome(200,\"build_attempt_missing\",item);",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.anUnassociatedAttemptCannotBuyLateUsage",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnassociatedAttemptCannotBuyLateUsage()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_association"
+ },
+ {
+ "id": "inbox/late_completed",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(\"completed\".equals(rs.getString(1)))return new Outcome(200,\"result_already_applied\",item);",
+ "after": "if(false)return new Outcome(200,\"result_already_applied\",item);",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.aCompletedBuildCannotBuyItsUsageAgainThroughLateRecovery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aCompletedBuildCannotBuyItsUsageAgainThroughLateRecovery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_completed"
+ },
+ {
+ "id": "inbox/late_active",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(\"active\".equals(item.workflowStatus()) && Objects.equals(result.attemptId(),item.progress().attemptId()))",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.aCurrentBuildMustUseNormalCompletionInsteadOfLateAccounting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aCurrentBuildMustUseNormalCompletionInsteadOfLateAccounting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_active"
+ },
+ {
+ "id": "inbox/build_item",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.build().workItemId().equals(item.workItemId())",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionRejectsAnotherItem",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionRejectsAnotherItem()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_item"
+ },
+ {
+ "id": "inbox/build_generation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.build().generation()==item.generation()",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionRejectsAnotherGeneration",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionRejectsAnotherGeneration()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_generation"
+ },
+ {
+ "id": "inbox/build_attempt",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.build().buildAttemptId().equals(result.attemptId())",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionRejectsAnotherAttempt",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionRejectsAnotherAttempt()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_attempt"
+ },
+ {
+ "id": "inbox/build_preparation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.build().preparationBinding().equals(item.preparation().binding())",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionRejectsAnotherPreparation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionRejectsAnotherPreparation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_preparation"
+ },
+ {
+ "id": "inbox/build_verification",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.verificationAttempt()==null",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionCannotClaimVerification",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionCannotClaimVerification()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_verification"
+ },
+ {
+ "id": "inbox/build_delivery",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.pullRequest()==null",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionCannotClaimDelivery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionCannotClaimDelivery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_delivery"
+ },
+ {
+ "id": "inbox/build_review",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "proof.reviewId()==null",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.buildCompletionCannotClaimReview",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "buildCompletionCannotClaimReview()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-build_review"
+ },
+ {
+ "id": "inbox/late_stopped",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemRunBridge.java",
+ "before": "Set.of(\"attempt_changed\",\"item_not_active\")",
+ "after": "Set.of(\"attempt_changed\")",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "F7BD7D503865E199F0D01B56F7D55DC5352E7F451551EC88EA9B64C674718678",
+ "evidencePrefix": ".handoff/s8b-inbox-late_stopped"
+ },
+ {
+ "id": "inbox/late_generation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemRunBridge.java",
+ "before": "Set.of(\"attempt_changed\",\"item_not_active\")",
+ "after": "Set.of(\"item_not_active\")",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.anOlderGenerationAccountsUsageWithoutFinishingTheCurrentBuild",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anOlderGenerationAccountsUsageWithoutFinishingTheCurrentBuild()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "F7BD7D503865E199F0D01B56F7D55DC5352E7F451551EC88EA9B64C674718678",
+ "evidencePrefix": ".handoff/s8b-inbox-late_generation"
+ },
+ {
+ "id": "inbox/late_dedup",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(history.stream().anyMatch(event->key.equals(event.correlationId())))",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_dedup"
+ },
+ {
+ "id": "inbox/late_unknown",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(!result.usageKnown())progress=progress.unknownUsage();\n var next=state(item",
+ "after": "if(false)progress=progress.unknownUsage();\n var next=state(item",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.unmeasuredLateBuildUsageRemainsUnknown",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unmeasuredLateBuildUsageRemainsUnknown()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_unknown"
+ },
+ {
+ "id": "inbox/late_usage",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "item.progress().account(result.wallSeconds(),result.costMillicents(),result.calls())",
+ "after": "item.progress()",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.anOlderGenerationAccountsUsageWithoutFinishingTheCurrentBuild",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anOlderGenerationAccountsUsageWithoutFinishingTheCurrentBuild()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "DAB8DCF1E4F168A6EE176075D039BE056E973C6BC2374F937C4E691F7260EC44",
+ "evidencePrefix": ".handoff/s8b-inbox-late_usage"
+ },
+ {
+ "id": "inbox/ready_processed",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemRunBridge.java",
+ "before": "SET ready_processed=true WHERE run_id=?",
+ "after": "SET ready_processed=false WHERE run_id=?",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aStoppedBuildAccountsLateReadinessAndAcknowledgesItsInbox()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "F7BD7D503865E199F0D01B56F7D55DC5352E7F451551EC88EA9B64C674718678",
+ "evidencePrefix": ".handoff/s8b-inbox-ready_processed"
+ },
+ {
+ "id": "inbox/projection_b_item",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "b.work_item_id=?",
+ "after": "CAST(? AS text) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherItem",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherItem()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_b_item"
+ },
+ {
+ "id": "inbox/projection_e_item",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "e.work_item_id=?",
+ "after": "CAST(? AS text) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherItem",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherItem()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_e_item"
+ },
+ {
+ "id": "inbox/projection_b_generation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "b.generation=?",
+ "after": "CAST(? AS bigint) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherGeneration",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherGeneration()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_b_generation"
+ },
+ {
+ "id": "inbox/projection_e_generation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "e.generation=?",
+ "after": "CAST(? AS bigint) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherGeneration",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherGeneration()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_e_generation"
+ },
+ {
+ "id": "inbox/projection_b_attempt",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "b.attempt_id=?",
+ "after": "CAST(? AS uuid) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherAttempt",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherAttempt()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_b_attempt"
+ },
+ {
+ "id": "inbox/projection_e_attempt",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "e.attempt_id=?",
+ "after": "CAST(? AS uuid) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherAttempt",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherAttempt()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_e_attempt"
+ },
+ {
+ "id": "inbox/projection_b_preparation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "b.preparation_binding=?",
+ "after": "CAST(? AS text) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherPreparation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherPreparation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_b_preparation"
+ },
+ {
+ "id": "inbox/projection_e_preparation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "e.preparation_binding=?",
+ "after": "CAST(? AS text) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.theProjectionIndependentlyRejectsAnotherPreparation",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "theProjectionIndependentlyRejectsAnotherPreparation()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-projection_e_preparation"
+ },
+ {
+ "id": "inbox/checkpoint_after_terminal",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FactoryRunProjection.java",
+ "before": "AND b.work_item_id=? AND b.generation=? AND b.attempt_id=? AND b.preparation_binding=?)",
+ "after": "AND b.work_item_id=? AND b.generation=? AND b.attempt_id=? AND b.preparation_binding=?) AND ended_at IS NULL",
+ "test": "dev.codespire.orchestrator.work.WorkItemRunBridgeTest.publicationBeforeReadinessCannotSkipTheBuildCheckpoint",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationBeforeReadinessCannotSkipTheBuildCheckpoint()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "151D7EDA8840126853FD55B66C716989B6C4D2A1356DEAF3BB800AF88BBDD622",
+ "evidencePrefix": ".handoff/s8b-inbox-checkpoint_after_terminal"
+ },
+ {
+ "id": "inbox/proposal_cas",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "AND (state=? OR (?='refused' AND state NOT IN ('delivered','refused')))",
+ "after": "AND (CAST(? AS text) IS NOT NULL OR (?='refused' AND state NOT IN ('delivered','refused')))",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aStalePublicationReaderCannotRearmAClaimedProposal",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aStalePublicationReaderCannotRearmAClaimedProposal()",
+ "failureType": "com.github.tomakehurst.wiremock.client.VerificationException",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-proposal_cas"
+ },
+ {
+ "id": "inbox/proposal_cas_return",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "if(!state(effect,\"pushed\",null))return;",
+ "after": "state(effect,\"pushed\",null);",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aStalePublicationReaderCannotRearmAClaimedProposal",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aStalePublicationReaderCannotRearmAClaimedProposal()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-proposal_cas_return"
+ },
+ {
+ "id": "inbox/delivery_processed",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "AND b.ready_processed AND r.checkpoint_head=?",
+ "after": "AND r.checkpoint_head=?",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anUnprocessedCheckpointCannotAuthorizePublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnprocessedCheckpointCannotAuthorizePublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_processed"
+ },
+ {
+ "id": "inbox/delivery_head",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "r.checkpoint_head=?",
+ "after": "CAST(? AS text) IS NOT NULL",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anotherObservedHeadCannotAuthorizePublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anotherObservedHeadCannotAuthorizePublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_head"
+ },
+ {
+ "id": "inbox/delivery_verified_state",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "AND v.state='completed'",
+ "after": "",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anIncompleteVerificationCannotAuthorizePublication",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anIncompleteVerificationCannotAuthorizePublication()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_verified_state"
+ },
+ {
+ "id": "inbox/delivery_verified_phase",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "AND v.phase='verify'",
+ "after": "",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anotherCompletedPhaseCannotStandInForVerification",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anotherCompletedPhaseCannotStandInForVerification()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_verified_phase"
+ },
+ {
+ "id": "inbox/delivery_held_state",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "\"pending\".equals(effect.state()) && !\"awaiting_delivery\".equals(run.status())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aRunWithoutTheHeldReadyStateCannotReceiveAPermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aRunWithoutTheHeldReadyStateCannotReceiveAPermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_held_state"
+ },
+ {
+ "id": "inbox/delivery_definite_miss",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "sent instanceof RunLaunch.DefiniteMiss",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aDefinitePermitMissRearmsOnlyTheSameDelivery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aDefinitePermitMissRearmsOnlyTheSameDelivery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_definite_miss"
+ },
+ {
+ "id": "inbox/delivery_native_state",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!Objects.equals(claim.request().draft(),opened.draft())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anObservedDraftCannotCompleteRegularDelivery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anObservedDraftCannotCompleteRegularDelivery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_native_state"
+ },
+ {
+ "id": "inbox/delivery_unknown_native_state",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!Objects.equals(claim.request().draft(),opened.draft())",
+ "after": "opened.draft()!=null && !Objects.equals(claim.request().draft(),opened.draft())",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anUnknownNativeStateCannotCompleteRegularDelivery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnknownNativeStateCannotCompleteRegularDelivery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_unknown_native_state"
+ },
+ {
+ "id": "inbox/delivery_published_branch",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!Objects.equals(\"refs/heads/\"+run.branch(),run.pushedRef())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aDifferentPublishedBranchCannotAuthorizeAProposal",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aDifferentPublishedBranchCannotAuthorizeAProposal()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_published_branch"
+ },
+ {
+ "id": "inbox/delivery_finished_agent",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!\"succeeded\".equals(run.status())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.anUnfinishedAgentCannotAuthorizeAProposal",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnfinishedAgentCannotAuthorizeAProposal()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_finished_agent"
+ },
+ {
+ "id": "inbox/delivery_current_selection",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "!transitions.select(observed,item).equals(admitted.policy())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.changedLabelsRevokeDeliveryWithoutChangingThePolicyRevision",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "changedLabelsRevokeDeliveryWithoutChangingThePolicyRevision()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_current_selection"
+ },
+ {
+ "id": "inbox/delivery_current_revision",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "observed.policy().revision()!=admitted.policyRevision()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.aNewPolicyRevisionRequiresANewDecisionEvenWithTheSameSelection",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aNewPolicyRevisionRequiresANewDecisionEvenWithTheSameSelection()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "388845A2F552BCC131915F8CA9B834F5B49166F8FABB355E2A8E1CABA46F9D96",
+ "evidencePrefix": ".handoff/s8b-inbox-delivery_current_revision"
+ },
+ {
+ "id": "inbox/publication_control",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkPublicationTransport.java",
+ "before": "emitter.control(command);",
+ "after": "emitter.dispatch(command);",
+ "test": "dev.codespire.orchestrator.work.WorkPublicationTransportTest.publicationUsesOnlyTheExistingRunsControlChannel",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publicationUsesOnlyTheExistingRunsControlChannel()",
+ "failureType": "java.lang.AssertionError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "626685A4461758B1A902FC72A0D3CD483332744718E734928FCE24444F9DBE4C",
+ "evidencePrefix": ".handoff/s8b-inbox-publication_control"
+ },
+ {
+ "id": "inbox/publication_unknown_ack",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkPublicationTransport.java",
+ "before": ":new RunLaunch.Uncertain(failure)",
+ "after": ":new RunLaunch.DefiniteMiss(failure)",
+ "test": "dev.codespire.orchestrator.work.WorkPublicationTransportTest.anUnclassifiedBrokerFailureStaysUncertain",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "anUnclassifiedBrokerFailureStaysUncertain()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "626685A4461758B1A902FC72A0D3CD483332744718E734928FCE24444F9DBE4C",
+ "evidencePrefix": ".handoff/s8b-inbox-publication_unknown_ack"
+ },
+ {
+ "id": "inbox/publication_definite_miss",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkPublicationTransport.java",
+ "before": "failure instanceof BrokerAckFailure ack && !ack.mayHaveLanded()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkPublicationTransportTest.aDefiniteSerializationMissCanBeRetried",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aDefiniteSerializationMissCanBeRetried()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "626685A4461758B1A902FC72A0D3CD483332744718E734928FCE24444F9DBE4C",
+ "evidencePrefix": ".handoff/s8b-inbox-publication_definite_miss"
+ },
+ {
+ "id": "inbox/publication_lost_ack",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkPublicationTransport.java",
+ "before": "!ack.mayHaveLanded()",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkPublicationTransportTest.aLostBrokerAcknowledgementStaysUncertain",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "aLostBrokerAcknowledgementStaysUncertain()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "626685A4461758B1A902FC72A0D3CD483332744718E734928FCE24444F9DBE4C",
+ "evidencePrefix": ".handoff/s8b-inbox-publication_lost_ack"
+ },
+ {
+ "id": "inbox/cancellation_control",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkPublicationTransport.java",
+ "before": "emitter.control(new RunCommand.CancelRun",
+ "after": "emitter.dispatch(new RunCommand.CancelRun",
+ "test": "dev.codespire.orchestrator.work.WorkPublicationTransportTest.cancellationUsesTheSameControlChannel",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "cancellationUsesTheSameControlChannel()",
+ "failureType": "java.lang.AssertionError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "626685A4461758B1A902FC72A0D3CD483332744718E734928FCE24444F9DBE4C",
+ "evidencePrefix": ".handoff/s8b-inbox-cancellation_control"
+ },
+ {
+ "id": "inbox/late_negative_wall",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "public WorkProgress account(long wall,long cost,long callCount) {\n if(wall<0 || cost<0 || callCount<0)",
+ "after": "public WorkProgress account(long wall,long cost,long callCount) {\n if(cost<0 || callCount<0)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.lateUsagePreservesTheCurrentAttemptAndItsExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "lateUsagePreservesTheCurrentAttemptAndItsExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-inbox-late_negative_wall"
+ },
+ {
+ "id": "inbox/late_negative_cost",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "public WorkProgress account(long wall,long cost,long callCount) {\n if(wall<0 || cost<0 || callCount<0)",
+ "after": "public WorkProgress account(long wall,long cost,long callCount) {\n if(wall<0 || callCount<0)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.lateUsagePreservesTheCurrentAttemptAndItsExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "lateUsagePreservesTheCurrentAttemptAndItsExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-inbox-late_negative_cost"
+ },
+ {
+ "id": "inbox/late_negative_callCount",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "public WorkProgress account(long wall,long cost,long callCount) {\n if(wall<0 || cost<0 || callCount<0)",
+ "after": "public WorkProgress account(long wall,long cost,long callCount) {\n if(wall<0 || cost<0)",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.lateUsagePreservesTheCurrentAttemptAndItsExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "lateUsagePreservesTheCurrentAttemptAndItsExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-inbox-late_negative_callCount"
+ },
+ {
+ "id": "inbox/late_current_attempt",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkProgress.java",
+ "before": "return new WorkProgress(attemptId,attemptPhase,attemptState,reserved,startedAt,runs,steps,\n Math.addExact",
+ "after": "return new WorkProgress(attemptId,attemptPhase,\"completed\",false,startedAt,runs,steps,\n Math.addExact",
+ "test": "dev.codespire.contract.work.WorkExecutionTest.lateUsagePreservesTheCurrentAttemptAndItsExecution",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "lateUsagePreservesTheCurrentAttemptAndItsExecution()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "1C391FCA991169D16205621FFD99A46862BFF199AFD7731D1CA8BA894100A2B1",
+ "evidencePrefix": ".handoff/s8b-inbox-late_current_attempt"
+ },
+ {
+ "id": "redaction/worker_rotated_scrub",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "failures.ofPublication(held.execution().execution(),held.permit(),\"PUBLISHER_FAILED\",",
+ "after": "failures.of(held.execution().execution(),\"PUBLISHER_FAILED\",",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.publisherFailuresRedactTheRotatedCredentialBeforeReporting",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "publisherFailuresRedactTheRotatedCredentialBeforeReporting()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "9BCCCE625A4E7B07900033F813DEA73299E8AA99A1CE1C180052B31E0FF553A5",
+ "evidencePrefix": ".handoff/s8b-redaction-worker_rotated_scrub"
+ },
+ {
+ "id": "redaction/worker_unreadable_scrub",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "failures.ofPublication(held.execution().execution(),held.permit(),\"PUBLISHER_FAILED\",",
+ "after": "failures.of(held.execution().execution(),\"PUBLISHER_FAILED\",",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.unreadablePublicationCredentialsKeepTheErrorTextInsideRecovery",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "unreadablePublicationCredentialsKeepTheErrorTextInsideRecovery()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "9BCCCE625A4E7B07900033F813DEA73299E8AA99A1CE1C180052B31E0FF553A5",
+ "evidencePrefix": ".handoff/s8b-redaction-worker_unreadable_scrub"
+ },
+ {
+ "id": "redaction/publication_current_secret",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunFailures.java",
+ "before": "return of(command, cause, current.clean(detail));",
+ "after": "return of(command, cause, detail);",
+ "test": "dev.codespire.runworker.PublicationFailureTest.bothOldAndRotatedCredentialsAreRedactedFromPublisherFailures",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "bothOldAndRotatedCredentialsAreRedactedFromPublisherFailures()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D19E09FD4E4C55B153D57A856F2BEFFC2FF8251CEBB2857635836AA361B08F71",
+ "evidencePrefix": ".handoff/s8b-redaction-publication_current_secret"
+ },
+ {
+ "id": "redaction/publication_original_secret",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunFailures.java",
+ "before": "return of(command, cause, current.clean(detail));",
+ "after": "return new RunResult.RunFailed(command.runId(),cause,current.clean(detail),false,null);",
+ "test": "dev.codespire.runworker.PublicationFailureTest.bothOldAndRotatedCredentialsAreRedactedFromPublisherFailures",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "bothOldAndRotatedCredentialsAreRedactedFromPublisherFailures()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "D19E09FD4E4C55B153D57A856F2BEFFC2FF8251CEBB2857635836AA361B08F71",
+ "evidencePrefix": ".handoff/s8b-redaction-publication_original_secret"
+ },
+ {
+ "id": "taxonomy/publication_permit_expired",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunFailureCause.java",
+ "before": "Map.entry(\"PUBLICATION_PERMIT_EXPIRED\", GATE_REFUSED),",
+ "after": "Map.entry(\"PUBLICATION_PERMIT_EXPIRED\", UNCLASSIFIED),",
+ "test": "dev.codespire.contract.event.RunFailureCauseTest.heldPublicationRefusalsKeepThePaidBuildAndDoNotRetryTheSamePermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "heldPublicationRefusalsKeepThePaidBuildAndDoNotRetryTheSamePermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "2D8BFCAECE92CFBADB9C21F7DC119D656CAFEFABA9C1765D605426704A6C22B9",
+ "evidencePrefix": ".handoff/s8b-taxonomy-publication_permit_expired"
+ },
+ {
+ "id": "taxonomy/permitted_head_unavailable",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/event/RunFailureCause.java",
+ "before": "Map.entry(\"PERMITTED_HEAD_UNAVAILABLE\", GATE_REFUSED),",
+ "after": "Map.entry(\"PERMITTED_HEAD_UNAVAILABLE\", UNCLASSIFIED),",
+ "test": "dev.codespire.contract.event.RunFailureCauseTest.heldPublicationRefusalsKeepThePaidBuildAndDoNotRetryTheSamePermit",
+ "selectedMethod": null,
+ "testsRun": 1,
+ "failedTests": 1,
+ "failedMethod": "heldPublicationRefusalsKeepThePaidBuildAndDoNotRetryTheSamePermit()",
+ "failureType": "org.opentest4j.AssertionFailedError",
+ "mutant": "one selected assertion failure",
+ "restored": "pass",
+ "snapshotSha256": "2D8BFCAECE92CFBADB9C21F7DC119D656CAFEFABA9C1765D605426704A6C22B9",
+ "evidencePrefix": ".handoff/s8b-taxonomy-permitted_head_unavailable"
+ }
+ ]
+}
diff --git a/.claude/reviews/global/factory-m3-slice8b.md b/.claude/reviews/global/factory-m3-slice8b.md
new file mode 100644
index 00000000..5590e132
--- /dev/null
+++ b/.claude/reviews/global/factory-m3-slice8b.md
@@ -0,0 +1,181 @@
+# Factory M3 slice 8b — held publication and observed delivery
+
+Local validation and the live standalone proof pass. Slice 8a is pushed on d9861bc3 with all CI checks green. Round 12 independently
+verified criterion 1: all seven acceptance criteria are now proved. This slice's local proofs and live
+standalone `/fix` obligation are recorded separately below.
+
+## Execution and publication
+
+`ExecuteWorkRun` is a distinct wire command containing the original M2 execution and the immutable
+item/generation/build-attempt/preparation binding. The worker commits this encrypted command and
+the shared M2 execution claim before acknowledging it. An older worker cannot ignore an optional
+hold flag and execute an item run automatically. Ordinary `ExecuteRun`, including standalone FIX
+source-branch execution, retains automatic publication.
+
+The initial publisher has a separate held entry point. It gates and checkpoints the actual built
+head without pushing. Trusted daemon labels mark every run resource before creation, including
+partial init failures. Runtime destruction and orphan recovery respect that hold. Active compute
+stops while the workspace, original topology and encrypted result outbox remain durable.
+
+`RunWorkReady` reports the actual full head, changed paths, measured active wall time and measured
+or unknown usage. It completes BUILD and releases the item's active reservation. Production has
+no M4 verifier: the next phase records `verify_capability_unavailable`. No successful verification,
+review, CI run or merge is fabricated.
+
+A short-lived `PublishWorkRun` control command binds the retained work, completed verification,
+delivery attempt and exact head. The delivery claim checks current source/actor/profile/ceiling,
+artifact versions, the original phase admission and the selected FACTORY identity. The encrypted
+permit commits before the broker write. Uncertain acknowledgement never resends execution or an
+unclaimed publisher; definite serialization misses may re-arm the same delivery effect.
+
+The worker resumes only the trusted publisher, using fresh SCM credentials, original topology and
+both original and current protected paths. The original harness credential is deliberately invalid
+in the credential-boundary test. The publisher checks its exact head and lease window before the
+write. Cancellation is checked before creation, before start and after start; an already observed
+push remains reported even if cancellation races it. Unobserved publication stays recoverable.
+
+## Delivery and review
+
+After actual publication, delivery commits a proposal claim before its external write. Recovery
+of an ambiguous POST only reads by both branches. A compare-and-set prevents a stale publication
+reader from changing `proposing` back to `pushed` and posting twice. Completed delivery recovery
+also checks the exact completed attempt, item, generation and phase.
+
+The sink contract carries requested and observed native draft state. GitHub and Bitbucket send
+the documented boolean; GitLab uses its documented native `Draft:` title. Missing draft state
+remains unknown. A regular or unknown existing request cannot masquerade as the requested draft.
+Unsupported native draft capability blocks delivery visibly. Official contracts checked 2026-09-14:
+[GitHub](https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request),
+[Bitbucket](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/),
+[GitLab draft semantics](https://docs.gitlab.com/user/project/merge_requests/drafts/) and
+[GitLab API](https://docs.gitlab.com/api/merge_requests/).
+
+Delivery tests identify their TEST-only prior verification driver. They prove native draft versus
+regular control-plane behavior, not a shipped verifier. REVIEW observes the existing reviewer for
+this repository and pull request, the exact reviewed and posted head, completed non-degraded
+results, an open unarchived request, readable findings/reconciliation and no open blockers. Missing
+or unreadable evidence stays waiting. Successful observation reaches unavailable LAND capability;
+there is no synthetic CI or merge success.
+
+## Recovery and paid usage
+
+Worker readiness and terminal publication have independent encrypted result slots and independent
+acknowledgements. The orchestrator validates the immutable work binding before projecting or
+charging readiness. It retains readiness metadata even when a terminal publication arrived first,
+without reopening the terminal run. Readiness completes BUILD once; publication cannot complete
+BUILD or add its usage again. Both results use the existing M2 call identity.
+
+A stopped or superseded build still bought usage. Late recovery records it once without completing
+the current attempt, changing its phase or erasing its reservation and execution evidence. Unknown
+usage remains unknown. Tests replay the inbox after aggregate commit but before acknowledgement.
+
+`WorkItemRunJourneyIT.preparedItemBuildsAndWaitsForVerification` uses real worker containers and a
+real local smart-HTTP origin, plus an isolated orchestrator TEST JVM and provider HTTP fixture.
+The actual result saga consumes readiness, records one build charge and exposes the checkpoint.
+VERIFY remains unavailable, no PR exists and the remote branch is absent. The only execution
+substitutions are the local origin/image/SCM and a bounded TEST wall clock.
+
+`WorkRunProcessRecoveryIT` uses three real worker JVMs and the same isolated database/runtime.
+The parent kills the first after durable readiness, then observes a new owner preserving the held
+workspace without rebuilding. It kills the second in each of two separate windows: after durable
+publication claim but before publisher IO, and after the real remote push but before terminal
+commit. A third owner recovers the original permit and publisher. Both cases assert the remote's
+exact head, `TEST-build-count` of one, no new harness execution, acknowledged results and cleanup.
+Only the exact dead TEST lease is backdated to avoid waiting a production minute.
+
+The separate charge test sends actual worker readiness and terminal results into another JVM with
+the production charge ledger and full production Flyway schema. Skipping readiness loses its
+measured tokens; changing the final call identity produces two ledger rows. Each mutation is
+isolated and restored. A fixture flag is not used as evidence of process death or remote writing.
+
+## Discriminating fixtures and defects found
+
+- The initial-hold mutation changes the actual remote head and fails the named delivery-off test.
+- A runtime wrong-run mutation initially reached an unrelated mount guard. Its assertion now names
+ the wrong-run error, so that second guard cannot count as evidence.
+- The unobserved-agent fixture initially had no pushed branch. It now supplies an actual observed
+ push with an unobserved agent, distinguishing that guard from the empty-result branch.
+- The reviewed-head mutation survived twice: first both reviewed and posted heads were wrong, then
+ a corrective `recordPosted` call correctly refused a mismatched commit. The final fixture changes
+ only the reviewed head with exact TEST SQL and asserts both fields before exercising the guard.
+- Terminal-before-ready delivery exposed missing checkpoint metadata; its metadata update now
+ preserves both the terminal status and independently bound build evidence.
+- Late readiness on a stopped or readmitted item exposed endless inbox recovery and lost spend.
+ A separately deduplicated accounting decision preserves the newer attempt and acknowledges it.
+- A stale publication reader could rewind a committed proposal claim. A two-thread test now pauses
+ that reader while another caller commits a POST with an ambiguous response; resumption performs
+ neither another POST nor another forge read.
+- Initial child-JVM failures were TEST bootstrap/profile and JSON discriminator issues. They were
+ corrected before claiming the real process proofs. No unexplained container deletion occurred.
+- Pinned Semgrep found four test process launchers with dynamic executable expressions. They now
+ name literal `git` or `java` executables; the worker test task pins PATH to its selected JDK, and
+ arguments remain separate process arguments. No shell or scanner suppression was introduced.
+ The final changed-file scan, including the later taxonomy fix, reports zero findings across
+ 98 files. Its only partial-parser warning has the same four pre-existing `api.ts` spans.
+- Publisher failure redaction originally used only build credentials even when publication used
+ a rotated SCM identity. It now redacts the current identity before the existing original-secret
+ scrub. An unreadable current credential leaves the publisher recoverable without emitting its
+ error text. Real Tink tests cover raw and Basic forms; worker tests distinguish old and new secrets.
+- The missing-checkpoint mutation originally caused `NoSuchElementException` in its fixture.
+ That was rejected as evidence. The case now asserts the exact optional checkpoint value; the
+ same production mutation produces one assertion failure and the scratch-restored case passes.
+- Delivery selection and policy revision have separate fixtures: changed labels preserve the
+ revision, while an unused mapping changes revision with identical selection. Both retain an
+ enabled source and allowed actor, so neither half can hide behind the other policy guard.
+- The first forced fast suite found two publisher causes missing from the shared vocabulary:
+ expired publication permits and unavailable permitted heads became `UNCLASSIFIED`. Both now
+ map to the existing non-retryable `GATE_REFUSED` category, which preserves paid-build accounting.
+ The producer-derived inventory caught this; its assertion was retained. A focused test checks
+ the category, retry answer and spend classification for both causes.
+
+## Validation and live obligation
+
+Forced `testFast` and `testServices` passed sequentially in 3m13s and 25m43s. Fresh archived JUnit
+reports contain 3970 tests across 444 suites and 30 modules, zero failures/errors and one existing
+Windows symlink privilege skip. This includes both process-death windows, late accounting and the
+stale proposal race. The complete UI passes 714 tests across
+88 files; all 40 route cases pass shuffled seeds 814, 2718 and 5192, and the production UI builds.
+Thirteen UI production mutations, including the required identical-journey-label replay, pass
+their isolated fail/restore checks. The final evidence audit accepts 248 checks covering 245
+distinct production mutations: 235 Java checks and 13 UI checks. Each has exactly one selected
+assertion failure, byte-identical scratch restoration and a passing restored case; the audit
+also verifies its final production anchor. All 48 final inbox/delivery mutations passed without
+a survivor or unexpected failure. The expanded inbox baseline has 36 cases and final delivery
+baseline has 24, all passing. Pinned Semgrep reports zero findings across 98 files, with all final
+file hashes matching the scan capture. Packaging passed in 47 seconds.
+
+The user started the live worker only after the final Docker service tier exited. The live
+orchestrator ran the tested source (matching `RunResultSaga` and `RunFailureCause` hashes).
+The real standalone proof is [TEST PR #32](https://github.com/artyomsv/spire-test/pull/32):
+
+- Initial head: `5c9eac3195308ec61b180ec67c2a1356e5606240`, the unchanged invoice example from
+ the existing test branch. Its actual initial review posted finding `4003204361` about missing
+ `customerId` validation, with zero prior fix runs.
+- [Command `4003211987`](https://github.com/artyomsv/spire-test/pull/32#discussion_r4003211987)
+ dispatched `run::github:artyomsv/spire-test:4003204361:1` at 08:00:13 UTC on 2026-09-14.
+- The real worker/publisher completed successfully at 08:01:00 UTC and automatically pushed
+ `refs/heads/TEST-s8b-standalone-fix-20260914`, commit
+ `264ff858b538a3c779cf94161d3bc601bcfcf69a`. The change adds the null check before invoice lookup.
+ The persisted run has kind `FIX`; its work-run effect count is zero. No work-item permit was used.
+- The next review completed on that exact new head. PostgreSQL `review_finding` row 157 records
+ `RESOLVED` for thread `4003204361`; the API reconciliation records `resolvedThread=true` and
+ GitHub thread `PRRT_kwDOTQMWo86iB31I` is resolved. Other invoice findings remain outside this task.
+- Cleanup closed PR #32 without merging and issued exactly
+ `DELETE /repos/artyomsv/spire-test/git/refs/heads/TEST-s8b-standalone-fix-20260914`.
+ Review/run history is retained as the live evidence. The user was notified to stop the worker.
+
+Live preflight exposed legacy configuration/data limits without bypassing them. The GitHub hook
+`26c8a671-6dcb-4cc7-a71a-a1184fcf0162` lacked its forge origin; its revision-7 gateway API repair
+selects the existing repository `4bc898a4-7dbc-4167-885a-fd354ddbb8bb` and `https://api.github.com`,
+preserving its secret and roles. The original GitHub event was redelivered. PR #26 then correctly
+refused because its old finding row has no thread reference; a normal fresh review confirmed the
+bug but retained that legacy row. PR #27 correctly refused unknown historical fork metadata.
+Neither attempt dispatched a run or consumed a fix slot. The announced TEST PR avoided both old
+data gaps; no cap, refusal guard or historical finding row was changed. PR #31's cap was preserved.
+
+All automated fixtures use isolated Testcontainers and TEST-prefixed identities. Cleanup binds
+owned IDs; worker cleanup deletes only its claimed TEST execution/lease/work rows and held units.
+The charge proof creates and drops only its generated TEST schema. Orchestrator cleanup deletes
+the exact delivery/run effects before their phase/item rows and then the fixture's profiles,
+accounts and repository bindings. No second backup or direct dev-database fixture insert/delete
+was performed; the live TEST review and run were created through the actual application path.
diff --git a/.claude/reviews/global/factory-m3-slice9-mutations.json b/.claude/reviews/global/factory-m3-slice9-mutations.json
new file mode 100644
index 00000000..c7a3531f
--- /dev/null
+++ b/.claude/reviews/global/factory-m3-slice9-mutations.json
@@ -0,0 +1,869 @@
+{
+ "summary": {
+ "checks": 78,
+ "distinctProductionMutations": 78,
+ "javaMain": 72,
+ "ui": 6
+ },
+ "method": "Mutate production code, require exactly one isolated assertion failure, restore byte-identical scratch content, and rerun the selected case green. Final production anchors and hashes are checked again. No fixture is mutated.",
+ "checks": [
+ {
+ "id": "core/ordinary_words",
+ "file": "spire-worksource/src/main/java/dev/codespire/worksource/WorkSourceActivity.java",
+ "before": "^/(approve|reject)",
+ "after": "^(approve|reject)",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.ordinaryApprovingWordsCannotAnswerAnOtherwiseEligibleGate",
+ "module": "spire-orchestrator",
+ "sourceSha256": "61cf764e45c63d830480e4dcf2b9da7891867eaec0c847d476ccd0181f0c4ab0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "core/stale_review_head",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "!Objects.equals(review.head(),pr.headCommit())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.approvalOnOldHeadCannotResolveGateBoundToCurrentHead",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "core/dismissed_review",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "current && open && \"APPROVED\".equals(review.path(\"state\").asText())",
+ "after": "current && open",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.dismissedReviewCannotResolveCurrentHeadGate",
+ "module": "spire-orchestrator",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "core/recorded_factory_id",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkControl.java",
+ "before": "actor.equals(factoryActor)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.recordedMachineIdSurvivesRenameAndAccountRotation",
+ "module": "spire-orchestrator",
+ "sourceSha256": "45c9607038f0ab0efe3fe09a6a1ca5f704680e01a79c3a40d4ae7fce8cd2e702",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "core/durable_publication_hold",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunStore.java",
+ "before": "AND NOT EXISTS (SELECT 1 FROM runworker.work_publication_revocation h WHERE h.run_id=work_run.run_id AND h.binding=work_run.binding)",
+ "after": "AND true",
+ "test": "dev.codespire.runworker.WorkRunProcessRecoveryIT.takeoverRevocationSurvivesKilledJvmWithoutAnM1CancelClaim",
+ "module": "spire-run-worker",
+ "sourceSha256": "33d2d4c51873bf9d5a603111eb7a9841566c12db09cc032881639098051b9400",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_human",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "!review.human()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.aBotTypedReviewCannotResolve",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_actor",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "!Objects.equals(review.actorId(),activity.actorId())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.approvalFromAnotherWebhookActorCannotResolve",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_closed",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "\"open\".equals(pull.path(\"state\").asText())",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.closedPullRequestCannotResolve",
+ "module": "spire-orchestrator",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/recorded_reviewer",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkControl.java",
+ "before": "actor.equals(reviewerActor)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.recordedReviewerDoesNotTakeOver",
+ "module": "spire-orchestrator",
+ "sourceSha256": "45c9607038f0ab0efe3fe09a6a1ca5f704680e01a79c3a40d4ae7fce8cd2e702",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/recorded_tracker",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkControl.java",
+ "before": "actor.equals(trackerActor)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.recordedTrackerIdentityDoesNotTakeOverAfterRotation",
+ "module": "spire-orchestrator",
+ "sourceSha256": "45c9607038f0ab0efe3fe09a6a1ca5f704680e01a79c3a40d4ae7fce8cd2e702",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/tracker_namespace",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemControl.java",
+ "before": "item.control().trackerMachine(actor)",
+ "after": "item.control().machine(actor)",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.scmIdentityCannotImpersonateTheRecordedTrackerIdentity",
+ "module": "spire-orchestrator",
+ "sourceSha256": "f2784277ff2f960a413ff225e6e6e9bd0cb8c6003a2e73af0c9c21a5b880c201",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/linked_branch",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "(\"refs/heads/\"+item.control().branch()).equals(activity.branch());\n }",
+ "after": "true;\n }",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.unrelatedBranchCannotSuspendItem",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/linked_pr",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "execution.pullRequest().number()!=activity.prId()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.unrelatedPullRequestCannotSuspendItem",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/linked_repository",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "!source.repository().equals(activity.repo())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.unrelatedRepositoryCannotSuspendItem",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/fix_precedence",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "activity.fixCommand()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.authorizedFixTakesPrecedenceOverCommentTakeover",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/tracker_artifact",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "Objects.equals(gate.artifact(),answer.artifact())",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.anotherArtifactCannotAnswer",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/resume_note",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemControl.java",
+ "before": "note==null || note.isBlank()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.resumeRequiresOperatorNote",
+ "module": "spire-orchestrator",
+ "sourceSha256": "f2784277ff2f960a413ff225e6e6e9bd0cb8c6003a2e73af0c9c21a5b880c201",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/resume_subject",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemControl.java",
+ "before": "subject==null || subject.isBlank()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.resumeRequiresVerifiedOperator",
+ "module": "spire-orchestrator",
+ "sourceSha256": "f2784277ff2f960a413ff225e6e6e9bd0cb8c6003a2e73af0c9c21a5b880c201",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/resume_version",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "if(history.size()!=expected)",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.explicitOperatorResumeRequiresVersionAndRefetchesHead",
+ "module": "spire-orchestrator",
+ "sourceSha256": "5f43c4a93ea51ac3f0bef9febc69631b42a87c44d868318589f8a564748fa064",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/poll_admission_time",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityPoller.java",
+ "before": "activity.occurredAt().isBefore(history.getFirst().occurredAt())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkActivityPollerIT.commentBeforeAdmissionCannotAnswerCurrentGate",
+ "module": "spire-orchestrator",
+ "sourceSha256": "ce8a1620207223b75526c88472c2a55648bce4b87e6d3f2330f6f426c3f05a01",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/poll_timestamp",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityPoller.java",
+ "before": "activity.occurredAt()==null || activity.occurredAt().isBefore(history.getFirst().occurredAt())",
+ "after": "activity.occurredAt()!=null && activity.occurredAt().isBefore(history.getFirst().occurredAt())",
+ "test": "dev.codespire.orchestrator.work.WorkActivityPollerIT.undatedCommentCannotAnswerCurrentGate",
+ "module": "spire-orchestrator",
+ "sourceSha256": "ce8a1620207223b75526c88472c2a55648bce4b87e6d3f2330f6f426c3f05a01",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/poll_supported",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityPoller.java",
+ "before": "if(!client.pollsActivities())return;",
+ "after": "if(false)return;",
+ "test": "dev.codespire.orchestrator.work.WorkActivityPollerIT.unsupportedPollingNeverReadsComments",
+ "module": "spire-orchestrator",
+ "sourceSha256": "ce8a1620207223b75526c88472c2a55648bce4b87e6d3f2330f6f426c3f05a01",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/poll_cursor",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityPoller.java",
+ "before": "if(!seen.add(cursor))",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkActivityPollerIT.repeatedCursorFailsVisiblyInsteadOfSpinning",
+ "module": "spire-orchestrator",
+ "sourceSha256": "ce8a1620207223b75526c88472c2a55648bce4b87e6d3f2330f6f426c3f05a01",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/github_sender",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubIngress.java",
+ "before": "root.path(\"sender\").path(\"id\").asText(null)",
+ "after": "root.path(\"pusher\").path(\"name\").asText(null)",
+ "test": "dev.codespire.scm.github.GitHubIngressActivityTest.pushUsesTransportStableIdentityInsteadOfCommitAuthor",
+ "module": "spire-scm-github",
+ "sourceSha256": "da358f08564c789a7a9b2a63f210cce8219af6823fd57c0a7b9ae5d603e38c8e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/github_review_comment",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubIngress.java",
+ "before": "\"commented\".equalsIgnoreCase(nativeReview.path(\"state\").asText())",
+ "after": "false",
+ "test": "dev.codespire.scm.github.GitHubIngressActivityTest.ordinaryReviewCommentIsHumanActivityNotAnApproval",
+ "module": "spire-scm-github",
+ "sourceSha256": "da358f08564c789a7a9b2a63f210cce8219af6823fd57c0a7b9ae5d603e38c8e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/github_review_action",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubIngress.java",
+ "before": "if(!java.util.Set.of(\"submitted\",\"dismissed\").contains(root.path(\"action\").asText()))return List.of();",
+ "after": "if(false)return List.of();",
+ "test": "dev.codespire.scm.github.GitHubIngressActivityTest.editedReviewIsNotANewApproval",
+ "module": "spire-scm-github",
+ "sourceSha256": "da358f08564c789a7a9b2a63f210cce8219af6823fd57c0a7b9ae5d603e38c8e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_sender",
+ "file": "spire-scm-gitlab/src/main/java/dev/codespire/scm/gitlab/GitLabIngress.java",
+ "before": "root.path(\"user_id\").asText(null)",
+ "after": "root.path(\"user_name\").asText(null)",
+ "test": "dev.codespire.scm.gitlab.GitLabIngressActivityTest.pushUsesTransportStableIdentityInsteadOfCommitAuthor",
+ "module": "spire-scm-gitlab",
+ "sourceSha256": "4418272ac61639af4f660e97430c54c9bbe07c82097c1000a6629ffcd1e54fed",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_system_note",
+ "file": "spire-scm-gitlab/src/main/java/dev/codespire/scm/gitlab/GitLabIngress.java",
+ "before": "attributes.path(\"system\").asBoolean(false)",
+ "after": "false",
+ "test": "dev.codespire.scm.gitlab.GitLabIngressActivityTest.systemNoteIsNotHumanTakeover",
+ "module": "spire-scm-gitlab",
+ "sourceSha256": "4418272ac61639af4f660e97430c54c9bbe07c82097c1000a6629ffcd1e54fed",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_fork_note",
+ "file": "spire-scm-gitlab/src/main/java/dev/codespire/scm/gitlab/GitLabIngress.java",
+ "before": "var mr=root.path(\"merge_request\");if(fromFork(mr))return List.of();",
+ "after": "var mr=root.path(\"merge_request\");if(false)return List.of();",
+ "test": "dev.codespire.scm.gitlab.GitLabIngressActivityTest.forkNoteCannotClaimTheLocalBranch",
+ "module": "spire-scm-gitlab",
+ "sourceSha256": "4418272ac61639af4f660e97430c54c9bbe07c82097c1000a6629ffcd1e54fed",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/bitbucket_actor",
+ "file": "spire-scm-bitbucket/src/main/java/dev/codespire/scm/bitbucket/BitbucketCloudIngress.java",
+ "before": "root.path(\"actor\").path(\"uuid\").asText(null)",
+ "after": "root.path(\"actor\").path(\"display_name\").asText(null)",
+ "test": "dev.codespire.scm.bitbucket.BitbucketCloudIngressActivityTest.pushUsesTransportStableIdentityInsteadOfCommitAuthor",
+ "module": "spire-scm-bitbucket",
+ "sourceSha256": "14c182ae41e1a7128569e1eb8bb629271e256102142c4f49cc3bec361126c19e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/bitbucket_tag",
+ "file": "spire-scm-bitbucket/src/main/java/dev/codespire/scm/bitbucket/BitbucketCloudIngress.java",
+ "before": "if(!\"branch\".equals(branch.path(\"type\").asText()))continue;",
+ "after": "if(false)continue;",
+ "test": "dev.codespire.scm.bitbucket.BitbucketCloudIngressActivityTest.aTagIsNotTheLinkedBranch",
+ "module": "spire-scm-bitbucket",
+ "sourceSha256": "14c182ae41e1a7128569e1eb8bb629271e256102142c4f49cc3bec361126c19e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/bitbucket_fork",
+ "file": "spire-scm-bitbucket/src/main/java/dev/codespire/scm/bitbucket/BitbucketCloudIngress.java",
+ "before": "if(!root.path(\"repository\").path(\"uuid\").asText(\"\").equals(source.path(\"repository\").path(\"uuid\").asText(\"\")))return List.of();",
+ "after": "if(false)return List.of();",
+ "test": "dev.codespire.scm.bitbucket.BitbucketCloudIngressActivityTest.forkCommentCannotClaimTheLocalBranch",
+ "module": "spire-scm-bitbucket",
+ "sourceSha256": "14c182ae41e1a7128569e1eb8bb629271e256102142c4f49cc3bec361126c19e",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/initial_identity_preserved",
+ "file": "spire-contract/src/main/java/dev/codespire/contract/work/WorkItemLifecycle.java",
+ "before": ".controlled(previous==null?null:previous.control())",
+ "after": ".controlled(null)",
+ "test": "dev.codespire.contract.work.WorkItemLifecycleTest.policyObservationPreservesTheRecordedTrackerIdentityBeforeAnyPhaseStarts",
+ "module": "spire-contract",
+ "sourceSha256": "8222e7206239b9e78284b2ba1a51c577418fbb3c963c37e526574c42ac43ede0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/factory_activity_route",
+ "file": "spire-gateway/src/main/java/dev/codespire/gateway/RegistryWebhookEdge.java",
+ "before": "repo.eventKind()==dev.codespire.contract.event.RepositoryEventKind.FACTORY?ingress.activity(raw):ingress.translate(raw)",
+ "after": "ingress.translate(raw)",
+ "test": "dev.codespire.gateway.registry.RepositoryWebhookKindsTest.preservesLegacyKeyAndSeparatesFactoryActivityFromReviewerCommands",
+ "module": "spire-gateway",
+ "sourceSha256": "dc7dcd112b9a3201bbab6929f91cd2a5faaf6a005eae288692e18310f1ecc080",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/hold_control_route",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/RunControlListener.java",
+ "before": "workRuns.hold(hold);",
+ "after": "/* TEST mutant drops control delivery. */",
+ "test": "dev.codespire.runworker.RunControlListenerTest.heldWorkControlReachesDurableRevocationInsteadOfM1Cancel",
+ "module": "spire-run-worker",
+ "sourceSha256": "05033cdf108ab5b39b6ef0656d715074126e68f7295dfa842b8ec7ea7c0495ff",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_issue_system",
+ "file": "spire-worksource-gitlab/src/main/java/dev/codespire/worksource/gitlab/GitLabWorkIngress.java",
+ "before": "attributes.path(\"system\").asBoolean(false)",
+ "after": "false",
+ "test": "dev.codespire.worksource.gitlab.GitLabWorkIngressTest.systemIssueNoteIsNotHumanActivity",
+ "module": "spire-worksource-gitlab",
+ "sourceSha256": "545a1fee0d6c00462e200d0bb36620e318c3ce529017da2604c8e4fccb521e2a",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_issue_edit",
+ "file": "spire-worksource-gitlab/src/main/java/dev/codespire/worksource/gitlab/GitLabWorkIngress.java",
+ "before": "!\"create\".equals(attributes.path(\"action\").asText(\"create\"))",
+ "after": "false",
+ "test": "dev.codespire.worksource.gitlab.GitLabWorkIngressTest.editedIssueNoteCannotBecomeANewGateAnswer",
+ "module": "spire-worksource-gitlab",
+ "sourceSha256": "545a1fee0d6c00462e200d0bb36620e318c3ce529017da2604c8e4fccb521e2a",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/hold_outbox",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemStore.java",
+ "before": "WHERE work_item_id=? AND run_id IS NOT NULL AND preparation_binding IS NOT NULL ON CONFLICT DO NOTHING",
+ "after": "WHERE work_item_id=? AND false AND run_id IS NOT NULL AND preparation_binding IS NOT NULL ON CONFLICT DO NOTHING",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.takeoverCancelsPendingDeliveryAndDurablyRequestsTheExactRunHold",
+ "module": "spire-orchestrator",
+ "sourceSha256": "97695d3cd2dd218714e11556254706fe0797603c1ba39793a4d12fdab382e956",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/registered_activity",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityConsumer.java",
+ "before": "delivery.registrationId()==null",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.activityWithoutGatewayRegistrationCannotTakeOver",
+ "module": "spire-orchestrator",
+ "sourceSha256": "d658046ca1d4f7011e06b3cc3c056542e3ce82fca41e880098bb8022dccda684",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/factory_envelope",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityConsumer.java",
+ "before": "delivery.eventKind()!=RepositoryEventKind.FACTORY",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.reviewerEnvelopeCannotSupplyFactoryActivity",
+ "module": "spire-orchestrator",
+ "sourceSha256": "d658046ca1d4f7011e06b3cc3c056542e3ce82fca41e880098bb8022dccda684",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/repository_envelope",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityConsumer.java",
+ "before": "!repository.id().equals(delivery.repositoryId())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.envelopeCannotSubstituteAnotherRepositoryIdentity",
+ "module": "spire-orchestrator",
+ "sourceSha256": "d658046ca1d4f7011e06b3cc3c056542e3ce82fca41e880098bb8022dccda684",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/repository_enabled",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkActivityConsumer.java",
+ "before": "!repository.enabled()",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.disabledRepositoryDoesNotAcceptActivity",
+ "module": "spire-orchestrator",
+ "sourceSha256": "d658046ca1d4f7011e06b3cc3c056542e3ce82fca41e880098bb8022dccda684",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/approval_measured",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FixPermissionService.java",
+ "before": "return authorize(repositoryId,actorId,true);",
+ "after": "return authorize(repositoryId,actorId,false);",
+ "test": "dev.codespire.orchestrator.factory.FixPermissionServiceTest.nativeApprovalRequiresMeasuredPushEvenWithAnAllowOverride",
+ "module": "spire-orchestrator",
+ "sourceSha256": "212e07aa87fd04fead838a4910ebe934904dfcd492dccabf6652e48b19991592",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/approval_deny",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/factory/FixPermissionService.java",
+ "before": "override==FixAuthorization.Override.DENY",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.factory.FixPermissionServiceTest.nativeApprovalStillHonorsDenyDespiteMeasuredPush",
+ "module": "spire-orchestrator",
+ "sourceSha256": "212e07aa87fd04fead838a4910ebe934904dfcd492dccabf6652e48b19991592",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/github_resume_branch",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubDiffSource.java",
+ "before": "!branch.equals(row.path(\"name\").asText())",
+ "after": "false",
+ "test": "dev.codespire.scm.github.GitHubApiTest.anotherBranchCannotSupplyTheResumeHead",
+ "module": "spire-scm-github",
+ "sourceSha256": "bb62a7d31e988dcfc47e06abcb15cdce04e7f879a361d31b180f813a8fd15cc9",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/github_resume_commit",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubDiffSource.java",
+ "before": "!head.matches(\"[0-9a-f]{40}|[0-9a-f]{64}\")",
+ "after": "false",
+ "test": "dev.codespire.scm.github.GitHubApiTest.malformedCommitCannotSupplyTheResumeHead",
+ "module": "spire-scm-github",
+ "sourceSha256": "bb62a7d31e988dcfc47e06abcb15cdce04e7f879a361d31b180f813a8fd15cc9",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_resume_branch",
+ "file": "spire-scm-gitlab/src/main/java/dev/codespire/scm/gitlab/GitLabDiffSource.java",
+ "before": "!branch.equals(row.path(\"name\").asText())",
+ "after": "false",
+ "test": "dev.codespire.scm.gitlab.GitLabApiTest.anotherBranchCannotSupplyTheResumeHead",
+ "module": "spire-scm-gitlab",
+ "sourceSha256": "7a2641a01c5f7abaa85752f20cad6cf67501730e12561e74aaa8500e380c7917",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gitlab_resume_commit",
+ "file": "spire-scm-gitlab/src/main/java/dev/codespire/scm/gitlab/GitLabDiffSource.java",
+ "before": "!head.matches(\"[0-9a-f]{40}|[0-9a-f]{64}\")",
+ "after": "false",
+ "test": "dev.codespire.scm.gitlab.GitLabApiTest.malformedCommitCannotSupplyTheResumeHead",
+ "module": "spire-scm-gitlab",
+ "sourceSha256": "7a2641a01c5f7abaa85752f20cad6cf67501730e12561e74aaa8500e380c7917",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/bitbucket_resume_branch",
+ "file": "spire-scm-bitbucket/src/main/java/dev/codespire/scm/bitbucket/BitbucketCloudDiffSource.java",
+ "before": "!branch.equals(row.path(\"name\").asText())",
+ "after": "false",
+ "test": "dev.codespire.scm.bitbucket.BitbucketCloudApiTest.anotherBranchCannotSupplyTheResumeHead",
+ "module": "spire-scm-bitbucket",
+ "sourceSha256": "729ab63268faa927fdc1a38a2c57e66c35e6231001476814d1473e1d04ac0b36",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/bitbucket_resume_commit",
+ "file": "spire-scm-bitbucket/src/main/java/dev/codespire/scm/bitbucket/BitbucketCloudDiffSource.java",
+ "before": "!head.matches(\"[0-9a-f]{40}|[0-9a-f]{64}\")",
+ "after": "false",
+ "test": "dev.codespire.scm.bitbucket.BitbucketCloudApiTest.malformedCommitCannotSupplyTheResumeHead",
+ "module": "spire-scm-bitbucket",
+ "sourceSha256": "729ab63268faa927fdc1a38a2c57e66c35e6231001476814d1473e1d04ac0b36",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/latest_review_state",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "complete && latestApproved && reviewId.equals(latest)",
+ "after": "complete && reviewId.equals(latest)",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.latestListedStateMustStillBeApprovedEvenWhenTheNamedReadIsOlder",
+ "module": "spire-orchestrator",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/latest_review_identity",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "complete && latestApproved && reviewId.equals(latest)",
+ "after": "complete && latestApproved",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.anotherLatestApprovalDoesNotAuthorizeTheNamedOlderReview",
+ "module": "spire-orchestrator",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/approval_land_only",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "command.channel()==ResolveGate.Channel.PR_REVIEW && !\"land\".equals(gate.phase())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.prReviewCommandCannotAnswerAPlanGate",
+ "module": "spire-orchestrator",
+ "sourceSha256": "5f43c4a93ea51ac3f0bef9febc69631b42a87c44d868318589f8a564748fa064",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/tracker_current_membership",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemTransitions.java",
+ "before": "command.channel()==ResolveGate.Channel.TRACKER && !observed.source().allowedActors().contains(resolver)",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.aggregateRechecksTrackerMembershipForARecognizedCommand",
+ "module": "spire-orchestrator",
+ "sourceSha256": "5f43c4a93ea51ac3f0bef9febc69631b42a87c44d868318589f8a564748fa064",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/tracker_generation",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "gate.generation()==answer.generation()",
+ "after": "true",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.staleGenerationCannotAnswer",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/dispatch_factory_identity",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkRunDispatcher.java",
+ "before": "factoryActor=rs.getString(2)",
+ "after": "factoryActor=null",
+ "test": "dev.codespire.orchestrator.work.WorkRunDispatchTest.dispatchRecordsServingIdentityAndPreservesTrackerIdentityFromAdmission",
+ "module": "spire-orchestrator",
+ "sourceSha256": "39cc6038319c8c0791bd36f98eacd3fbc966353d76d3cdb67290cffa520ffc4d",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_named_id",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "!reviewId.equals(review.path(\"id\").asText())",
+ "after": "false",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.namedReviewIdentityMustMatch",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_stable_id",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "!actor.matches(\"[1-9][0-9]*\")",
+ "after": "false",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.aDisplayNameCannotReplaceStableReviewActor",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_merged",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "!pull.path(\"merged\").asBoolean(true)",
+ "after": "true",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.mergedPullCannotBeApproved",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_pr_id",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "pull.path(\"number\").asLong()==pullRequest",
+ "after": "true",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.anotherPullCannotBeApproved",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_history_array",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "if(!rows.isArray())",
+ "after": "if(false)",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.malformedHistoryCannotProveAnApproval",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_actor_history",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "actor.equals(row.path(\"user\").path(\"id\").asText())",
+ "after": "true",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.anotherActorsLatestReviewCannotDismissThisActorsApproval",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_decisive_history",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "java.util.Set.of(\"APPROVED\",\"CHANGES_REQUESTED\",\"DISMISSED\").contains(row.path(\"state\").asText())",
+ "after": "true",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.commentOnlyReviewDoesNotReplaceADecisiveApproval",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/review_history_complete",
+ "file": "spire-scm-github/src/main/java/dev/codespire/scm/github/GitHubPullRequestApprovalSource.java",
+ "before": "complete && latestApproved && reviewId.equals(latest)",
+ "after": "latestApproved && reviewId.equals(latest)",
+ "test": "dev.codespire.scm.github.GitHubPullRequestApprovalSourceTest.aFullLastPageCannotProveHistoryComplete",
+ "module": "spire-scm-github",
+ "sourceSha256": "a3bf7f693f0ce116b878294bea3084bb6c7943be5a13de46475387e5f09aaaab",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/hold_before_build",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "claims.taken(id,RunDispatcher.CANCEL_SLOT) || store.revoked(command)",
+ "after": "claims.taken(id,RunDispatcher.CANCEL_SLOT)",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.durableHoldBeforeBuildBuysNoHarnessCallWithoutM1Cancel",
+ "module": "spire-run-worker",
+ "sourceSha256": "2029e5db819404f7e4f7734285578472060b0e0923dc0d289aae4b7e9460c357",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/hold_publisher_start",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "!cancelled(command.runId()) && !store.revoked(command)",
+ "after": "!cancelled(command.runId())",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.durableHoldIsCheckedAgainAtPublisherStart",
+ "module": "spire-run-worker",
+ "sourceSha256": "2029e5db819404f7e4f7734285578472060b0e0923dc0d289aae4b7e9460c357",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/hold_ready_recovery",
+ "file": "spire-run-worker/src/main/java/dev/codespire/runworker/WorkRunWorker.java",
+ "before": "else if(\"ready\".equals(held.state()) && (cancelled(id) || store.revoked(held.execution())))",
+ "after": "else if(\"ready\".equals(held.state()) && cancelled(id))",
+ "test": "dev.codespire.runworker.WorkRunWorkerTest.durableHoldRecoversReadyRunWithoutM1Cancel",
+ "module": "spire-run-worker",
+ "sourceSha256": "2029e5db819404f7e4f7734285578472060b0e0923dc0d289aae4b7e9460c357",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/retry_branch_delegate",
+ "file": "spire-review-worker/src/main/java/dev/codespire/worker/adapters/RetryingDiffSource.java",
+ "before": "return withRetry(\"fetchBranchHead\", () -> delegate.fetchBranchHead(repo, branch));",
+ "after": "return null;",
+ "test": "dev.codespire.worker.adapters.RetryingDiffSourceTest.branchHeadDelegationPreservesCoordinatesResultAndRetry",
+ "module": "spire-review-worker",
+ "sourceSha256": "7258b780d52508ae92332fd931296dd44c7121d289fba80ff276dd18e9fec39b",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/held_proposal_once",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkDelivery.java",
+ "before": "if(claimed==null || !\"proposing\".equals(claimed.state()))return;",
+ "after": "if(false)return;",
+ "test": "dev.codespire.orchestrator.work.WorkDeliveryIT.takeoverPreservesAnInFlightProposalOutcomeWithoutResumingOrPostingAgain",
+ "module": "spire-orchestrator",
+ "sourceSha256": "4cf5c90943494273a94ade7d00685213b7b7438e46cbf3c73d70f476b2ded8c8",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/gate_superseded",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemControl.java",
+ "before": "if(gate!=null && \"OPEN\".equals(gate.state()))",
+ "after": "if(false)",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.humanIdSuspendsAndSupersedesGateEvenWithTheBotsDisplayName",
+ "module": "spire-orchestrator",
+ "sourceSha256": "f2784277ff2f960a413ff225e6e6e9bd0cb8c6003a2e73af0c9c21a5b880c201",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/machine_review",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkGateChannels.java",
+ "before": "item.control()!=null && item.control().machine(review.actorId())",
+ "after": "false",
+ "test": "dev.codespire.orchestrator.work.WorkGateChannelsIT.recordedMachineCannotApproveEvenWhenForgeCallsItAUser",
+ "module": "spire-orchestrator",
+ "sourceSha256": "deea1a7cd14e373fe2345f0ca18a8e4b501d26c69ff7980362e06e7053e2e5fe",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "guards/pending_build_invalidated",
+ "file": "spire-orchestrator/src/main/java/dev/codespire/orchestrator/work/WorkItemStore.java",
+ "before": "\"ARTIFACTS_REQUIRED\",\"HUMAN_TAKEOVER\",\"WORK_ITEM_RETIRED\"",
+ "after": "\"ARTIFACTS_REQUIRED\",\"WORK_ITEM_RETIRED\"",
+ "test": "dev.codespire.orchestrator.work.WorkRunDispatchTest.takeoverInvalidatesTheUnstartedBuildBeforeAnyDispatcherRecheck",
+ "module": "spire-orchestrator",
+ "sourceSha256": "97695d3cd2dd218714e11556254706fe0797603c1ba39793a4d12fdab382e956",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/resume_note_required",
+ "file": "spire-ui/src/components/work-items/WorkItemDetail.tsx",
+ "before": "busy || item.workflowStatus === 'suspended' && !note.trim()",
+ "after": "busy",
+ "test": "src/components/work-items/WorkItems.test.tsx",
+ "name": "requires an operator note to resume suspended work and shows the recorded head",
+ "sourceSha256": "0c54954c7318a340ed5aca0bfebb3382623e085c4ffc1e455b41a4a11357eba0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/resume_note_sent",
+ "file": "spire-ui/src/components/work-items/WorkItemDetail.tsx",
+ "before": "resumeWorkItem(item, readmit, note)",
+ "after": "resumeWorkItem(item, readmit)",
+ "test": "src/components/work-items/WorkItems.test.tsx",
+ "name": "requires an operator note to resume suspended work and shows the recorded head",
+ "sourceSha256": "0c54954c7318a340ed5aca0bfebb3382623e085c4ffc1e455b41a4a11357eba0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/takeover_note",
+ "file": "spire-ui/src/components/work-items/WorkItemDetail.tsx",
+ "before": "{item.control.note}",
+ "after": "{'TEST-hidden'}",
+ "test": "src/components/work-items/WorkItems.test.tsx",
+ "name": "requires an operator note to resume suspended work and shows the recorded head",
+ "sourceSha256": "0c54954c7318a340ed5aca0bfebb3382623e085c4ffc1e455b41a4a11357eba0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/takeover_head",
+ "file": "spire-ui/src/components/work-items/WorkItemDetail.tsx",
+ "before": "Observed head: {item.control.observedHead}",
+ "after": "Observed head: TEST-hidden",
+ "test": "src/components/work-items/WorkItems.test.tsx",
+ "name": "requires an operator note to resume suspended work and shows the recorded head",
+ "sourceSha256": "0c54954c7318a340ed5aca0bfebb3382623e085c4ffc1e455b41a4a11357eba0",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/pr_unavailable",
+ "file": "spire-ui/src/components/work-items/Approvals.tsx",
+ "before": "row.prReviewAvailable ? 'Available' : 'Unavailable'",
+ "after": "'Available'",
+ "test": "src/components/work-items/Approvals.test.tsx",
+ "name": "makes an unavailable PR channel visible while retaining dashboard and tracker answers",
+ "sourceSha256": "056468c79863445fa2f94ef44818c7c4f3b9c6ed50af55f3c8e5d1c2d1d26776",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ },
+ {
+ "id": "ui/pr_supported",
+ "file": "spire-ui/src/components/work-items/Approvals.tsx",
+ "before": "row.prReviewAvailable ? 'Available' : 'Unavailable'",
+ "after": "'Unavailable'",
+ "test": "src/components/work-items/Approvals.test.tsx",
+ "name": "announces supported PR answers explicitly",
+ "sourceSha256": "056468c79863445fa2f94ef44818c7c4f3b9c6ed50af55f3c8e5d1c2d1d26776",
+ "mutant": "one selected assertion failure",
+ "restored": "pass"
+ }
+ ]
+}
diff --git a/.claude/reviews/global/factory-m3-slice9.md b/.claude/reviews/global/factory-m3-slice9.md
new file mode 100644
index 00000000..5d583901
--- /dev/null
+++ b/.claude/reviews/global/factory-m3-slice9.md
@@ -0,0 +1,94 @@
+# M3 slice 9 — external answers and human takeover
+
+## Scope
+
+Tracker commands, dashboard decisions and supported native PR approvals enter ResolveGate and
+record GATE_RESOLVED. Tracker commands bind the gate UUID, generation and artifact; ordinary
+approving text is human activity. Native GitHub approval reads verify the named review, latest
+decisive state, open PR and live head, then require measured push permission without DENY.
+GitLab and Bitbucket native approval channels remain visibly unavailable.
+
+Takeover compares stable identities recorded for the build and the tracker identity recorded at
+admission. It supersedes gates, invalidates pending effects and durably queues an exact run-binding
+publication hold. The worker stores that revocation independently of M1 cancellation. Resume
+requires a verified operator, expected revision and note, re-observes current evidence and creates
+a new generation. Retired items cannot resume. ADR-045 records the FR-F22/FR-F25 precedence.
+
+## Four requested discriminating proofs
+
+1. `ordinaryApprovingWordsCannotAnswerAnOtherwiseEligibleGate`: an allowlisted actor supplies
+ the exact gate, generation and artifact, but the comment lacks the slash. Removing the slash
+ requirement in production makes the test fail. The restored case records takeover, no resolution.
+2. `approvalOnOldHeadCannotResolveGateBoundToCurrentHead`: the native approval exists, measured
+ permission passes and gate/live PR share the new head; only the review commit is old. Removing
+ the review/live-head comparison fails. The dismissed-state case independently leaves the list
+ approved while the named reread reports DISMISSED, so the latest-list check cannot mask it.
+3. `recordedMachineIdSurvivesRenameAndAccountRotation`: signed push sender ID is the recorded bot,
+ while its display name and current configured account have changed. Removing the recorded-ID
+ comparison suspends the item and fails. A separate human-ID fixture uses the bot's display and
+ commit-author strings and must suspend. Dispatch and admission snapshot tests cover persistence.
+4. `takeoverRevocationSurvivesKilledJvmWithoutAnM1CancelClaim`: a first JVM builds and retains a
+ real Docker workspace without pushing to the real local Git origin. A second JVM commits the
+ hold and is killed before Docker cancellation. No M1 cancel slot is claimed. An otherwise valid
+ permit must fail; a third JVM runs watchdog/recovery and cannot push or rebuild. Removing the
+ durable SQL revocation predicate fails the permit assertion. The restored case passes.
+
+The [production mutation inventory](factory-m3-slice9-mutations.json) records all five core
+mutations and the additional guards. Each has one isolated assertion failure, an exact scratch-byte
+restoration and a passing restored test. Final production anchors and hashes were audited again.
+
+## Additional guards and validation
+
+Forced testFast, testServices and assemble passed sequentially on Java 25, each with
+--rerun-tasks --no-parallel. The XML totals are **4075 Java tests across 451 suites and
+30 modules**, zero failures/errors and 1 existing Windows symlink privilege skip.
+The fast tier has 1555 tests; the service tier has 2520. The final worker report contains
+all three actual process-recovery cases, including takeover without an M1 cancellation claim.
+The concurrent in-flight PR recovery test passes with two readers and exactly one persisted outcome.
+
+The full UI suite passed **730 tests across 92 files**; its production build, including TypeScript,
+passed. The accepted shared styling and route assertion remain intact.
+
+**78 checks cover 78 distinct production mutations**:
+72 target Java main sources and 6 target UI production sources. No fixture is mutated.
+Additional proofs cover source membership and artifact/generation bindings, native review identity
+and latest state, measured permission and DENY, signed activity identities, related coordinates,
+command precedence, identity snapshots, gate supersession, pending-effect invalidation, durable
+hold delivery, worker publication/recovery checks, resume authority and UI honesty.
+
+Pinned Semgrep 1.172.0 reports **zero findings across 82 final changed code files**.
+All captured hashes match final source. Its one partial-parser warning covers four unchanged
+optional-field spans in api.ts, identical to accepted 9ec2f9db. No suppression was added.
+
+One restored mutation run hit a Kafka-native startup failure (exit 126, “Text file busy”). Its failed
+log was retained; only the restored case was retried and passed before recording that pair. It was
+not counted as a mutation kill. No outside-container-deletion failure occurred in the final tiers.
+
+Local logs and selected JUnit reports are in the worktree's git-ignored .handoff/s9-* files;
+the checked-in inventory includes every production edit, selected case and restored-source hash.
+
+## Boundaries
+
+The first retry-wrapper mutation survived the existing reflection-only port coverage test:
+that test proves an override exists, not that it delegates. A new behavioral case checks the
+branch/repository arguments, returned head and transient retry. Its production null-return
+mutation is recorded separately only after the assertion failure and restored pass.
+
+Provider contracts checked on 2026-09-14: GitHub documents chronological
+[PR review history](https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28), which the
+latest-decisive-review scan uses. Native activity fields follow the
+[GitLab webhook payloads](https://docs.gitlab.com/user/project/integrations/webhook_events/).
+Jira Cloud comment polling uses the
+[REST v2 issue comments API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-comments/).
+
+No live external gate or operator resume is claimed. Native provider observations use WireMock;
+the publication-hold process-death proof uses real processes, PostgreSQL, Docker and a local Git
+origin. The accepted slice 8b live standalone /fix proof remains separate and is not repeated.
+Jira Cloud polls comments; Data Center cannot. Incomplete native review history cannot authorize
+an approval. Jira processes individual comments and reports failure at its polling page bound.
+Signed deletion/transfer handling still needs live payload evidence. Remote
+publication already in progress cannot be recalled; recovery records its outcome and leaves the
+item suspended. No atomic ordering with a human's remote push is claimed.
+
+All fixtures are TEST-owned with exact cleanup. No dev run worker was started and no live data
+baseline was changed. All seven previously accepted criteria remain unchanged.
diff --git a/CLAUDE.md b/CLAUDE.md
index ae180a07..70c8d5b5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,6 +19,9 @@ services — see `LICENSING.md`. Never call the project "open source" in docs or
## Read first
+Before writing a new dashboard widget, check [spire-ui/docs/WIDGETS.md](spire-ui/docs/WIDGETS.md)
+for the existing components, vocabulary and account/person helpers.
+
The design is fully specified in `docs/` — **treat those files as the source of truth**:
| Doc | Contents |
@@ -32,72 +35,80 @@ The design is fully specified in `docs/` — **treat those files as the source o
| `docs/SECURITY.md` | Trust boundaries, OIDC/RBAC, Tink encryption, LLM threat model, cost gaps |
| `docs/TLS.md` | The five requirements a TLS terminator must satisfy, the identity-provider leg included, three worked topologies, and a symptom table. Code Spire terminates no TLS by design |
| `docs/REPO-RULES.md` | The `.codespire` file: format, the target-branch rule and why, writing effective rules |
-| `docs/DECISIONS.md` | ADR-001..041 — every locked decision with its why. ADR-029..040 are the software factory's; `docs/factory/` explains them in context |
+| `docs/DECISIONS.md` | Architecture decisions and their rationale. ADR-029..040, ADR-042..045 cover the software factory; `docs/factory/` explains them in context |
| `docs/UNVERIFIED.md` | **Read before claiming something works.** The register of claims the code or the docs make that no test establishes — known-broken-and-guarded, fixed-but-never-run-live, paths no test reaches, and claims needing a corpus or spend. Three milestones in a row shipped a feature that was green, documented, and did not work |
| `docs/RESEARCH.md` | Market landscape + the PR-Agent code evaluation that justified greenfield |
| `docs/ROADMAP.md` | Phases P0–P4 with exit criteria |
| `docs/HISTORY.md` | The per-milestone delivery log: what shipped, what each review round found, the traps each one paid for. **Append new milestones there**, then rewrite the Status snapshot below |
-| `docs/factory/` | **M0, M1 and M2 delivered (PRs #95/#96/#119, 2026-09-02/03/04), M3–M6 designed.** The software factory: work item → spec → plan → sandboxed agent runs → branch → PR reviewed by the existing reviewer. PRD (FR-F1..F32), architecture, module reference, execution layer (harness terms quoted with retrieval dates), run topology, autonomy model, product packaging, prior art, M0–M6 build order, and `AGENT-IMAGE-CONTRACT.md` — the published contract any agent image may satisfy, checked by `spire-agent-image verify`. Decisions are ADR-029..ADR-040. ROADMAP's M0 section records what the build taught that the design had wrong |
+| `docs/factory/` | **M0–M3 implemented (PRs #95/#96/#119/#153); M3 final review pending, M4–M6 designed.** The software factory: work item → spec → plan → sandboxed agent runs → branch → PR reviewed by the existing reviewer. PRD (FR-F1..F32), architecture, module reference, execution layer (harness terms quoted with retrieval dates), run topology, autonomy model, product packaging, prior art, M0–M6 build order, and `AGENT-IMAGE-CONTRACT.md` — the published contract any agent image may satisfy, checked by `spire-agent-image verify`. Decisions are ADR-029..ADR-040. ROADMAP's M0 section records what the build taught that the design had wrong |
| `docs/CICD-AND-PACKAGING.md` | **Parked plan.** No CI exists today; analysis of GitHub Actions + GHCR images + Helm/kustomize/ArgoCD, why Terraform is declined, and why it waits for D10 |
| `docs/D10-AUTH-PLAN.md` | **Planned, not started.** The auth gate: hybrid OIDC, per-service URL prefixes so cookie scoping is real, the spike that must precede code, and the two designs review falsified |
## Status (a snapshot — rewrite it, never append to it)
-The per-milestone story — what shipped, what each review round found, the traps each one paid for —
-is in **`docs/HISTORY.md`**. A new milestone gets a new entry there; this section is rewritten to
-describe the new current state. Everything below is true as of **2026-09-12**.
+Measured on **2026-09-14**. Delivery history is in docs/HISTORY.md; the consolidated
+[M3 acceptance record](docs/factory/M3-ACCEPTANCE.md) maps all seven verified criteria to their
+proving slices and reviews. **M3 implementation is complete; final operator review is pending.**
+PR #153 remains draft. No merge or promotion to ready is authorized by this handoff.
+
+- **The reviewer (P0–P4) is delivered.** Gateway, orchestrator and review worker communicate over
+ Kafka, with the React dashboard. Bitbucket Cloud, GitHub and GitLab have measured live reviewer
+ parity. Jira, Confluence, GitHub/GitLab Issues, repository rules and rung-2 code knowledge supply
+ context. Repository prompts, /finding, learned memory, analytics and operator SCM sign-in remain.
+- **Operations are delivered.** Hybrid OIDC uses separate service prefixes and session cookies;
+ stored sensitive content is encrypted with Tink. The charge ledger, fleet caps, refused status,
+ archive policy, attention, circuit breakers, provider-neutral boundaries and split licensing
+ remain. CI, Compose, Helm/kustomize and the separate GitLab e2e tier are present.
+- **M0–M2 execution and standalone /fix are delivered.** The Docker run unit, trusted publisher,
+ durable transcript/control, salvage and orphan watchdog, harness pool, corporate environment
+ and image contract remain. Per-finding and per-review fix caps still apply. The live chain was
+ proved on 2026-09-12 by runs 3987682681:1 and 3987682176:1, then re-proved on TEST PR #32 by
+ run 4003204361:1 on 2026-09-14: automatic push, next review, resolved thread and persisted
+ verdict, with the other six prior findings UNCHANGED. The TEST PR was closed and its exact
+ branch deleted; review/run history was retained. The development run worker remains stopped.
+- **M3 owns repositories, people and work-item policy.** Explicit repository-role bindings replace
+ account workspace lookup. People resolve to stable provider IDs and render observed handles;
+ source actor allowlists remain independent. /fix measures effective push access through the
+ selected reviewer, with explicit ALLOW/DENY overrides. V72 now drops only the unused account
+ workspace column after a fresh validated backup. IDs, ciphertext/AAD, context references,
+ bindings, immutable legacy mapping evidence and live history are preserved.
+- **M3 intake and approvals are durable.** GitHub/GitLab/Jira source parity is derived from adapters
+ and test sources. Unknown label attribution selects nothing. Current labels, pinned admission
+ bounds and the current ceiling restrict each next action. Scanner pages and uncertain tracker
+ writes survive real process death. Humans register actual specification and single-step plan
+ references; bodies stay transient. Suggest stops before BUILD, assisted requires PLAN approval,
+ and autonomous admits one prepared build. UI proofs compare phases, gates, decisions and runs.
+- **M3 item publication and takeover preserve authority through restart.** Item builds checkpoint
+ without pushing and retain their workspace. An exact current permit resumes only the publisher.
+ Dashboard answers, bound tracker commands and supported native PR reviews share ResolveGate.
+ Ordinary approving prose cannot approve; stale/dismissed reviews cannot resolve a gate.
+ Takeover uses recorded stable machine IDs across rename/rotation, supersedes gates, invalidates
+ pending effects and persists publication revocation before stopping compute. Actual JVM death
+ without an M1 cancel claim cannot restore publication authority. In-flight PR recovery records
+ the observed outcome once and keeps the item suspended. Resume requires a verified operator,
+ expected revision, note and fresh evidence; retired items cannot resume.
+- **Measured final validation:** 4076 Java tests across 452 suites and 30 modules,
+ zero failures/errors and 1 existing Windows symlink privilege skip. Forced testFast,
+ testServices and packaging passed sequentially. The full UI passed 742 tests across 93 files,
+ TypeScript and production build. Slice 10 adds four verified production mutations (the real
+ migration and separate read/INSERT/UPDATE guards); slice 9 has 78 distinct production mutations.
+ Earlier per-slice counts and selectors remain in the acceptance record, without claiming a
+ globally deduplicated total. Pinned Semgrep reports zero findings across 5 changed code files.
+ The final scan and rollout evidence are in
+ .claude/reviews/global/factory-m3-slice10.md.
+
+**Limits that remain:**
-- **The reviewer (P0–P4) is delivered.** Three deployables over Kafka — `spire-gateway` (:34081),
- `spire-orchestrator` (:34080), `spire-review-worker` (:34082) — plus the `spire-ui` dashboard
- (:34000). Three SCM adapters (Bitbucket Cloud, GitHub, GitLab) at full-flow parity — webhook →
- review → conversation → ADR-019 reconciliation — verified live on all three (SMOKE-TEST Mode G).
- Context: Jira, Confluence, GitHub Issues, GitLab Issues, `.codespire` repo rules, and the
- repository knowledge base at rung 2 (`spire-context-code` + `worker.code_symbol`, ADR-026).
- Per-repository prompts, `/finding`, learned memory + analytics (ADR-027), operator SCM sign-in
- (ADR-028).
-- **Operations are delivered.** Hybrid OIDC operator auth with per-service URL prefixes (ADR-022);
- Tink encryption at rest; the priced charge-line cost ledger (ADR-023), fleet spend caps and the
- `refused` status (ADR-025), archive-not-delete (ADR-024); the operator attention panel; per-host
- circuit breakers on SCM and LLM calls; provider-neutrality enforced by the `spire-arch` build check
- (ADR-020); split licensing (ADR-021). CI/CD: nine GitHub Actions workflows, four production images
- on GHCR, Compose + Helm + kustomize under `deploy/`, and the nightly `spire-e2e` tier against a
- real containerised GitLab.
-- **Software factory M0–M2 are delivered (ADR-029..040; PRs #95, #96, #119 — 2026-09-02/03/04).**
- `POST /api/runs` → `cs.run-commands` → `spire-run-worker` (:34083) → a three-container run unit on
- Docker → push gate → a branch on the real remote. M1 added the run event stream, cancel over
- `cs.run-control`, salvage-before-teardown, the orphan watchdog, idempotent dispatch that fails
- closed, the harness credential pool, the corporate run-unit environment (FR-F14) and the checkable
- agent image contract (`spire-agent-image verify`). **M2 made the reviewer close its own findings:**
- `/fix` on a finding dispatches a run that pushes onto the pull request's own source branch
- (ADR-040), bounded by two caps (per finding AND per review, FR-F32); a `PullRequestSink` port with
- three adapters, so a run can end at a pull request rather than at a branch; `GET /api/runs`, the
- run↔review join and the `/runs` screen; and `spire-run-worker` in **both packaged stacks behind
- the `factory` compose profile** — opt-in because the Docker socket it mounts is root-equivalent
- on the host. **The loop M2 exists to close has never been run end to end in one place**: the
- dispatch, the push and the reconciliation are each proved separately, and a run unit cannot
- reach the e2e stack's GitLab because `RunUnitSpec` has no network field (`docs/UNVERIFIED.md`).
- **Next is M3** — `docs/factory/ROADMAP.md`. The two factory images are still not on GHCR.
-- **Accounts normalization (#148, ADR-041).** Machine accounts now own forge and Atlassian
- credentials in one registry. Context sources select a compatible account and retain their own
- URL, project keys and path allowlists. V59 plus an idempotent startup reconciler moves legacy
- credentials across Tink AADs atomically per source; failed rows remain recoverable. Account
- rotation reaches every source, disabling an account stops its context resolution, and referenced
- deletion returns the source names. Accounts show Used by and advisory scope reports. REVIEWER
- and FACTORY remain separate scalar roles; CONTEXT has no workspace. Code credentials carry an
- explicit platform through the worker contract. Repositories still show the existing serving
- identities through unchanged role resolvers. Live token-family and rollout gaps are recorded in
- UNVERIFIED; scoped Atlassian gateway tokens are not claimed supported. M3 retains workspace
- remodeling, per-repository push checks and handle-to-id allowlist resolution.
-- **Known gaps** are in `docs/UNVERIFIED.md` (read before claiming something works) and `techdebt/`
- (one entry per item, per module). Review dispositions per round are in `.claude/reviews/`.
-- **Measured, not estimated (2026-09-12):** 2954 Java tests across 336 suites, 0 failures,
- 1 skipped; 613 UI tests across 75 files; TypeScript clean. Java verification uses JDK 25,
- Docker and Git's shell on PATH. Nine intentional mutations fail their targeted tests, including
- migration rollback, credential equality, disabled-account filtering, platform dispatch,
- provider-neutrality, unknown scopes, account-picker compatibility, legacy wire degradation
- and retaining scope observations through outages. UI table layout was
- observed in headless Chrome at 1280/1440/1920 widths. The nightly testE2e tier and a production
- credential migration were not run for this change.
+- **Production VERIFY and LAND remain unavailable.** M4 owns the verifier; M3 does not ship one.
+- **No live item-build proof.** TEST PR #32 proves standalone /fix; real local-origin item tests
+ do not prove an item-linked build against a real forge.
+- **The automated GitLab run-unit gap is still open.** RunUnitSpec has no network field, so a
+ run unit cannot reach the e2e stack's GitLab. A live GitHub run does not close it.
+- **The two factory images are still not on GHCR.**
+- Per-forge identity and permission limits remain separate UNVERIFIED entries. Native external
+ gate answers and operator resume have no live proof. GitLab/Bitbucket native PR approvals and
+ Jira Data Center comment polling remain unavailable. Publication already in progress cannot
+ be recalled; no atomic ordering with a remote human push is claimed.
## Build & run
@@ -189,8 +200,8 @@ the LLM mock's request journal, and GitLab's own webhook-delivery history.
- Java 25 / Quarkus 3.38.3 / Gradle Kotlin DSL; **pure domain code stays free of framework imports** —
build-enforced for `spire-contract` and `spire-diff` by `PureModulesAreFrameworkFreeTest`
(`spire-arch`), which permits only the JDK, those modules themselves, and one documented
- exception: **`jackson-annotations`** (annotations only, no databind) on the sealed
- `IntegrationEvent` / `ActionCommand` hierarchies, because those types *are* the Kafka wire
+ exception: **`jackson-annotations`** (annotations only, no databind) on the wire
+ event/command hierarchies and repository envelope records, because those types *are* the Kafka wire
contract and their discriminators belong with them. Per-service mix-ins were considered and
rejected: they spread one registry across every `ObjectMapper` in three services, where a missed
site is a runtime wire break rather than a compile error. Adding a second exception means
diff --git a/Dockerfile b/Dockerfile
index 2544efcb..739432b3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -45,6 +45,10 @@ COPY spire-scm-bitbucket/build.gradle.kts spire-scm-bitbucket/
COPY spire-scm-github/build.gradle.kts spire-scm-github/
COPY spire-scm-gitlab/build.gradle.kts spire-scm-gitlab/
COPY spire-secrets/build.gradle.kts spire-secrets/
+COPY spire-worksource/build.gradle.kts spire-worksource/
+COPY spire-worksource-github/build.gradle.kts spire-worksource-github/
+COPY spire-worksource-jira/build.gradle.kts spire-worksource-jira/
+COPY spire-worksource-gitlab/build.gradle.kts spire-worksource-gitlab/
COPY spire-workspace/build.gradle.kts spire-workspace/
# A Windows checkout gives gradlew CRLF and /bin/sh then rejects the shebang with
diff --git a/LICENSING.md b/LICENSING.md
index e0308b13..ab78c4cd 100644
--- a/LICENSING.md
+++ b/LICENSING.md
@@ -26,6 +26,10 @@ libraries you build *against* are Apache-2.0, the services you *run* are FSL.
| `spire-context-gitlab` | Apache-2.0 | Same. |
| `spire-context-code` | Apache-2.0 | Repository code context provider (ADR-026). |
| `spire-llm` | Apache-2.0 | Reference LLM provider. |
+| `spire-worksource` | Apache-2.0 | The tracker work-source SPI and transient evidence types. |
+| `spire-worksource-github` | Apache-2.0 | Reference GitHub issue work source and signed control-fact ingress. |
+| `spire-worksource-jira` | Apache-2.0 | Reference Jira ticket work source with separate write facade. |
+| `spire-worksource-gitlab` | Apache-2.0 | Reference GitLab ticket work source with separate write facade. |
| `spire-harness` | Apache-2.0 | The agent-execution SPI. Every harness arm compiles against it (ADR-030). |
| `spire-harness-codex` | Apache-2.0 | Reference harness arm — the worked example a second arm copies. |
| `spire-secrets` | Apache-2.0 | One credential scrubber, for every process that writes a failure message a human reads. JDK-only ON PURPOSE: the run worker must not inherit a git library to obtain it. |
diff --git a/build.gradle.kts b/build.gradle.kts
index 1b0f686e..03292ab4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -215,6 +215,10 @@ val fastTestModules = listOf(
"spire-harness-codex",
"spire-secrets",
"spire-workspace",
+ "spire-worksource",
+ "spire-worksource-github",
+ "spire-worksource-jira",
+ "spire-worksource-gitlab",
"spire-runtime",
)
diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md
index 4ff41900..e2dc44af 100644
--- a/docs/CONTRACT.md
+++ b/docs/CONTRACT.md
@@ -1,5 +1,99 @@
# Domain Contract (`spire-contract`)
+## Work-item channels and policy (M3, ADR-043/045)
+
+Signed issue delivery produces `WorkSourceDelivery` on `cs.work-integration`, keyed by the
+stable `WorkItemIds` digest. Its coordinates bind the registration, repository, source, SCM
+origin and external scope; a webhook hint is not proof of complete current-label history.
+`WorkItemIntake` and the scanner fetch the current issue and bounded audit evidence through
+the explicitly selected source account. Unknown attribution grants nothing, even with an
+otherwise allowed actor hint. Actor membership and missing identity have separate reasons.
+
+`WorkItemLifecycle` is the sole work-event decider. `WorkItemEvent` is stored and decoded as
+its own type, outside the review `DomainEvent` and `IntegrationEvent` hierarchies. One JTA
+transaction appends its encrypted envelope, updates bookkeeping, deduplicates delivery and
+enqueues the encrypted notification. The outbox publishes the stable envelope ID on
+`cs.work-events` and marks it published only after broker acknowledgement; delivery is at least
+once. Work ingress failures use `cs.work-dlq`. Review history does not consume work events.
+
+Profiles have immutable versions and an operator-defined unique precedence. Every eligible
+current label contributes a component-wise restriction; the repository ceiling and the full
+mode vector retained at admission also bound later decisions. The selected profile name is a
+display choice, not the whole effective policy. Events retain applied/ignored evidence and the
+source, account, repository and policy revisions. Humans register fetched specification and plan
+references; their versions bind later decisions. Dashboard answers, explicit tracker commands
+and supported current native PR approvals enter `ResolveGate` and persist `GATE_RESOLVED`.
+Production VERIFY and LAND remain unavailable; manual artifact acceptance does not invent an
+executor completion. Tracker status never supplies workflow status.
+
+`ExecuteWorkRun` uses `cs.run-commands` and yields a durable `RunWorkReady` checkpoint without
+publishing. `PublishWorkRun` on `cs.run-control` carries the exact current delivery permit;
+only the trusted publisher resumes. `HoldWorkRun` uses that same control topic and durably revokes
+the exact run binding before stopping compute. The revocation survives restart and orphan salvage
+independently of M1 cancellation. Ordinary `ExecuteRun` and standalone /fix retain their automatic
+publication path. Readiness and terminal results use `cs.run-results`; transcript facts remain
+on `cs.run-events` and do not become work-item domain events.
+
+Takeover records stable actor IDs in `WorkControl`, supersedes open gates and invalidates pending
+effects. Deliberate authorized commands are classified before generic comment takeover. Resume
+requires the server-derived operator subject, expected revision and note, fresh issue/repository/
+head/policy evidence, and a new generation. Retired items cannot resume. See the
+[acceptance record](factory/M3-ACCEPTANCE.md) for measured journeys and remaining live limits.
+
+## Repository metadata and ingress channels (M3 slices 1–2, ADR-042)
+
+`cs.registry-integration` carries `RepositoryRegistration`, keyed by registration UUID (not a
+review id). Its `type` discriminator is `RepositoryRegistration`; fields are `registrationId`,
+positive monotonic `revision`, `providerType`, nullable `forgeOrigin`, `scope` (`repo`/`org`),
+`target`, `enabled`, `deleted`, nullable `repositoryId`, `eventKind` (legacy default REVIEWER),
+and nullable `sourceId`. The gateway outbox publishes only after its SQL transaction
+commits and marks sent only after broker acknowledgement. The orchestrator accepts newer
+revisions transactionally. New records reject blank origins while allowing null. At legacy
+ingress the bridge converts blank origins to null before strict record decoding, then records
+`registration_origin_unknown` and acknowledges the snapshot. Missing origins are never inferred
+from account workspace equality; they remain operator-visible pending mappings for repair.
+
+This is an integration snapshot, not a domain event or a new aggregate. Webhook keys and secrets
+never cross the channel. Failed processing uses `cs.dlq`; the discriminator routes manual replay
+back to cs.registry-integration.
+
+Slice 2 sends signed SCM ingress on cs.repository-integration as RepositoryDelivery, with the
+RepositoryDelivery discriminator, repositoryId (nullable for legacy/org hooks), registrationId,
+registrationRevision, providerType, forgeOrigin, eventKind, deliveryId and the existing typed
+IntegrationEvent. Gateway deliveryId is the SHA-256 of the signed request bytes. A manual review
+uses its explicit repository UUID. The consumer resolves full forge identity, verifies any
+explicit UUID and repository state, then applies REVIEWER lifecycle handling or forwards FACTORY
+activity to cs.repository-activity for the existing activity publication. M3's `WorkActivityConsumer`
+independently consumes FACTORY `RepositoryActivity` envelopes directly from cs.repository-integration
+under the `spire-orchestrator-work-activity` group, rechecking registration and repository identity.
+It does not consume cs.repository-activity. ISSUE routes through the bound work-source ingress to
+cs.work-integration rather than the SCM review path.
+
+New deliveries for unknown repositories produce Attention with their incoming registration and
+prefilled coordinates. Unknown origins remain repairable, never inferred from namespace equality.
+Old raw SCM messages on cs.integration fail with a provenance reason and enter cs.dlq; operators
+must redeliver through a verified webhook. DLQ replay of RepositoryDelivery returns to its new
+topic with provenance intact. Worker integration-result channels and old review IDs stay intact.
+
+**Upgrade order:** provision cs.registry-integration, cs.repository-integration and
+cs.repository-activity and their producer/consumer ACLs when auto-creation is disabled. Upgrade
+gateway first: new ingress can wait durably on its new topic while the old orchestrator runs.
+Then upgrade orchestrator (the new input begins at earliest), then the UI. Upgrading orchestrator
+first would dead-letter still-legacy gateway deliveries. Retain database/keyset backups; an
+application-only downgrade is not an ingress rollback because the wire topic changed.
+
+**API cutover:** account ProviderInput/ProviderView have no workspace. Account create/update
+accept an optional validationRepositoryId query parameter for account-less token validation;
+it must name the same forge kind and origin and creates no binding. Stored account checks may
+use one of that account's explicit repository bindings as validation scope. Scope introspection
+remains advisory; an unobserved report never establishes permission.
+
+Manual registration without a URL and POST /api/runs require repositoryId. Supplied coordinates
+must agree with it; URL preview resolves complete forge identity. Serving views take repositoryId
+and read the selected REVIEWER/FACTORY accounts. Missing, disabled and unmapped configurations
+cannot dispatch. The repository screen owns coordinates, role bindings and optional per-kind
+hooks; legacy organization hooks and origin repair remain at /settings/webhooks.
+
> The shared kernel every service depends on: identifiers, the event envelope, the event & command
> catalog, the `ReviewLifecycle` decider, the SPI ports, the context-aggregation policy, topics, and
> the Bitbucket **Cloud** mapping. Companion to [EVENT-MODEL.md](EVENT-MODEL.md) (the narrative slices)
@@ -247,18 +341,28 @@ active `LlmProvider`/`DiffSource`. Adding a plugin = new bean, no core edit.
referenced by `contextRef` on `ContextAssembled`/`GenerateReview`. **Jira is the first live provider**
(`spire-context-jira`).
-## 9. Kafka topics (keyed by `reviewId`)
+## 9. Kafka topics
| Topic | Carries |
|---|---|
-| `cs.integration` | ingress events (`PullRequestEventReceived`, `PullRequestClosed`, `ManualCommandReceived`, `AuthorReplied`, `PushReceived`) |
+| `cs.registry-integration` | Revisioned registration metadata; keyed by registration UUID |
+| `cs.repository-integration` | Verified `RepositoryDelivery` envelopes around SCM ingress; keyed by the existing event key |
+| `cs.repository-activity` | Existing FACTORY activity publication, keyed by repository UUID; M3 takeover instead consumes the verified original envelope on cs.repository-integration |
+| `cs.work-integration` | Bound tracker deliveries, keyed by stable work-item identity |
+| `cs.work-events` | Durable work-item event notifications from the encrypted outbox |
+| `cs.work-dlq` | Failed tracker and factory-activity processing |
+| `cs.run-commands` | Standalone ExecuteRun and held ExecuteWorkRun, keyed by run ID |
+| `cs.run-control` | CancelRun, SteerRun, PublishWorkRun and HoldWorkRun; each worker consumes control independently |
+| `cs.run-results` | RunStarted, RunWorkReady and terminal RunFinished/RunFailed facts |
+| `cs.run-events` | Bounded run transcript facts, independent of workflow decisions |
+| `cs.integration` | Retained legacy SCM ingress; new consumers dead-letter it with a provenance repair reason |
| `cs.commands` | action + record commands |
| `cs.events` | aggregate domain events |
| `cs.results` | worker-produced integration events (`DiffFetched`, `ContextRequested`, `ContextContributed`, `ContextAssembled`, `ReviewGenerated`, `ReviewFailed`, `CommentsPosted`, follow-ups). **Short retention** — carries source-quoting payloads without app-layer encryption (ADR-014) |
| `cs.dlq` | dead-letters (after retry budget); surfaced on the dashboard with a replay action (FR-8) |
-All keyed by `reviewId` so a PR's messages are strictly ordered within a partition. (Topic split is a
-starting point; can be refined — the keying discipline is the important invariant.)
+Review lifecycle messages retain their existing `reviewId` ordering. Repository metadata and
+factory activity use the explicit UUID keys shown above.
## 10. Bitbucket **Cloud** mapping
@@ -291,3 +395,42 @@ REST (`api.bitbucket.org/2.0`), auth = bot **App Password** (Basic) or OAuth, sc
## 11. Versioning
`eventVersion` starts at 1 per type. Additive fields don't bump it; breaking changes bump it and ship an
upcaster (`vN → vN+1`) in `spire-contract`. Consumers tolerate unknown fields. Published events are immutable.
+
+## Resolved people and repository fix overrides (M3 slice 3)
+
+All person endpoints require `spire-admin` and use the explicitly selected account. A resolution
+response contains status, candidates (`providerUserId`, observed `handle`, `displayName`) and a
+safe capability explanation. It never contains credentials or email fields.
+
+| Endpoint | Behavior |
+|---|---|
+| `POST /api/providers/{id}/actors/resolve` | Resolve `{handle, repositoryId?}`; repository scope must be bound to this account. |
+| `GET /api/providers/{id}/actors?refresh=true` | Read account policy; optional refresh resolves each stored ID, never its old handle. |
+| `POST /api/providers/{id}/actors` | Save `{handle, providerUserId, revision, repositoryId?}` after repeating lookup and by-ID verification. |
+| `DELETE /api/providers/{id}/actors/{actorId}?revision=` | Remove an account entry with optimistic policy revision. |
+| `/api/repositories/{id}/fix-actors` | Corresponding list/save/delete operations using its reviewer; save additionally requires `effect: ALLOW|DENY`, and the existing actor revision (zero for creation). |
+| `POST /api/repositories/{id}/fix-actors/resolve` | Resolve through that repository's selected reviewer. |
+
+Not-found, ambiguous or unsupported identity input returns 422 without writing; upstream
+unavailability returns 503. A stale policy, disabled account or missing reviewer returns 409.
+GitHub/GitLab support exact handles. Bitbucket/Jira return `SELECTION_REQUIRED`; the browser
+must name a returned candidate, and the server repeats that selection check on save. Candidate
+IDs are disambiguators in selection controls, not an operator input requirement.
+
+Account create no longer accepts a raw author list: save credentials first, then resolve people.
+An ordinary account update can omit `authors` to preserve policy; a supplied list must match the
+current list under the account lock. A policy edit during token validation makes the old form
+return 409 and rolls back its credential/configuration changes. It cannot replace the list with
+unresolved text. Account views include `actorDisplays` for cached labels and stale states. Legacy
+unresolved entries are labelled for repair. Repository fix overrides are separate from account
+review/conversation policy; /fix uses explicit overrides and measured push access.
+
+## Effective repository permission (M3 slice 4)
+
+RepositoryPermissionSource.permission(RepoRef, providerUserId) returns CAN_PUSH, CANNOT_PUSH or
+UNKNOWN with a safe explanation. It reads through the repository's selected reviewer account.
+FixAuthorization evaluates unknown identity, DENY, ALLOW, then measured permission. UNKNOWN
+records PERMISSION_UNAVAILABLE and refuses dispatch; a prior successful read gives no authority.
+The saga records the exact reason as FixAuthorization in the timeline, with the reason and safe
+capability detail in the durable refusal. Allowed commands still pass the existing dispatch guards.
+The full lookup, including identity, redirects and pagination, is bounded to 20 seconds.
diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md
index 4f0d4d57..f79ffe4d 100644
--- a/docs/DATA-MODEL.md
+++ b/docs/DATA-MODEL.md
@@ -1,5 +1,74 @@
# Data Model
+## Work-item bookkeeping (M3)
+
+V63 adds `scm_provider.revision` for credential-authority rechecks. V64 introduces `work_source`
+(explicit account/repository, immutable tracker scope, health and scan cursor), source-owned
+stable actor IDs, immutable `autonomy_profile_version` definitions, repository ceilings and
+label mappings. It also introduces `work_item`, delivery dedupe, encrypted work outbox and the
+reserved work gate table, plus nullable work coordinates on `factory_run`.
+
+`work_item` contains coordinates, generation, admission profile/version, workflow phase/status,
+policy revision and reason. It contains no title, body or tracker status. Those are fetched for
+each detail request and are not copied into events or outbox payloads. `WorkItemEvent` retains
+the combined mode vector at admission, effective modes, applied/ignored label provenance and
+authority revisions. Its event-store and outbox encryption use distinct AADs.
+
+The item ID is a versioned SHA-256 digest of eight length-prefixed, normalized SCM/repository
+and tracker identity components. Mutable issue keys, account credentials and display names do
+not change identity. Redelivery records, event append, projection and outbox share one JTA
+transaction; a failed projection rolls all of them back. A scan page advances its cursor only
+with the reconciled page. Source failure records health and preserves existing workflow.
+
+V65–V71 add staged source pages and recoverable tracker effects, clamp and gate projections,
+phase attempts and reservations, prepared artifact references, run dispatch/result inboxes and
+delivery claims. Preparation records references, digests and base coordinates, never artifact
+bodies. WorkControl preserves recorded factory/reviewer/tracker IDs and takeover/resume facts
+inside encrypted workflow history. V71's work_activity_receipt deduplicates channel deliveries;
+work_run_hold_outbox durably repeats exact-binding revocations until the run ends.
+
+The runworker schema separately stores encrypted work execution, retained topology, readiness,
+publication permits and terminal acknowledgements. V4 adds work_publication_revocation keyed by
+run ID and binding digest. It deliberately has no execution-row FK: a hold may arrive before the
+execution command. Its durable refusal is independent of the M1 cancel claim. In-flight publication
+outcomes remain recorded without completing a suspended work-item phase.
+
+## M3 repository ownership (ADR-042, slices 1–2)
+
+V60 adds `repository` (UUID, kind, canonical forge origin, workspace, slug, enabled, revision)
+with unique `(scm_type, forge_origin, workspace, slug)`, and `repository_account` with a repository
+FK, account FK and one row per REVIEWER/FACTORY role. `review_status.repository_id` and
+`factory_run.repository_id` are nullable during the bridge. Existing history keys stay intact.
+
+`repository_legacy_account` is immutable migration evidence: account UUID, kind, base URL,
+workspace and role, without credentials or an FK that would erase evidence on deletion.
+`repository_registration_bridge` stores the latest registration revision, metadata, selected
+repository or named reconciliation problem. Duplicate/stale revisions cannot overwrite it.
+Operators repair pending mappings through `/api/repositories/pending`.
+
+Gateway V3 adds nullable `webhook_repo.forge_origin` and `repository_snapshot_outbox`. Triggers
+enqueue create/change/delete metadata atomically, including a bootstrap row for each existing
+registration. The outbox stores a global monotonic revision, registration id, JSON and `sent_at`;
+it contains neither `webhook_key` nor `webhook_secret`. Account/context ciphertext and UUID/AAD
+are unchanged. The gateway API accepts/returns an optional canonical `forgeOrigin`; old clients
+that omit it on update preserve the recorded origin.
+
+Gateway V4 adds repository UUID, event kind, optional work-source UUID and current revision.
+UNIQUE(repository_id,event_kind) enforces one hook per kind through the real registry; a partial
+legacy index retains scoped uniqueness for registrations awaiting explicit association. Revision
+triggers include these metadata fields. Existing webhook keys, encrypted secrets and rejection
+counters are unchanged. ISSUE is a reserved source-bound kind, not an SCM review route.
+
+Orchestrator V61 drops the former account (type,workspace,role) UNIQUE and workspace-by-role
+CHECK while retaining scalar role checks and, through slice 9, the populated scm_provider.workspace
+column. Account DTOs and runtime reads/writes stopped using it in the cutover. V72 explicitly drops
+only that column after the final pre-migration backup. Account IDs, ciphertext/AAD, bindings,
+context references and repository_legacy_account evidence remain. repository_unregistered_event
+records verified but unregistered deliveries without payload
+or secrets, coalesced by registration and full repository identity; its Attention action prefills
+registration. Review dispatch requires review_status.repository_id; factory_run.repository_id
+is written atomically with queueing. Unmapped legacy rows remain readable but cannot dispatch.
+
> Defines the actual data: (1) the **domain value types** that flow through events & ports, and (2) the
> **persistence model** — the event store (the versioned source of truth), the blob store, and the
> read-model projections, with relationships and encryption. Companion to [CONTRACT.md](CONTRACT.md)
@@ -193,9 +262,9 @@ Operational state (not projections — ADR-013 guards):
### Machine accounts and context sources (V59, ADR-041)
`scm_provider` owns machine credentials for GitHub, GitLab, Bitbucket Cloud and Atlassian.
-`role` is non-null (`REVIEWER | FACTORY | CONTEXT`); `workspace` is NULL exactly for CONTEXT.
-The existing `(type, workspace, role)` uniqueness still limits forge workspace roles to one
-account; PostgreSQL's distinct NULLs permit multiple context accounts, including Atlassian.
+`role` is non-null (`REVIEWER | FACTORY | CONTEXT`). V61 removes the former workspace-by-role
+constraint and `(type,workspace,role)` uniqueness; repositories explicitly select accounts.
+The populated legacy workspace column is retained without runtime use until slice 10.
`reported_scopes TEXT NULL` distinguishes unknown from an empty report; `scopes_checked_at
TIMESTAMPTZ NULL` records the last completed registration/Check scope observation. Failed probes
preserve both values; they do not replace a prior report with NULL. Both are advisory metadata.
@@ -260,3 +329,25 @@ includes the account platform separately from the source type.
`provider:`); LLM and harness credentials retain their separate registries and boundaries.
- **Never stored:** diffs/source (re-fetched by commit). Bootstrap encryption and service secrets
come from the deployment secret store; registered account tokens are encrypted in PostgreSQL.
+
+## Resolved policy actors (orchestrator V62)
+
+`provider_author.author` remains the stable-id account policy key. V62 adds `observed_handle`,
+`display_name`, `resolved_at` and `refresh_failed` as display observations; `scm_provider.actor_policy_revision`
+protects account list edits. Credential-only updates preserve these observations and policy.
+
+`repository_fix_actor` has primary key `(repository_id,actor_id)`, closed `ALLOW|DENY` effect,
+observed display metadata and a sequence-assigned revision. Contradictory edits compare the
+stored revision; deletion followed by recreation cannot reuse an earlier revision. Repository
+binding changes and actor writes serialize on the repository row. Account writes serialize on
+the account row so resolution cannot race credential/origin replacement.
+
+An unresolved observation, one older than 24 hours, or a failed refresh is labelled stale.
+Refresh failure persists across reloads; a successful read of the same stable ID clears it.
+Display freshness never supplies authorization evidence.
+
+Migration grants only legacy numeric GitHub/GitLab ids or ids already observed as stable review
+authors on that exact registered repository. Other legacy strings remain in the account list for
+explicit re-resolution and do not acquire new fix authority. The account workspace rollback
+column and all credential ciphertext are untouched. No runtime INSERT or UPDATE may name that
+retained column, as enforced by `AccountWorkspaceIsUnusedTest` alongside its read checks.
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index a7744f76..2ea16b19 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -4,6 +4,233 @@ Architecture decision records for Code Spire. Newest first.
---
+## ADR-045 — Declared profile precedence and bounded phase decisions
+
+**Status:** implemented through slices 7–9, including artifact handoff, held publication,
+external gate answers and takeover. All seven M3 criteria are independently verified.
+Production VERIFY and LAND remain unavailable; M4 owns the verifier.
+
+**Decision.** Operators assign distinct nonnegative precedence numbers to profile identities and
+create immutable profile versions. Names select no code path. The lowest-precedence eligible label
+selects the displayed profile. An eligible label is current, mapped and attributed to a person on
+that work source's allowlist. Missing attribution grants nothing.
+
+The effective vector is never above any applied label in any component.
+
+Meet every eligible label, the pinned admission vector and the current repository ceiling, component
+by component: ordinary phases use `off < approve < auto`, delivery uses `off < draft_pr < pr`, and
+land uses `off < approve < auto_if_green`. Omitted phases are off. Numeric maxima take the minimum;
+zero stops execution. Protected paths take the union, including the publisher's existing immutable
+CI floor. Incomparable vectors are valid and compose without widening either one. Precedence is a
+selection/display rule and never substitutes for these bounds. A higher-precedence request or a
+restricted vector records a durable clamp milestone and a current attention condition.
+
+Gate lifetime is an integer from 1 through 2,147,483,647 seconds. Reject larger values at profile
+creation so an accepted policy cannot overflow the persisted deadline during admission.
+
+Admission pins the profile version and effective bounds. Later label edits, allowed-person changes,
+account/source changes and ceiling edits are checked at each continuation. Raising authority cannot
+widen an admitted generation; an operator must explicitly re-admit. Remote observations happen before
+registry locks, and the commit compares source/account/repository and policy revisions. A concurrent
+edit or unavailable observation cannot grant permission. This is a fresh check at a phase boundary,
+not instantaneous cancellation of an effect already accepted by a remote service.
+
+The phase order is `intake → spec → plan → build → verify → deliver → review → land`. Delivery must
+precede the existing PR reviewer because that reviewer needs a pushed PR. Slice 8a corrects the
+published diagrams and binds fetched tracker artifact digests and build coordinates. A policy permitting a phase does not supply
+its implementation. Tests identify their execution capability explicitly; production does not turn a
+missing specification, verifier or publisher hold into a successful no-op.
+
+Dashboard gates are encrypted aggregate facts with synchronous query rows. Bind an answer to the
+item/generation, phase, policy, authority, item revision and artifact/head. One open gate holds a
+dispatch slot. At `now >= expiresAt`, expiry wins, persists refusal
+and releases the slot. A stale answer requires a new decision. The winning answer key is idempotent;
+a conflicting answer is 409. Resolver identity comes from the verified operator session, never the
+request body. Restart sweeps persisted overdue gates; ordinary retries cannot reopen a refused gate.
+Expiry takes the same local registry, policy and item lock order as an answer: projection foreign
+keys also lock parent rows. It needs no successful remote read to revoke an overdue decision.
+
+**FR-F22 / FR-F25 conflict.** FR-F22 treats a person's commenting as takeover; FR-F25 permits
+that same comment channel to carry a gate answer. Gate answers and authorized `/fix` commands
+are deliberate workflow actions. Classify and deduplicate them before
+ordinary comment takeover. An ordinary comment is neither an approval nor a resume instruction.
+A command that fails its authorization checks cannot borrow the gate-answer exception. The tracker
+and PR-review channels must prove their own stable actor and current artifact/head before entering
+the same decision boundary; the dashboard permission does not grant them authority.
+
+All three channels enter `ResolveGate` and persist `GATE_RESOLVED`. Tracker answers require an exact
+`/approve ` or `/reject` command (`-` explicitly binds an absent
+artifact), by a person allowed in that source. Signed GitHub/GitLab comments and authenticated Jira
+Cloud comment polling carry only coordinates, the stable actor and parsed command, never prose.
+Jira Data Center cannot prove person identity here. Native PR review answers are restricted to an
+open land gate and the linked PR's current head. GitHub re-reads the named review, the person's
+latest decisive review and open PR state; other forges visibly disable native approval answers.
+An unmatched PR approval is not a resume command and cannot reactivate a suspended item.
+
+Takeover compares stable identities recorded for the item/build, including the tracker writer's
+separate namespace. Display names, commit author text and replacement accounts do not redefine a
+recorded bot. Missing origin suspends conservatively, and unrelated repository/branch/PR activity
+does not target the item. Takeover supersedes gates, releases reservations, refuses unstarted
+effects and durably requests a publication revocation. The worker commits that revocation before
+stopping compute; fresh permits, restart and orphan salvage cannot restore publication authority.
+M1 cancellation alone is insufficient because it can salvage and publish.
+
+An already executing remote publication cannot be recalled. Its observed push/PR remains recorded
+without completing a suspended item's phase. Webhook receipt is not atomically ordered with a
+remote human push. Operator resume requires a verified subject, expected item revision and note,
+re-fetches tracker/repository/head evidence, re-resolves policy and starts a new bounded generation.
+A moved head discards the old preparation and requires fresh artifacts. Any required gate opens
+before execution. Old runs retain their publication revocations. A retired item cannot resume;
+its replacement needs a new identity and admission.
+
+**Consequences.** The UI displays requested profile, ceiling, actual modes, limits and clamp reason;
+it does not invent a profile name for a composite vector. Usage and attempt identities persist across
+re-admission. Dispatch reservations constrain local work and do not erase the existing documented
+softness of monetary limits while remote work is in flight. M3's accepted journey proof remains at
+the prepared plan/build boundary, with actual publication hold proved separately in slice 8b.
+
+---
+## ADR-044 — Stable identities for person policy and repository fix overrides
+
+**Status:** identity and override editing implemented in M3 slice 3; effective push authorization
+implemented in slice 4. Criteria 7, 6 and 5 are independently verified.
+
+**Decision.** Resolve people with the selected account's credential. GitHub/GitLab handle lookup
+must match exactly; Bitbucket/Jira use explicit selection where their API exposes candidates.
+Repeat resolution and stable-ID verification on save. Lock the account through the write so
+credential/origin edits cannot replace the resolving identity midway. Cache only display metadata,
+refresh by ID and report stale labels without assigning a new ID after a handle changes hands.
+A policy-bearing account cannot change forge kind or origin until its people are removed.
+
+Repository ALLOW/DENY overrides have one row per stable actor and optimistic revisions. Legacy
+numeric GitHub/GitLab IDs, or stable IDs observed on that repository's actual review history,
+become grants. Other strings need repair. These grants never replace target, branch, observe-mode
+or spending checks. The shared person control is reusable by work-source registration, which
+is introduced in slice 5; source authority must remain scoped to that source.
+
+For /fix, reject an unknown actor or unusable repository/reviewer first. Then apply an explicit
+deny, an explicit grant, or a fresh effective push measurement, in that order. Unknown permission
+refuses. Slice 5 replaces the initial pessimistic lock: a short transaction snapshots repository,
+account and actor-policy revisions, the bounded remote read holds no database transaction, and a
+second short transaction checks the revisions. Rotation, rebinding or an override edit discards the
+measurement as PERMISSION_UNAVAILABLE. Operator saves do not wait for the forge. The total identity/redirect/
+pagination budget is 20 seconds and cancellation stops unfinished work; successes are not cached.
+Only /fix leaves the legacy common author-list gate. Review, finding and conversation eligibility
+continue to use that account list, matching stable IDs only. A numeric username cannot impersonate
+a different actor's stored ID. Target/finding, self-loop, observe/archive, spend and fix caps remain.
+
+No credential elevation or secret response is introduced. Capability prerequisites are visible
+in the person control, with separate per-forge evidence limits in UNVERIFIED.
+
+---
+
+## ADR-043 — Tracker authority and durable work-item bookkeeping
+
+**Status:** implemented through slices 5–9: source parity, tracker effect recovery, policy gates,
+build handoff and external answers. [M3 acceptance](factory/M3-ACCEPTANCE.md) links the measured
+evidence; live tracker behavior remains in UNVERIFIED.
+
+**Decision.** A source names an explicit credential, tracker origin and stable project ID, and one
+target repository. Its stable actor allowlist belongs to that source; account authors and reviewer
+permissions do not grant label authority. Signed ISSUE hooks have a source/repository binding and
+publish normalized control facts on `cs.work-integration`, keyed by the stable work-item ID.
+No raw webhook body or ticket content crosses that durable channel.
+
+The ID hashes versioned, UTF-8 length-prefixed SCM coordinates and stable tracker coordinates.
+Mutable issue keys, credentials and titles are not identity. The bounded subject fits `RunIds`.
+Work events use a separate domain type and `cs.work-events`; they cannot be decoded as review
+domain events. The existing encrypted `event_log` stores both kinds under separate stream IDs.
+
+Current labels are reconciled with the complete bounded audit, including removals and re-additions.
+An incomplete or ambiguous history cannot establish an applier. A verified actor hint may remain
+visible with origin UNATTRIBUTED; it grants nothing even when the hinted actor is allowed.
+Missing ID, unknown attribution and unlisted actor have separate refusal reasons and mutations.
+Webhook intake and scans use the same evidence and policy path. A second current-label fetch
+detects changes during the observation; external revocation cannot be globally atomic with a
+local commit. Source, account, repository and policy revisions are checked under a short lock
+after the remote reads, and stale observations are discarded.
+
+Profiles have immutable numbered versions and operator-owned unique precedence. Names select no
+behavior. Missing phases are off; the effective vector meets every eligible label, the combined
+mode vector at admission and the current ceiling. Admission retains the selected version and
+every effective mode, so removing another restrictive label cannot widen the original grant.
+Events retain applied-label provenance and all local authority revisions. The detail screen
+shows these restrictions separately from the display profile. Automatic specification waits
+for real input; disabled and approval modes remain stopped or explicitly unavailable.
+Missing M4 executors never become successful no-op phases. Slice 7 completes caps, approval gates
+and the transition policy surface; slice 8 supplies the explicit artifact/build handoff.
+
+**Atomicity.** The lifecycle alone decides work domain events. JDBC event append, projection,
+delivery dedupe and encrypted notification outbox participate in one JTA transaction. A scan
+page advances its cursor in that transaction only after all observations reconcile. Notifications
+are at least once, with stable event IDs; no run command is emitted by this admission slice.
+The injected projection-failure test proves rollback after append, and its separate-transaction
+mutant proves that a surfaced exception alone is insufficient.
+
+`work_item` retains coordinates, profile/version, phase, workflow status, reason and revisions.
+It has no title, body or tracker-status mirror. Detail retrieves tracker content separately, so
+an inaccessible tracker cannot hide durable workflow history or masquerade as deletion. Gates
+and later effects retain item/generation references; ticket-derived sensitive payloads stay under
+the existing Tink encryption boundary with stream/effect associated data.
+
+---
+
+## ADR-042 — Repositories own coordinates and explicitly bind role accounts
+
+**Status:** implemented through M3 slice 2. Runtime resolution uses explicit repository bindings.
+The old account key and workspace-by-role check are removed; the populated workspace column
+remains rollback evidence until slice 10.
+
+**Decision.** A repository is identified by `(scm_type, forge_origin, workspace, slug)` and has
+its own UUID. Forge origin is the canonical HTTP(S) scheme, host and non-default port, with no
+API path suffix. Nested namespaces stay in workspace. `repository_account` binds at most one
+REVIEWER and one FACTORY account. Configuration may leave either role empty or select a disabled
+account; resolution requires an enabled repository and enabled account of the matching kind,
+origin and role. Known reviewer/factory identities must differ at binding time and at resolution,
+including after credential rotation. Account UUIDs,
+ciphertexts, `provider:` AADs and context source references are preserved.
+
+**Why.** The former `(type, workspace, role)` account key conflates credentials with repository
+selection and cannot distinguish hosts. Explicit references support credential rotation without
+reassigning repositories. Repository edits use revision checks; account deletion names its
+referencing repositories, and changing a referenced account's kind/origin is refused.
+
+**Bridge.** V60 snapshots legacy account assignments without changing existing serving resolvers.
+The gateway retains ownership of webhooks and credentials. Its V3 transactional outbox publishes
+typed, revisioned `RepositoryRegistration` metadata on `cs.registry-integration`, keyed by
+registration UUID. Broker acknowledgement marks an outbox row sent; duplicate delivery and stale
+revisions are harmless. Consumer failures use the existing DLQ with a registry-specific replay
+route. No webhook secret or routing key appears in this event.
+
+An unambiguous legacy match with an evidenced origin creates bindings once; subsequent snapshots
+preserve operator edits. A workspace alone never establishes a host. Conflicting or missing origins
+become attention rows with explicit mapping repair. A bounded history sweep uses persisted review
+URLs as origin evidence and links reviews/runs, including repositories observed through legacy org hooks. Org
+auto-enrollment ends at cutover: verified unregistered deliveries create attention naming the
+repository, origin and source registration, with a prefilled Register action. Registration
+snapshots alone do not create repositories after cutover.
+
+**Rollback evidence.** Keep the old account key and populated workspace in slice 1. Slice 2 drops
+the key/checks and all runtime reads, but retains workspace untouched until slice 10. Before any
+dev upgrade, preserve a verified full database dump and the matching keysets. The repeatable
+commands and real credential continuity probe are in the M3 plan; `.handoff/` survives sessions.
+
+**Cutover.** Signed gateway deliveries carry kind/origin/registration provenance on the new
+`cs.repository-integration` topic. REVIEWER deliveries enter the review lifecycle; FACTORY
+activity is forwarded separately; ISSUE requires a work source and is reserved for its later
+slice. A repository has at most one webhook per kind. Gateway V4 preserves existing keys,
+ciphertexts and rejection history. Raw legacy SCM deliveries are dead-lettered with a repair
+reason, never assigned credentials by a workspace match. Existing review IDs and credential
+transport AADs retain their original namespace split, including nested GitLab paths.
+
+**Proof.** Criterion 7 uses the named resource, fresh-schema gateway and UI tests in the M3
+plan. `RepositoryResolverCutoverTest` observes each dispatch entry at the actual database-backed
+credential boundary; separate choreography suites exercise subsequent commands.
+`AccountWorkspaceIsUnusedTest` scans runtime SQL, including SELECT-star mappings. The mutation
+ledger and real-row rollout measurements are in `.claude/reviews/global/factory-m3-slice2.md`.
+
+---
+
## ADR-041 — Credentials live on accounts; context sources reference an account
**Context.** `context_provider` copied the credential shape of `llm_provider`: a key without
diff --git a/docs/HISTORY.md b/docs/HISTORY.md
index 2821c126..9add4d31 100644
--- a/docs/HISTORY.md
+++ b/docs/HISTORY.md
@@ -1808,3 +1808,286 @@ lives in `docs/`, the locked decisions in `docs/DECISIONS.md`, and claims no tes
scope-query wrapper and unconditional scope-write overload were removed so future call sites
cannot silently choose the old path. Disabled migration rows intentionally remain in Attention
because their legacy credential columns must be retired too; that intent is now commented.
+
+- **Factory M3 slice 1 (2026-09-13, PR #153; ADR-042) — repository registration before resolver cutover.**
+ Repositories own explicit forge origin, workspace and slug plus reviewer/factory account bindings.
+ The settings view and admin API expose those choices, disabled or missing accounts, identity
+ conflicts and stale edits. Existing review/run callers retain their legacy resolution until
+ slice 2; the account workspace and its constraints remain intact.
+ - V60 expands the orchestrator schema without changing account ids, ciphertext or context
+ references. Gateway V3 queues versioned metadata snapshots in a transactional outbox; broker
+ acknowledgement precedes completion, and failed snapshots have a registry DLQ replay route.
+ Replays preserve operator rebindings. Automatic migration requires explicit registration
+ origin or stored PR URL evidence; unknown/mismatched origins produce named attention rows
+ and explicit mapping repair. Historical review and run coordinates are linked together.
+ - The reviewed backup command now targets persistent, git-ignored `.handoff/` in the worktree.
+ The existing 182-object, 492,480-byte archive was reused; no second dump was taken for the
+ review correction. A read-only encrypted continuity probe matched 9 real credential/reference
+ entries. This remains pre-upgrade evidence: no dev deployment or live migration was performed.
+ - M2's live proof is recorded for `artyomsv/spire-test#31`, runs `3987682681:1` and
+ `3987682176:1`, resolved threads and persisted verdicts. The automated GitLab networking gap
+ remains separate. Slice 8b retains the standalone live `/fix` publication regression proof.
+ - Review evidence and the per-guard mutation ledger are in
+ `.claude/reviews/global/factory-m3-slice1.md`.
+ - Final sequential forced gates: 3005 Java tests across 348 suites, zero failures and one
+ existing Windows symlink privilege skip; 620 UI tests across 76 files and the UI build passed.
+ Forty distinct mutations each failed one targeted test and passed after scratch restoration.
+ Docker-driving tests passed with the live run workers stopped; none was started for this slice.
+ - **Slice 1 review correction:** reject blank origins in the wire record while preserving null;
+ constrain the gateway and repository columns; normalize legacy blank payloads to unknown at
+ the bridge boundary. The broker test requires both the durable `registration_origin_unknown`
+ mapping/revision and a committed consumer offset. Removing the blank-safe check failed the
+ mapping assertion after the offset committed, so DLQ delivery cannot stand in for repair.
+ Four additional isolated mutations bring the total to 44. Final forced gates passed 3009 Java
+ tests across 348 suites, zero failures and the same Windows symlink skip. The archived-review
+ retry fixture now uses a future test clock to prevent its background scheduler stealing the
+ live-row precondition; production timing is unchanged. Dev Services startup timeouts on a
+ parallel retry were cleared by the final invocation's `--no-parallel --max-workers=2`.
+ - **CI correction:** replace the private history-link table-name concatenation with two complete
+ literal SQL constants, preserving the four bound values without suppressing Semgrep. The
+ existing history-bridge suite passed all 11 tests. The failed full scan and separate OSS check
+ each named the same single finding.
+ - **Reviewed bridge rollout:** orchestrator V60 and gateway V3 from `a956532` reached dev before
+ the new CI hold. All 37 reviews and 14 runs mapped to six repositories with eight bindings;
+ all three existing gateway snapshots were acknowledged and remain origin-unknown repair rows.
+ The real encrypted baseline comparison passed all 9 entries after V60. Runtime account
+ resolution remains on the legacy path; slice 2's later cutover comparison is still required.
+
+- **M3 slice 2 — repository cutover (PR #153, 2026-09-13; awaiting review):** all active SCM
+ dispatch uses explicit repository/role bindings. Account workspace leaves DTOs, forms and
+ runtime SQL; V61 drops its old constraints while retaining populated rollback evidence.
+ Gateway V4 preserves hook keys/secrets/rejection history and adds one hook per kind. Verified
+ provenance uses a new topic, FACTORY activity is separate, and unknown repositories produce
+ Attention instead of auto-enrollment. The repository screen supports optional hooks, missing
+ roles, partial-save retry and legacy repair at the gateway owner. Nested GitLab paths preserve
+ old review IDs and transport AADs. Criterion 7 has its exact named tests and production-schema
+ mutation; 62 distinct mutations are recorded. 3048 Java tests across 357 suites, zero failures and 1 existing Windows symlink privilege skip; 630 UI tests and packaging passed.
+ The real gateway-first V4/V61 rollout preserved 6/37/85/14/3 rows, all retained workspaces and
+ hook rejection metadata; 9 account/context and 12 webhook entries matched after decryption.
+ The three missing origins remain explicit repairs, not inferred mappings. No live run worker,
+ new spend, synthetic live row or second dump was used. Later M3 criteria remain pending.
+
+- **Factory M3 slice 3 — resolved people and editable overrides (PR #153, 2026-09-13).**
+ Account person entry resolves with the selected credential, repeats lookup and stable-ID
+ verification on save, and renders cached handles after reload. Raw-author writes through the
+ older account endpoint are refused; ordinary credential edits retain policy and observations.
+ GitHub/GitLab exact matching and Bitbucket/Jira explicit selection have separate capability
+ notes. Identity redirects cannot cross origin. V62 adds display observations, durable stale
+ flags, optimistic revisions and one ALLOW/DENY row per repository actor. Its legacy-grant
+ filters and constraints are measured against fresh V61 schemas. The account/workspace write
+ guard now covers INSERT and UPDATE; the startup reconciler omits the retained column.
+ The HTTP/DB and fresh-reader-JVM proof plus UI round trip establish automated criterion 6;
+ work-source screen wiring follows in slice 5/6 and effective push authorization in slice 4.
+ Tests caught a refresh failure that vanished on reload, an ambiguity fixture that could
+ pass on a 404 and an old account form overwriting a concurrent policy during token validation.
+ The locked registry write now owns the compatibility check and rolls back the stale edit. 3118 Java tests,
+ 636 UI tests, packaging and pinned Semgrep passed; 92 production mutations
+ have selected failures and scratch-restored passes. See the slice 3 review notes for boundaries
+ and the exact ledger. Live V62 preserves the approved row and credential baselines; the existing
+ GitHub person resolves by stable ID and keeps its observed handle across a full service restart.
+
+- **Factory M3 slice 4 — effective push authorization (PR #153, 2026-09-13).**
+ /fix applies repository DENY, ALLOW, then a fresh effective permission read through the selected
+ reviewer. Unknown refuses with a named capability error. GitHub effective collaborator roles,
+ GitLab inherited active membership and Bitbucket effective paginated rights have fixture proofs;
+ live token limits remain explicit. The total read budget is 20 seconds and cancels unfinished
+ work. Repository/account locks prevent a credential edit or rebind from splitting a decision.
+ Self-loop, observe/archive, finding, target, spending and both fix-cap guards remain in force.
+ Account review policy now matches only stable IDs: a numeric username cannot impersonate another
+ actor's stored ID. 64 compiling production mutations have exactly one selected assertion
+ failure each and byte-identical scratch restoration with a passing baseline. A malformed-user
+ fixture initially masked its guard with a second missing field; the repaired fixture preserves
+ valid repository metadata and kills exactly the intended identity mutant. 3186 Java tests,
+ packaging and pinned Semgrep passed. Criteria 6 and 7 were independently accepted; criterion 5
+ has automated proof. No production migration, live credential elevation or second dump was needed.
+
+- **Factory M3 slice 5 — first durable tracker ticket (PR #153, 2026-09-13).**
+ The GitHub work-source arm shares the existing pinned read transport and authentication, with
+ writes on a separate facade. The Apache-2.0 SPI remains framework-free. Explicit source and
+ repository bindings, source-owned resolved actor IDs, immutable profiles and a repository
+ ceiling govern signed issue intake and scans. Current-label authority requires complete audit
+ evidence; an allowed actor hint with UNATTRIBUTED origin still grants nothing. Membership,
+ attribution and genuinely missing identity have separate service cases. Deleting membership
+ or attribution fails only its named test across the complete intake class.
+ Events, projection, dedupe and encrypted outbox commit together. The rollback mutant commits
+ an orphan event on a separate transaction, which the test detects after an injected projection
+ failure. Real Kafka tests require committed consumer offsets before cleanup, including the
+ duplicate delivery; merely seeing the first row had allowed cleanup to race redelivery.
+ Work events have their own topics and cannot become review history. Work items displays
+ durable workflow, effective/admitted modes and label evidence; current title/body/status comes
+ from a separate tracker fetch. Removing a restrictive label cannot widen the combined policy
+ retained at admission. Missing specification/approval execution stays visibly unavailable.
+ Round 7 independently accepted criterion 5 and found that a 20-second permission read held
+ repository/account locks. Short revision snapshots now bracket the bounded remote call;
+ rotation/rebinding completes while the forge is still waiting and invalidates its answer.
+ The carry has seven mutation proofs, including deleting the re-read. No shared dev database,
+ live token privileges, backup or run worker was changed. Live tracker/token behavior, parity,
+ uncertain remote writes and bounded recovery of slow scan pages remain explicit later work.
+ Validation: 3371 Java tests / 393 suites / 28 modules, zero failures and 1
+ existing Windows skip; 650 full UI tests plus 12 final targeted tests; packaging and pinned
+ Semgrep passed. 154 mutation checks cover 153 distinct production changes, including
+ Java, schema and UI guards. Scope validation also accepts valid dot-prefixed GitHub names
+ while refusing dot path segments. Criterion 3 awaits independent review.
+
+- **M3 round 8 — route smoke-test correction (2026-09-13).** Criterion 3 was independently
+ accepted, bringing the count to four of seven. Dashboard CI exposed a route fixture missing
+ required policy fields; the test could pass against its initial wrapper before the payload
+ rendered because it awaited the shell loading text rather than the detail responses. The
+ same exception was reproduced in isolation, so a single isolated pass did not establish
+ mock leakage. The fixture now satisfies the API type and both response headings must render
+ before the unchanged `.content` assertion. Each case gets fresh session/storage/globals and
+ unmounts before global teardown. The complete file passed 35/35 in normal order and with
+ shuffle seeds 42, 99 and 5191448392. Removing the production wrapper failed exactly the
+ retained assertion; scratch-byte restoration passed. Final full UI: 650/650, no unhandled
+ errors, production build passed. The prior full UI run predated the last display additions;
+ targeted tests had missed this fixture. Evidence: `.claude/reviews/global/factory-m3-round8.md`.
+
+- **Factory M3 slice 6 — source parity and recovery (PR #153, 2026-09-13).**
+ Round 9 independently accepted the pending-effects teardown correction on d426501c, with
+ 650 UI tests and the retained mutation-verified route wrapper assertion. Criteria 3, 5, 6
+ and 7 remain four of seven. GitLab and Jira now share the work-source contract through the
+ existing pinned context transports and separate writers. GitLab has authenticated issue hooks;
+ Jira polls, binds actual Cloud accountIds and refuses unsupported Data Center attribution.
+ Both new arms separately prove unlisted and unattributed labels grant nothing, with the
+ unattributed hint deliberately passing membership. Source settings expose explicit mapping,
+ resolved actor entry, enablement, rescans and measured capabilities.
+ V65 stages coordinates and commits each reconciliation with its checkpoint. Actual packaged
+ JVMs are killed between pages and mid-page; restart admits each item exactly once without
+ rereading completed coordinates. V66 encrypts tracker effects separately from Kafka outbox
+ events. It commits uncertainty before HTTP, checks current policy/local revisions and never
+ blindly resends. A successful remote POST followed by client timeout recovers through its
+ marker with exactly one POST. A second connection observes the durable claim during HTTP.
+ Mutation work exposed a Jira offset fixture whose second inconsistent completion field
+ masked its guard; every other completion fact now agrees. An explicit Java-test Semgrep
+ scan exposed a computed ProcessBuilder executable; literal java plus the Gradle-selected
+ toolchain PATH removed the pattern without a suppression. 3545 Java tests, 675 UI tests,
+ packaging and pinned Semgrep passed. 142 checks cover 140 distinct production mutations.
+ No dev database, backup, live tracker write or run worker was used. Per-arm live forge/token
+ limits and the exact TEST cleanup remain explicit in UNVERIFIED and the slice review notes.
+
+- **Factory M3 slice 7 — visible policy bounds and durable approvals (PR #153, 2026-09-14).**
+ Round 10 accepted slice 6. GitHub now inherits the shared parity cases; an architecture test
+ derives adapter modules and fails when Jira loses inheritance. Profiles bound every mode and
+ numeric cap by all eligible labels, admission and the current ceiling, with cumulative protected
+ paths. ADR-045 states the invariant and resolves the FR-F22/F25 classification conflict.
+ V67 persists visible clamp milestones. Separate meet, silent-write and empty-message mutants
+ fail the named criterion 2 tests. A running SPEC result under a newly disabled PLAN persists
+ plan_off without another attempt or PR; a gate policy revision change requires a new decision
+ even when every mode and cap remains identical. Both criterion 4 mutants are discriminating.
+ V68 binds versioned gates and attempts, encrypted notes, usage and reservations. Dashboard
+ answers and expiry serialize on the aggregate; late/conflicting answers refuse, winning retries
+ are idempotent, and expiry releases the reservation and invalidates pending effects. One child
+ JVM itself opens the gate, is killed while OPEN, and a second expires it. Approvals, policy
+ settings, detail and current attention conditions expose the durable results.
+ 3649 Java tests, 701 UI tests, packaging and pinned Semgrep passed. 123 mutation checks
+ cover 120 distinct production changes and the authorized inheritance check. Criteria 2 and 4
+ are ready for independent review; 3, 5, 6 and 7 remain verified. Artifact/build handoff remains
+ slice 8. No dev data, backup, live tracker write or live run worker was used.
+
+- **Factory M3 slice 8a — prepared tasks and distinct build journeys (PR #153, 2026-09-14).**
+ Round 11 accepted slice 7 with no findings and independently verified criteria 2 and 4,
+ bringing the count to six of seven. Humans register actual tracker specification and single-step
+ plan versions. The dashboard reads the specification digest before the plan exists and supplies
+ its JSON format; all three tracker arms resolve stable scoped identities through their selected
+ source. Artifact bodies remain transient. The existing phase policy accepts manual evidence,
+ binds approvals to artifact versions and build coordinates, and keeps its real approval branch.
+ Suggest stops before build with zero runs; assisted opens a durable plan gate with zero runs,
+ then records its human answer and admits one build; autonomous admits one build without a gate.
+ The UI proof receives no profile names and exposes actual phases, gates, decisions and runs.
+ V69 joins a phase attempt to its M2 run and commits uncertainty before broker dispatch. Dispatch
+ compares current authority with the original attempt decision, so intake redelivery cannot
+ authorize a pending build after a lowered ceiling. Encrypted terminal-result inboxes recover
+ across the completion/acknowledgement boundary; duplicate old results cannot change a new phase
+ attempt. Usage survives readmission, unknown potential spend blocks, and M2's narrow pre-agent
+ failure classification permits repair without inventing a purchased call. Associated runs cannot
+ enter standalone automatic PR creation. Verification caught an overflowing JSON schema version,
+ the pending-dispatch policy race, absent-charge accounting that would block a proven pre-agent
+ failure, and a late deployment validation error that could escape instead of refusing a pending
+ build durably. Production item execution remains unavailable until the slice 8b publication hold;
+ the policy proof explicitly supplies a test transport. No M4 verifier, real-container item build,
+ draft delivery or live-forge journey is claimed by 8a. 3739 Java tests, 711 UI tests, packaging and
+ pinned Semgrep passed. 92 checks cover 92 distinct production mutations. Criterion 1 is ready
+ for independent review. Evidence is in `.claude/reviews/global/factory-m3-slice8a.md`. No dev data,
+ second backup or live worker was used.
+
+- **Factory M3 slice 8b — held publication and observed delivery (PR #153, 2026-09-14).**
+ Round 12 independently verified criterion 1 on d9861bc3; all seven acceptance criteria are
+ proved. The review accepted five independent journey axes and the assertions that history
+ persists artifact references while manual acceptance cannot invent executor completion.
+ Item execution now uses a distinct held command and publisher entry point. The worker persists
+ encrypted execution, topology and independent readiness/terminal acknowledgements. It checkpoints
+ the real head without pushing, stops active compute and retains its workspace through restart and
+ orphan recovery. A current short-lived delivery permit resumes only the trusted publisher with
+ fresh SCM credentials, exact head and both original/current protected-path floors. Production
+ VERIFY and LAND remain unavailable. Native draft delivery is requested and observed across all
+ three adapters; REVIEW consumes the existing review at the exact reviewed and posted head.
+ An ambiguous PR creation recovers only by reading; a stale publisher reader cannot rewind its
+ durable proposal claim. Late paid usage survives stop/readmission without completing a newer
+ attempt. Separate worker results share the existing M2 charge identity.
+ Real local-origin execution and three worker JVMs prove recovery after readiness, after a
+ durable publication claim before IO, and after a real push before terminal commit, with one
+ build. Verification exposed missing late accounting, checkpoint loss after an early terminal
+ result, a stale proposal race, rotated-credential failure redaction and two unmapped publisher
+ causes. The producer-derived cause inventory caught the last pair during the full fast suite;
+ both now retain paid-build accounting and refuse retry of the same publication permit.
+ Forced fast/service suites and packaging passed sequentially: 3970 Java tests, 714 UI tests,
+ 248 checks covering 245 distinct production mutations, and zero pinned Semgrep findings across
+ 98 files. The live orchestrator was refreshed from the tested source after Docker tests exited;
+ its repository/account inventory is unchanged. Live ingress required an explicit repair of the
+ GitHub webhook's missing origin. Two older PRs refused missing historical finding/fork metadata
+ without dispatch or cap changes. Fresh TEST PR #32 then completed the standalone `/fix` chain:
+ run `4003204361:1` automatically pushed `264ff858b538a3c779cf94161d3bc601bcfcf69a`, the next
+ review completed on that head, and both GitHub and the persisted finding recorded resolution.
+ The user started the worker after the service tier exited and was notified when the proof ended.
+ Cleanup closed the TEST PR and deleted its exact source branch, retaining the review/run audit.
+ Evidence: `.claude/reviews/global/factory-m3-slice8b.md`. No second backup was taken.
+
+- **Factory M3 final implementation and handoff (PR #153, 2026-09-14).**
+ All seven acceptance criteria were independently verified through round 12; round 16 accepted
+ slice 9 with no findings. The consolidated acceptance record maps each criterion to its slice,
+ exact witnesses and mutation ledger. Shared table/form/empty-state styling was accepted after
+ operator inspection, with route-derived guards and save lock-out preserved.
+ External gate answers share ResolveGate while retaining distinct channel authority. Ordinary
+ approving prose cannot approve. Current named native reviews bind the linked head, human ID and
+ measured push permission; dismissed/stale approvals refuse. Recorded bot IDs survive renames
+ and rotation, while a human wearing the bot's display name still takes over. Takeover supersedes
+ gates, invalidates unstarted effects and durably holds publication. A real worker JVM is killed
+ after revocation commits but before compute stops; a fresh permit and watchdog cannot publish
+ without an M1 cancel claim masking the proof. Concurrent in-flight PR recovery records one
+ observed outcome and keeps the item suspended. Operator resume requires current evidence and
+ an authenticated, versioned action with a note. Retired items cannot resume.
+ Slice 10 ran AccountWorkspaceIsUnusedTest before the drop and independently re-killed its read,
+ INSERT and UPDATE arms. V72 explicitly removes only scm_provider.workspace. A populated private
+ V71→V72 migration preserves every other account field, ciphertext/AAD, bindings, context
+ references and immutable legacy mapping rows; replacing DROP with SELECT 1 fails its assertion.
+ The fresh .handoff backup was verified at 2026-09-14T14:10:32.6500742+00:00 before dev migration.
+ Dev's full inventory remains 6 accounts / 38 reviews / 93 findings / 15 runs / 3 hooks.
+ The accepted PR #32 audit explains +1 review, findings 152–159 and run 4003204361:1; excluding
+ only those recorded proof rows, the original 6/37/85/14/3 baseline still matches. All 9 encrypted
+ credential/reference entries and 12 webhook entries remain identical after decryption.
+ Final forced fast/service tiers and packaging passed sequentially: 4076 Java tests in
+ 452 suites and 30 modules, zero failures/errors and 1 existing Windows symlink skip.
+ The full UI passed 730 tests in 92 files, TypeScript and production build. Slice 9 records
+ 78 distinct production mutations; slice 10 records four. Pinned Semgrep reports zero findings
+ across 5 changed code files. Earlier per-slice counts remain in
+ docs/factory/M3-ACCEPTANCE.md without an inflated cross-slice distinct total.
+ **Production VERIFY and LAND remain unavailable. M4 owns the verifier; M3 does not ship one.**
+ **No live item-build proof:** the accepted TEST PR #32 run proves standalone /fix only.
+ **The automated GitLab run-unit gap remains open:** RunUnitSpec has no network field and cannot
+ reach the e2e stack's GitLab; a live GitHub run does not close it. **Both factory images remain
+ absent from GHCR.** Separate per-forge identity/permission UNVERIFIED entries remain unchanged.
+ No warm GitLab e2e stack was available for this handoff. No new live canary or dev run worker
+ was started. PR #153 remains draft for final operator review; no merge is claimed.
+ Evidence: .claude/reviews/global/factory-m3-slice10.md.
+
+- **Factory M3 operator setup usability (PR #153, round 18, 2026-09-14).**
+ Work policy/source pages now open on lists with Add actions, and saved records appear with
+ confirmation. Source repository choices explain account prerequisites and origin mismatches;
+ configured-account pickers reuse accountOptionLabel. Field help, required/optional/automatic
+ markers and the policy nav icon complete the operator setup fixes while preserving save locks.
+ WIDGETS.md inventories shared controls. Route-derived guards now preserve quoted wildcard paths;
+ a previously surviving profile-table mutation exposed that comment-parser gap and now fails.
+ Measured 742 UI tests in 93 files, shuffled setup/routes, TypeScript/build, 28 production
+ mutations and clean Semgrep. Eight browser screenshots use intercepted TEST-only data.
+ All seven accepted criteria and the separate production/live-proof limits remain unchanged.
+ Evidence: .claude/reviews/global/factory-m3-round18.md.
diff --git a/docs/SCM-MAPPING.md b/docs/SCM-MAPPING.md
index aa92b17f..40c4cdec 100644
--- a/docs/SCM-MAPPING.md
+++ b/docs/SCM-MAPPING.md
@@ -171,6 +171,52 @@ worse than no mark, because a consumer learns to trust it and is then silently w
The mark is a fixed marker at the top of the description instead, written by the orchestrator, and
it is identical on all four.
+## People directories (M3 slice 3)
+
+| Forge | Lookup and refresh | Capability |
+|---|---|---|
+| GitHub | GET /users/{login}; GET /user/{id} | Exact login, then durable numeric ID; visibility depends on selected credential. |
+| GitLab | GET /users?username=; GET /users/{id} | Exact username, refuse ambiguous/incomplete replies. |
+| Bitbucket Cloud | Workspace members; GET /users/{account_id} | Explicit member selection across bounded pages; workspace/user-read access required. Nicknames are not unique. |
+| Jira Cloud | GET /rest/api/3/user/search; GET /rest/api/3/user?accountId= | Explicit candidate selection; Browse users and groups plus user-read access. |
+| Jira Data Center | Unsupported person lookup | Existing context-read support does not imply Cloud accountId support. |
+
+Every save repeats resolution through the same configured account, verifies the returned stable
+ID, and stores observed labels separately. Identity reads reject redirects to another origin.
+Per-forge documentation links and measured-versus-live limits are recorded separately in
+UNVERIFIED. No source allowlist inheritance is implied by reusable account person controls.
+
+## Effective repository push permission (M3 slice 4)
+
+| Forge | Read | Binding and interpretation |
+|---|---|---|
+| GitHub | GET /repos/{owner}/{repo}/collaborators/{username}/permission | Resolve username afresh by stable ID; verify response user.id. Base write/admin allows; read/none refuses; other base roles are unknown. |
+| GitLab | GET /projects/{encoded namespace/repo}/members/all/{id} | Verify returned ID and active state. Known 30/40/50 allows; known read levels refuse. Expired membership refuses; unfamiliar shapes/levels are unknown. |
+| Bitbucket Cloud | GET /workspaces/{workspace}/permissions/repositories/{slug} | Verify account_id via user read, match UUID and repository full_name across all pages. write/admin allows, read refuses. Caller requires repository-admin access. |
+
+Permission reads pin the configured origin and never try another credential. Unknown, forbidden,
+rate-limited, timed-out or incomplete reads refuse; a 404 is not treated as known absence. A
+complete Bitbucket list without the actor is known no-write. The permission says general code
+write, not that a protected target branch will accept a push. Per-forge live limitations are in
+UNVERIFIED; fixture measurements do not establish live token capability.
+
+## External work answers and activity (M3 slice 9)
+
+| Channel | Implemented evidence | Limit |
+|---|---|---|
+| GitHub native PR review | Re-read named review, complete bounded chronological review history and current open PR/head; stable user ID, human type and measured push permission without DENY | Can answer an open linked LAND gate only; production LAND remains unavailable. Controlled-response tests, no live gate proof |
+| GitLab / Bitbucket native PR review | Capability unavailable in the current approval port composition | Visibly disabled; dashboard and tracker remain usable |
+| GitHub / GitLab tracker comment | Authenticated created-comment delivery with stable actor ID, source membership and explicit gate/generation/artifact command | Ordinary approving text is not an approval; system/edited GitLab notes are excluded |
+| Jira Cloud tracker comment | Authenticated REST v2 comment polling using accountId, timestamp and explicit command | Ignore undated/pre-admission comments; bounded pages report a polling failure when incomplete |
+| Jira Data Center tracker comment | Unavailable | Context-read support does not establish Cloud person identity |
+
+SCM takeover adapters use signed actor fields, not commit-author or display text. Repository and
+linked PR/branch coordinates must match. Recorded factory/reviewer identities remain authoritative
+after rename or rotation; tracker identity is a separate namespace. Unknown actors suspend
+conservatively. Gate answers and authorized /fix commands precede generic comment takeover.
+No native approval can resume a suspended item. These are automated provider-response proofs;
+the separate identity/permission and live activity limits remain in UNVERIFIED.
+
## Sources
Bitbucket Cloud: developer.atlassian.com/cloud/bitbucket/rest + support.atlassian.com event-payloads ·
GitHub: docs.github.com/rest/pulls · GitLab: docs.gitlab.com/api/merge_requests, /discussions ·
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index f098af0b..d8d9fc21 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -3,6 +3,27 @@
Decided model (ADR-009). Clean-room, OSS-standard, zero code copied from any private source.
Trust boundaries, authn/authz, encryption, and secrets.
+## M3 workflow authority
+
+Repository role bindings select credentials; an account's former workspace is not authority and
+V72 removes that unused column. Source actor allowlists remain independent of reviewer lists.
+Unknown label attribution selects no profile even when an accompanying actor hint is allowed.
+Every eligible label, the pinned admission policy and the current ceiling restrict continuation.
+
+Dashboard gate answers use the verified operator subject. Tracker answers require authenticated
+delivery, this source's membership and an explicit command binding gate, generation and artifact.
+GitHub native PR approval additionally re-reads the named latest decisive review, linked current
+head, human identity and measured push permission without DENY. Unsupported native channels are
+visibly unavailable. These authorities converge on ResolveGate; they are not interchangeable IDs.
+
+Takeover compares stable machine identities recorded at admission/dispatch, independently of
+renames or later credential rotation. Its durable exact-binding publication revocation commits
+before compute is stopped and survives process death without relying on M1 cancellation. A
+publication already in progress cannot be recalled. Resume requires a verified operator, expected
+revision and note and re-observes current evidence. Retired items cannot resume. Production VERIFY
+and LAND remain unavailable. [M3 acceptance](factory/M3-ACCEPTANCE.md) and the separate per-forge
+[UNVERIFIED entries](UNVERIFIED.md) distinguish automated evidence from live deployment limits.
+
## Actors & trust boundaries
| Actor | Boundary crossed | How it's trusted |
diff --git a/docs/SMOKE-TEST.md b/docs/SMOKE-TEST.md
index ea9c45dc..e9829fa7 100644
--- a/docs/SMOKE-TEST.md
+++ b/docs/SMOKE-TEST.md
@@ -1,5 +1,111 @@
# Smoke Test Runbook
+## M3 final upgrade (slice 10)
+
+**Production VERIFY and LAND remain unavailable. No live item-build proof is claimed.** The
+standalone /fix proof on TEST PR #32 is separate from the real-container/local-origin item tests.
+The automated GitLab run-unit network gap remains open, both factory images remain absent from
+GHCR, and per-forge identity/permission limits remain in [UNVERIFIED](UNVERIFIED.md). The
+[acceptance record](factory/M3-ACCEPTANCE.md) names all seven independently verified criteria.
+
+V72 explicitly drops scm_provider.workspace, retained through slice 9 as rollback evidence.
+Before that migration reaches an existing database:
+
+1. Run `:spire-arch:test --tests dev.codespire.arch.AccountWorkspaceIsUnusedTest --rerun-tasks`
+ with Java 25. It checks production reads, INSERTs, UPDATEs, aliases and account models.
+2. Take a **fresh** custom-format backup to the worktree's git-ignored .handoff directory, validate
+ its archive listing and record its hash. Keep the matching encryption keysets outside git.
+ The old slice 1 backup does not satisfy this final-upgrade prerequisite. This PowerShell command
+ preserves binary bytes and refuses to overwrite an existing file:
+
+```powershell
+$m3Handoff = Join-Path (Get-Location).Path '.handoff'
+[void](New-Item -ItemType Directory -Force -Path $m3Handoff)
+$m3Dump = Join-Path $m3Handoff ('m3-before-slice10-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.dump')
+$m3Process = [Diagnostics.Process]::new()
+$m3Process.StartInfo = [Diagnostics.ProcessStartInfo]::new('docker')
+$m3Process.StartInfo.UseShellExecute = $false
+$m3Process.StartInfo.RedirectStandardOutput = $true
+@('exec','spire-postgres','sh','-c','exec pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc') | ForEach-Object { $m3Process.StartInfo.ArgumentList.Add($_) }
+$m3File = [IO.File]::Open($m3Dump, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write)
+try { [void]$m3Process.Start(); $m3Process.StandardOutput.BaseStream.CopyTo($m3File); $m3Process.WaitForExit(); if ($m3Process.ExitCode -ne 0) { throw 'pg_dump failed' } } finally { $m3File.Dispose(); $m3Process.Dispose() }
+Get-FileHash -LiteralPath $m3Dump -Algorithm SHA256
+docker run --rm --mount "type=bind,source=$m3Handoff,target=/backup,readonly" postgres:18.4-alpine pg_restore --list "/backup/$([IO.Path]::GetFileName($m3Dump))"
+if ($LASTEXITCODE -ne 0) { throw 'Backup archive validation failed' }
+```
+
+3. Record the actual counts of orchestrator.scm_provider, review_status, review_finding,
+ factory_run and gateway.webhook_repo before the upgrade. Compare the original encrypted
+ credential/reference and webhook baselines with scripts/verify-dev-credential-continuity.ps1
+ in Compare mode. Do not replace those baselines or delete accepted proof history to match
+ an older count. The accepted PR #32 proof accounts for +1 review, +8 findings and +1 run.
+4. Upgrade the gateway, orchestrator, review worker and UI using the tested source. External Kafka
+ deployments must provision the work/run topics and ACLs in [CONTRACT](CONTRACT.md). Keep the
+ development run worker stopped while Docker-driving test tiers run. No compose down is needed.
+5. Require V72 success in orchestrator.flyway_schema_history and zero information_schema.columns
+ rows for orchestrator.scm_provider.workspace. Re-run the five counts and both encrypted
+ continuity comparisons; inspect readiness and startup errors before declaring the upgrade done.
+
+AccountWorkspaceDropMigrationTest applies the real V71→V72 migration to a populated private schema.
+It checks every remaining account field, unchanged ciphertext/AAD, repository bindings, context
+references and legacy mapping evidence. Replacing the production DROP with SELECT 1 fails the
+column-absence assertion. The shared dev database is never a mutation target.
+
+For supported journey checks, use the existing repository/source/policy screens and inspect actual
+phase, gate, decision and run fields. Suggest stops before BUILD; assisted opens PLAN approval;
+autonomous admits a prepared build. Missing artifacts, unavailable capabilities and suspended
+items must remain visible. Tracker answers require `/approve `
+or `/reject` with the same bindings; `-` explicitly binds no artifact. Ordinary comments do not
+approve. A suspended item's resume needs a note and fresh evidence; a retired item cannot resume.
+Any new live TEST canary requires its exact identity and cleanup to be recorded before insertion.
+
+## M3 repository cutover upgrade (slices 1–2)
+
+Before rebuilding dev, follow slice 1 in
+[`2026-09-12-factory-m3-work-items.md`](superpowers/plans/2026-09-12-factory-m3-work-items.md).
+The verified `.handoff/spire-dev-pre-m3-2026-09-13.dump` satisfied that historical cutover prerequisite;
+slice 10 requires the fresh backup above. `.handoff/m3-real-credentials.bin` holds encrypted baseline evidence
+for 9 real credential/reference entries. Keep the matching keysets outside git. Slice 2 must
+rerun the probe's Compare mode on the real upgraded rows.
+
+The first upgraded gateway enqueues existing registrations in V3. Legacy registrations with no
+recorded origin require explicit mapping; workspace alone is not host evidence. The gateway API
+accepts an optional `forgeOrigin`, preserving it when an old client edits other fields.
+The orchestrator applies V60,
+consumes `cs.registry-integration`, and sweeps existing history. Topic auto-creation works in the
+bundled broker; external brokers must provision the topic and ACLs first. Inspect pending broker
+records in the existing DLQ screen and replay after fixing the reported cause.
+
+Open Settings → Repositories → Registered repositories and accounts. Confirm the real workspace,
+forge origin and explicit reviewer/factory selections. Pending mappings name the registration and
+target; verify the host, register the matching repository if needed, then explicitly link it.
+Slice 2 switches pipeline routing to explicit repository bindings. Do not invent accounts or runs in this live stack to
+test registration: `RepositoryResourceTest` exercises the real HTTP API in Dev Services.
+
+The migration tests upgrade populated private schemas and prove ciphertext/UUID/reference/key
+preservation. Broker tests separately exercise the gateway publisher and orchestrator consumer;
+they do not claim a live multi-service rollout. The production account workspace remains populated
+through slice 2 as rollback evidence and is removed only in slice 10.
+
+For slice 2, provision `cs.repository-integration` and `cs.repository-activity` with their ACLs
+alongside the metadata topic. Rebuild **gateway first**, then orchestrator, then UI, using the
+same Compose overlays and `--build --no-deps`. New gateway deliveries wait on the new topic
+until the upgraded orchestrator consumes them. Raw old-topic SCM messages enter the DLQ with
+a provenance reason; redeliver through their verified registration after repair.
+
+Accounts no longer have workspace. Register the repository's kind, canonical forge origin,
+workspace and slug, then select REVIEWER and FACTORY accounts on that repository. Save the UUID
+returned by `/api/repositories`; manual registration without a URL and `POST /api/runs` require
+`repositoryId`. Hooks are optional. A repository has one hook per event kind; the existing
+REVIEWER key and secret survive upgrade. Pending gateway registration repair updates that
+retained registration's origin and repository association; history-only repair stays in the
+orchestrator. Confirm the real origin before selecting it. An unknown origin cannot route.
+
+After rebuilding, compare `.handoff/m3-real-credentials.bin` using the existing probe and compare
+`.handoff/m3-real-webhooks.bin` with `-Scope Webhooks`. Assert counts 6/37/85/14/3 for accounts,
+reviews, findings, runs and hooks, plus unchanged retained workspaces and hook rejection metadata.
+No run worker is needed for this slice; the live run proof waits until slice 8b.
+
**A** stub pipeline, zero external accounts; **B** real Bitbucket Cloud PR (webhook);
**C** real GitHub PR via manual Register PR (no webhook); **D** real GitLab MR via manual Register
PR (no webhook); **E** real GitHub PR via webhook (Tailscale Funnel); **F** real GitLab MR via
@@ -30,7 +136,10 @@ docker ps --filter name=spire # both should show (healthy)
```
Open **http://localhost:34080**, flip the **Review-mode** slider in the sidebar to **active**
-(a fresh DB seeds to *observe*), then press **Simulate PR**.
+(a fresh DB seeds to *observe*). On this disposable stub stack, register a `TEST-` reviewer account
+and repository with a workspace beginning `TEST-`, and bind that reviewer. Then call
+`POST /dev/simulate-pr?repositoryId=`. The simulator refuses
+real SCM mode, an absent repository UUID and namespaces without that test prefix.
**Expected:** the timeline animates through
`PullRequestEventReceived -> ReviewRequested -> FetchDiff -> DiffFetched -> GatherContext ->
@@ -190,13 +299,14 @@ summary comments — but the PR is registered manually through the dashboard ins
arriving on a webhook. Works for any registered provider (used here with GitHub).
**Gateway is not needed** — the orchestrator's `POST /api/reviews/register` publishes the
-same `PullRequestEventReceived` the gateway webhook would, onto the same `cs.integration`
+same `PullRequestEventReceived` in a provenance envelope, onto `cs.repository-integration`
topic. Minimal set: Postgres + Redpanda + **orchestrator + worker**.
### 1. One-time prerequisites
-1. Register a **GitHub account** in Settings → Accounts (role Reviewer; workspace = repo owner, e.g.
- `artyomsv`) with a token scoped **Contents: Read** + **Pull requests: Read and write**.
+1. Register a **GitHub account** in Settings → Accounts (role Reviewer) with a token scoped
+ **Contents: Read** + **Pull requests: Read and write**. Register the repository separately with
+ forge origin `https://api.github.com`, workspace = repo owner and its slug, then select that account.
Leave "Bot account id" blank — it is resolved from the token on save (`IdentitySource`).
2. In Settings → LLM (ADR-018): first **add a model** (e.g. name `gpt-4o`, input `$2.50` / output
`$10.00` per 1M tokens — from OpenAI's pricing page), then register an **LLM provider**: type
@@ -232,7 +342,7 @@ nothing to anchor inline comments to — the summary still posts). Then either:
```bash
curl -s -X POST http://localhost:34080/api/reviews/register \
-H 'Content-Type: application/json' \
- -d '{"workspace":"","slug":"","pr":}'
+ -d '{"repositoryId":"","pr":}'
# → {"reviewId":"review::/#", ...}
```
@@ -1650,13 +1760,13 @@ against a forge, authenticated as a machine account.
```bash
curl -sS -X POST http://localhost:34080/api/providers -H 'content-type: application/json' -d '{
- "name":"factory-bot","type":"github","baseUrl":"https://api.github.com","workspace":"",
+ "name":"factory-bot","type":"github","baseUrl":"https://api.github.com",
"authKind":"bearer","secret":"","enabled":true,"authors":[],
"botUsername":"","role":"FACTORY"}'
```
- A workspace may hold a `REVIEWER` row and a `FACTORY` row side by side; the role is part of every
- lookup's key, so neither path can be handed the other's token.
+ Select that account in the registered repository's FACTORY slot. The REVIEWER and FACTORY
+ bindings are independent, so neither dispatch path can be handed the other's token.
5. **The harness credential pool.** The run's model key comes from the factory's OWN pool, never
from the LLM provider registry the reviewer uses. There is no fallback: with an empty pool the
@@ -1696,7 +1806,7 @@ against a forge, authenticated as a machine account.
```bash
curl -sS -X POST http://localhost:34080/api/runs -H 'content-type: application/json' -d '{
- "workspace":"","slug":"","providerType":"github","baseCommit":"",
+ "repositoryId":"","baseCommit":"",
"subject":"m0-ordinary","harness":"codex","model":"gpt-5.6",
"prompt":"Add a file NOTES.md containing one line: hello from the factory. Commit it."}'
```
diff --git a/docs/UNVERIFIED.md b/docs/UNVERIFIED.md
index d6817e82..784b6559 100644
--- a/docs/UNVERIFIED.md
+++ b/docs/UNVERIFIED.md
@@ -33,6 +33,274 @@ evidence would settle it**.
---
+## External work approvals and takeover (M3 slice 9, 2026-09-14)
+
+Slice 9's native GitHub approval reads and GitHub/GitLab/Bitbucket activity payloads are tested
+against controlled provider responses. Tracker answers exercise the real PostgreSQL aggregate;
+Jira Cloud comment polling has adapter and aggregate tests. No live external gate answer or
+operator resume is claimed. GitLab and Bitbucket native approval channels are explicitly
+unavailable; Jira Data Center comment polling is unavailable. GitHub approval refuses incomplete
+review history after 20 pages. Jira processes individual comments and reports a polling failure
+if its 20-page bound is exceeded. Live provider evidence,
+including token visibility, dismissal delivery, issue transfer payloads and renamed account
+observations, remains necessary to establish those deployment paths.
+
+The publication revocation does have an actual killed-JVM proof against a real local Git origin:
+the hold commits before the process dies, an otherwise valid permit is refused without an M1
+cancel claim, and a fresh worker/watchdog cannot publish it. This is separate from slice 8b's
+accepted live standalone `/fix` proof. It does not establish atomic ordering with a human's remote
+push or guarantee recall of a publication already in progress.
+
+## Work-source GitHub label audit and tracker writes (2026-09-13)
+
+Slice 5 measures signed issue normalization, repository metadata and stable-ID checks, current
+labels, remove/re-add audit across pages, origin-bound pagination, incomplete-history refusal,
+separate comment/transition writes and acknowledgements against WireMock. The real PostgreSQL
+intake and real gateway Kafka route have automated proof; no dev work source or live tracker
+write was created for this slice. The shared client retains its existing `2022-11-28` API pin.
+[Issue timeline events](https://docs.github.com/en/rest/issues/timeline?apiVersion=2022-11-28)
+and [repository issues](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28)
+are the contracts checked on 2026-09-13. Token-family visibility, Enterprise variants, deletion/
+transfer confirmation and real label attribution still need live evidence. A 404/410 is treated
+as unavailable, not proof of deletion. A partial audit never grants authority.
+
+Slice 6 adds a real PostgreSQL tracker outbox and a WireMock write that succeeds remotely before
+the client times out. A fresh dispatcher reads the durable uncertain claim and finds the exact
+comment marker, with one POST in the server journal. Missing recovery evidence leaves the effect
+uncertain; it never authorizes another write. This has not been measured against a live GitHub
+token. Admission still emits only work notifications; later phase decisions enqueue tracker effects.
+
+## Work-source GitLab label audit, hooks and writes (2026-09-13)
+
+Measured against local WireMock GitLab REST v4 responses, real PostgreSQL intake and real gateway
+Kafka delivery: nested project scope, global issue identity, current labels, paginated additions/
+removals, authenticated Issue Hook deltas, allowed-person lookup, separate comment POST and
+close/reopen PUT. The unattributed intake fixture retains allowed actor hint 900123; incomplete
+audit selects nothing. No live GitLab ticket, token family or installation version was measured.
+The [label-event API](https://docs.gitlab.com/api/resource_label_events/),
+[issue API](https://docs.gitlab.com/api/issues/) and
+[Issue Hook contract](https://docs.gitlab.com/user/project/integrations/webhook_events/)
+were checked on 2026-09-13. Private/confidential visibility, PAT/project/group token permissions,
+self-hosted payload variants and real marker retention still need recorded live measurements.
+
+The shared context client supplies pinned authenticated reads; a separate writer uses the same
+connection configuration. A full page without completion evidence refuses rather than truncates.
+404/410 means unavailable. Transition recovery observes the requested state, so it proves the
+desired outcome, not which person changed it. A timeout with no confirming state remains uncertain.
+
+## Work-source Jira Cloud label audit and writes (2026-09-13)
+
+Measured against local WireMock Jira Cloud REST v2 responses and real PostgreSQL intake: explicit
+project-to-SCM mapping across origins, opaque search cursors, complete paginated changelog reads,
+actual label-set differences and author accountIds. Retained labels never acquire the editor's
+identity. Missing or incomplete audit selects nothing, including allowed actor hint 900123.
+The existing person directory requires explicit selection when a query is ambiguous; neither
+display name nor issue reporter supplies missing attribution. No live Jira Cloud site was measured.
+
+The [current search API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/)
+uses `/search/jql`; the [issue API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/)
+documents changelog label sets, real transition IDs, required fields, history metadata and 204
+transition success. Fixtures exercise separate comment writes and effect/transition metadata
+recovery. Real classic API-token/PAT permissions, changelog completeness under privacy restrictions,
+historyMetadata retention, workflow validators, rate limits and marker rendering remain unproven.
+Opaque search-token lifetime across long downtime still needs a real Cloud measurement; an
+expired/unusable cursor reports unavailable and does not advance.
+Jira uses polling; no unauthenticated webhook channel or approval parser is exposed.
+Scoped Atlassian gateway tokens remain unsupported by the existing site-host configuration.
+
+## Work-source Jira Data Center boundary (2026-09-13)
+
+WireMock Server/Data Center responses exercise project-scoped legacy search and ticket fetch.
+Capabilities expose polling and comments, but omit label audit and recoverable transitions.
+The existing identity adapter cannot confirm Cloud accountIds on this deployment; no DC username,
+user key or display-name fallback is invented. Thus polling can retain an unattributed work item
+but cannot select a profile. This is an explicit unsupported attribution path, not Cloud parity.
+No real Data Center version or token was measured. A versioned complete changelog contract,
+stable person lookup and live write/recovery evidence are required to expand these capabilities.
+
+## Work-item process recovery and execution boundary (2026-09-13)
+
+Slice 5 rebuilds a fresh store instance from the real encrypted event log, verifies no tracker
+content in the projection, and injects a PostgreSQL projection failure after event append. The
+separate-transaction mutant leaves an event behind and fails the rollback assertion. This is
+automated persistence/transaction evidence. Slice 6 additionally launches the packaged orchestrator
+in separate JVMs against isolated test PostgreSQL/Kafka and a WireMock GitHub tracker, forcibly kills it
+between pages and mid-page, and resumes from durable coordinates. Both tests assert two items,
+one history entry each, no skipped coordinate and no re-fetch of the committed page/item. This
+is an actual process-kill proof on a test stack, not a dev-stack restart or a live forge measurement.
+Candidate reconciliation and checkpoint removal share one transaction. A sweep processes at most
+ten coordinates and stops at a committed boundary after its 20-second loop deadline; an in-flight
+observation has its own 20-second bound. Listing is separately bounded to 20 seconds. No global
+20/30-second sweep duration is claimed. Live tracker deletion/reordering during pagination remains
+unproven; scheduled full rescans provide eventual revisitation, not a remote snapshot guarantee.
+
+Slice 7 adds numeric caps, cumulative protected paths and dashboard gates. A packaged child JVM
+admits the TEST ticket and opens its gate; the test kills that JVM while the gate is OPEN. A second
+packaged JVM expires the persisted gate and releases its reservation. Real test-stack HTTP reads
+prove the gate's open view, disappearance after expiry and durable detail reason. This proves
+process recovery against isolated PostgreSQL/Kafka and WireMock, not live OIDC or a live tracker.
+Recovery children use separate TEST incoming topics and consumer groups so a killed child cannot
+retain the parent test's partitions. Kafka Admin verifies actual assignments while both gate JVMs
+are alive; the normal work-consumer duplicate-acknowledgement test remains separate and unchanged.
+
+Policy tests explicitly supply a test execution capability to record phase attempts and deliver
+their results through the internal transition service. Production reports missing executors as
+unavailable. Slice 8a fetches and validates manual tracker specifications and single-step plans,
+binds their identities/digests and build coordinates to approvals, and reuses M2 assembly for one
+prepared build. Local GitHub/GitLab/Jira fixtures establish reference resolution; they establish
+no live tracker-artifact journey. Generated specification, multi-step planning and verification
+executors remain M4.
+
+The three-profile slice 8a proof replaces the final broker emitter. It runs the real state machine,
+encrypted PostgreSQL history, M2 assembly, launcher and durable dispatch/result association. That
+test does not execute an agent or push a branch. Slice 8b separately exercises actual held builds
+in `WorkItemRunJourneyIT` against real containers and a local smart-HTTP origin, with an isolated
+orchestrator JVM and provider fixture. It observes the checkpoint and one charge; the remote branch
+is absent and production VERIFY remains capability-unavailable. Draft/regular delivery tests use
+an explicitly TEST-only verification driver and real sink adapters against WireMock. They establish
+native request/response handling, not live draft support or a shipped verifier. No live tracker-artifact
+journey, live item publication, M4 verification or merge follows from these local proofs.
+
+Result-inbox tests stage both sides of the aggregate-commit/acknowledgement boundary against real
+PostgreSQL and recover without another completion or charge. Those are staged durable-state tests.
+`WorkRunProcessRecoveryIT` separately kills real worker JVMs after readiness, after the publisher
+claim before IO, and after remote publication before terminal commit. New owners retain the hold,
+resume only the original publisher and observe one build on the actual local remote. Runtime tests
+also retain the hold after partial creation and after containers are lost while volumes survive.
+A claimed send with no known outcome stays uncertain until a result or explicit never-ran resolution
+arrives. The standalone `/fix` live regression passed on 2026-09-14: TEST PR #32, run
+`4003204361:1`, automatic source-branch push `264ff858b538a3c779cf94161d3bc601bcfcf69a`,
+then a completed review, persisted `RESOLVED` verdict and resolved GitHub thread. The TEST PR was
+closed and its branch deleted. This does not establish live item publication or draft behavior.
+M2 reports one aggregate agent call per run;
+it does not count internal model calls. Proven pre-agent failures reuse M2's existing zero-call
+classification and can be readmitted. Unmeasured potential spend blocks continuation, including
+after readmission, until accounting can be repaired; no automated usage-repair workflow is supplied.
+The user started the live worker after the Docker service tier finished and was notified when
+the proof completed. Legacy preflight limits remain: PR #26 has an old finding row without a
+thread reference, and PR #27 has unknown fork metadata. Both refused without a run; neither row
+was manually rewritten. The GitHub webhook's missing origin was repaired through its gateway API
+to the existing repository and origin; GitLab/Bitbucket's two origin repairs remain pending.
+
+## Repository push permission — GitHub (2026-09-13)
+
+Measured against a local WireMock GitHub API: by-ID handle refresh followed by effective
+collaborator permission, matching returned user ID, inherited write access, reader refusal,
+unknown/custom base-role refusal, and 403/404/429/503 after a prior success. The real service
+uses its repository's selected reviewer credential; a stronger factory token is never tried.
+The [effective permission API](https://docs.github.com/en/rest/collaborators/collaborators#get-repository-permissions-for-a-user)
+reports the base role; custom role names do not establish write capability. A real token's
+metadata visibility, inherited organizational roles and Enterprise variants still need live
+permission measurements. Slice 3's live identity refresh establishes identity only.
+
+## Repository push permission — GitLab (2026-09-13)
+
+Measured against a local WireMock GitLab API: the all-members endpoint (including inherited
+membership), matching stable ID, active known Developer/Maintainer/Owner levels, reader refusal,
+expired membership, integer overflow, unknown roles, malformed responses and failed reads.
+[Project members](https://docs.gitlab.com/api/project_members/#retrieve-a-member-of-a-project)
+is the endpoint contract; [membership expiration](https://docs.gitlab.com/user/project/members/)
+removes access from the expiry date. The adapter compares that date in UTC. No live permission
+measurement on gitlab.com, git.epam.com or gitbud.epam.com is claimed; installation-specific
+custom roles, invited/private-group visibility and expiration timezone behavior need proof.
+
+## Repository push permission — Bitbucket Cloud (2026-09-13)
+
+Measured against a local WireMock Bitbucket Cloud API: account_id-to-UUID identity binding,
+effective repository rights across server-provided pagination cursors, inherited write access,
+reader refusal, foreign-origin page rejection, contradictory/malformed/incomplete pages and
+403/404/429/503 after a prior success. The [effective repository permission endpoint](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-workspace-permissions-repositories-repo-slug-get)
+requires a repository-admin caller. A failure reports that capability requirement explicitly;
+no factory-token fallback or token authority change occurs. Ten pages and the service's
+20-second total lookup budget are hard limits: reaching either without a complete answer is
+UNKNOWN and refuses. Live token families, group inheritance and account_id/UUID visibility
+remain unmeasured. An explicit override still cannot bypass target or spending guards.
+
+## People directory — GitHub identity behavior (2026-09-13)
+
+Measured against a local WireMock GitHub API with TEST-prefixed people and credentials:
+exact login matching, stable numeric ID selection, by-ID rename refresh, malformed/refused
+responses and rejection of foreign-origin redirects. The resource persistence test also uses
+this fixture through the configured account and actual PostgreSQL. The dev GitHub reviewer at
+api.github.com also refreshed its existing actor 3218389 by ID and resolved @artyomsv back to
+that ID on 2026-09-13; policy and revision were unchanged. A subsequent full orchestrator restart
+preserved the identical cached policy and observations without another refresh. This measures that configured credential,
+not other token families or Enterprise Managed User visibility. The documented
+[login and durable-ID endpoints](https://docs.github.com/en/rest/users/users) are the contract;
+a 404 can also mean the selected account cannot see the person.
+
+## People directory — GitLab identity behavior (2026-09-13)
+
+Measured against local WireMock, including the configured /api/v4 prefix in the resource suite:
+exact username filtering, ambiguous result refusal, stable-ID refresh and foreign-origin
+redirect refusal. No live measurement against gitlab.com, git.epam.com or gitbud.epam.com is
+claimed. [GitLab Users API](https://docs.gitlab.com/api/users/) distinguishes exact username
+filtering from fuzzy search; installation-specific visibility remains unmeasured.
+
+## People directory — Bitbucket Cloud identity behavior (2026-09-13)
+
+Measured against local WireMock: matching workspace members across pages, duplicate nickname
+selection, missing-scope capability errors, by-account-ID verification and foreign-origin redirect
+refusal. No live token was given more authority. [Workspace membership](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-workspace-members-get)
+requires workspace-read access; [user reads](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/)
+require user-read access. These operations and account_id availability still need live proof for
+each token family. Nicknames are not treated as exact handles. Ten member pages is a hard limit;
+exceeding it refuses selection. Effective push-permission reads additionally require repository-admin
+access and belong to slice 4, not to this identity proof.
+
+Atlassian's [identity privacy contract](https://developer.atlassian.com/cloud/bitbucket/bitbucket-api-changes-gdpr/)
+specifies account_id or UUID in user URLs and defines nicknames as non-unique. The adapter uses
+account_id consistently with the existing webhook author identity.
+
+## People directory — Jira Cloud identity behavior (2026-09-13)
+
+Measured against local WireMock: disambiguated accountId selection, refusal of responses without
+Cloud account IDs, inactive-user refusal, by-ID refresh and foreign-origin redirect refusal.
+The [search contract](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-user-search/#api-rest-api-3-user-search-get)
+and [by-ID contract](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get)
+require the applicable user-read scopes and Browse users and groups permission. Privacy can hide
+results; an empty search proves no visible match, not absence from the site. Up to 50 candidates
+are shown and the operator must select one. No live Jira Cloud installation was measured.
+Work-source UI wiring is a slice 5/6 dependency; it must use the source's explicit account.
+
+## People directory — Jira Data Center identity behavior (2026-09-13)
+
+Not implemented or measured against a live Data Center installation. The current person adapter
+uses Cloud accountId endpoints; it never treats a Data Center username or display name as a
+Cloud account ID. The UI states this capability limit. Existing Data Center context reads do
+not establish support for person lookup, and are unchanged.
+
+## Repository cutover — boundaries of the proof (ADR-042, 2026-09-13)
+
+- **Real-row continuity is measured.** Dev was rebuilt gateway first (V4), then orchestrator
+ (V61), then UI. All 9 account/context credential/reference entries and all 12 webhook entries
+ matched their encrypted baselines after cutover. Counts stayed 6 accounts, 37 reviews, 85
+ findings, 14 runs and 3 hooks; retained workspace values and rejection metadata were unchanged.
+ Authenticated serving reads matched all eight selected bindings. This establishes local
+ migration and credential continuity, not successful live forge commands from every entry point.
+- **Three webhook origins remain unconfirmed.** The Bitbucket artyomsv/pr-test, GitHub
+ artyomsv/spire-test and GitLab artyomsv-group/code-review-poc registrations still lack origin
+ metadata. A read-only webhook-settings probe returned 403, 404 and 403 respectively; none
+ establishes the host associated with a retained key. The UI can repair the owning gateway
+ registration after explicit origin selection. Until that confirmation, verified deliveries
+ remain Attention events and cannot dispatch. Matching a namespace alone is insufficient.
+- **The dispatch matrix is observed at the credential boundary.** Its decoys and real database
+ decryption establish repository/role selection for manual, rerun, conversation, prompt, run,
+ fix and proposal entry points. Separate choreography suites test later commands. No live
+ run worker or new spend was used in this slice; slice 8b retains its live proof.
+- **FACTORY and ISSUE consumers are later work.** FACTORY activity is isolated on
+ cs.repository-activity; the later work-item slice consumes it. ISSUE hooks require source
+ metadata and SCM ingress rejects them; native work-source intake is not claimed implemented.
+- **Token validation is not repository permission proof.** Bitbucket account-less validation
+ uses the selected repository's namespace for the existing fallback request. WireMock tests
+ establish same-kind/origin selection and account binding use; a successful workspace listing
+ does not prove access to every repository. The separate reviewer-access check targets the
+ chosen full repository path. Existing per-token-family scope gaps remain below.
+- **Custom forge URL mappings still need installation evidence.** Local tests map GitHub and
+ Bitbucket public web URLs to their API origins and preserve self-hosted origins; GitLab nested
+ namespaces preserve old review IDs and transport AADs. Custom web/API proxies must be checked
+ against the actual installation. No new native permission endpoint behavior is claimed.
+
## Accounts normalization — live evidence still needed (ADR-041, 2026-09-12)
Sources below were retrieved **2026-09-11**. WireMock checks prove how the application handles
@@ -271,14 +539,15 @@ Not work. Written down because each has been rediscovered at least once.
matches, a no-diff run reports the forge's own error, which is honest; the status gate makes a
wrong match much harder. One measurement against a live GitLab (SMOKE-TEST Mode G) settles it,
and nothing should depend on this arm until then.
-- **The M2 loop is covered in three places and joined in none.** Finding → fix run → push →
- reconciliation is what M2 exists to close. `FixRunDispatcherTest` proves the dispatch,
- `Adr040ExistingBranchTest` proves the push against a real remote with real containers, and
- `ReviewChainTest` proves review and reconciliation against a real GitLab. **Nothing proves the
- halves meet**, and it is not a matter of effort: a run unit lands on the default bridge and
- cannot resolve the e2e stack's `gitlab` service, because `RunUnitSpec` has no network and
- `DockerRunRuntime` never sets one. Rebinding GitLab off loopback would undo a deliberate
- security control in `compose.e2e.yml`, so it is not the answer.
+- **~~The M2 loop has no joined live proof~~ — CLOSED 2026-09-12.** On the live GitHub pull
+ request `artyomsv/spire-test#31`, runs `3987682681:1` and `3987682176:1` traversed finding → fix
+ run → push → reconciliation. The review threads were resolved and verdicts persisted. This
+ observation closes the live-chain claim; it does not establish an automated GitLab loop.
+- **The automated GitLab M2 loop still cannot join dispatch, push and reconciliation.**
+ `FixRunDispatcherTest`, `Adr040ExistingBranchTest` and `ReviewChainTest` cover those legs
+ separately. A run unit cannot resolve the e2e stack's `gitlab` service: `RunUnitSpec` has no
+ network field and `DockerRunRuntime` never sets one. Rebinding GitLab off loopback would undo a
+ deliberate security control in `compose.e2e.yml`, so it is not the answer.
— `techdebt/spire-runtime-docker/2-3-a-run-unit-has-no-network-so-it-is-neither-isolated-nor-reachable.md`
- **~~The publisher's trunk floor is not exercised end to end~~ — CLOSED 2026-09-11.**
It now is. This entry said the run died as `RUNTIME_UNAVAILABLE, init container failed with exit 1`
diff --git a/docs/factory/ARCHITECTURE.md b/docs/factory/ARCHITECTURE.md
index 041b77b8..4b0d2473 100644
--- a/docs/factory/ARCHITECTURE.md
+++ b/docs/factory/ARCHITECTURE.md
@@ -28,6 +28,14 @@ The planes are not services. They are ownership boundaries: the policy plane dec
sequences, the run plane executes. Only the run plane touches a sandbox, and it never decides
anything a human would call a policy.
+The work-plane cursor is `intake → spec → plan → build → verify → deliver → review → land`.
+Delivery supplies the pushed PR required by the existing reviewer (ADR-045). The
+[prepared task handoff](PREPARED-TASKS.md) stores tracker references and digests, then associates
+one durable build attempt with the existing M2 run record. Slice 8a keeps production item execution
+unavailable until slice 8b supplies trusted publication hold; the standalone run plane above retains
+automatic publication. Missing generation, verification and merge executors never become successful
+phase results.
+
## 2. Services
| Service | Today | Added |
@@ -107,28 +115,16 @@ failed runs was **dropped commit** — the agent did the work and the container
### 3.3 `WorkSource` — where work comes from
-```java
-public interface WorkSource {
- WorkSourceType type();
- WorkSourceCapabilities capabilities(); // supportsTransitions, supportsPlans, supportsLabelAudit
- List candidates(WorkQuery q);
- WorkItem fetch(WorkItemRef ref);
- void comment(WorkItemRef ref, String body);
- void transition(WorkItemRef ref, String state);
-
- /** Labels WITH the actor who applied each one. A label whose applier is unknown carries
- * {@code Actor.UNKNOWN} and selects no autonomy profile — never a silent fallback. */
- List labelEvents(WorkItemRef ref);
-}
-
-record LabelEvent(String label, Actor appliedBy, Instant at, Origin origin) {}
-enum Origin { WEBHOOK, AUDIT_TRAIL, UNATTRIBUTED }
-```
+The implemented [WorkSource port](../../spire-worksource/src/main/java/dev/codespire/worksource/WorkSource.java)
+binds one source scope and explicit account. Candidates and label events are paged. Fetch results
+distinguish found, unavailable, confirmed deletion and confirmed transfer. Comment/transition
+writes have durable effect identities and separate read-only recovery operations; uncertain absence
+does not authorize a repeat write. Jira Cloud also polls authenticated comment activities.
`labelEvents` replaced a `Set labels(ref)` after a review showed the safety rule built on it
was unimplementable. A set of strings has no author, and FR-F24 must know who applied a label. Only a
webhook names a sender; a label found by polling needs the tracker's own audit trail (GitHub's timeline
-API, Jira's changelog), which `supportsLabelAudit` declares. Where neither is available the label is
+API, Jira's changelog), which the `LABEL_AUDIT` capability declares. Where neither is available the label is
`UNATTRIBUTED` and **selects nothing** — the honest degradation, rather than quietly enforcing the rule
only for labels a webhook happened to witness.
@@ -136,29 +132,12 @@ Arms: GitHub Issues, GitLab Issues, Jira — reusing the HTTP clients the `spire
already have. Same hosts, same registry, wider rights (see [PACKAGING.md](./PACKAGING.md) §Knowledge
vs Build).
-### 3.4 `PullRequestSink` — the port that does not exist yet
-
-**There is no `Forge` type in this codebase.** An earlier draft of these documents said M2 would open
-pull requests "via the existing `Forge` seam"; that name was imported from prior art and does not
-appear in a single Java file here. The real SCM ports are `ScmIngress`, `DiffSource`, `CommentSink`,
-`ThreadSource`, `IdentitySource` and `PrUrlParser`, and **none of them can create a pull request** —
-nothing in Code Spire ever has.
-
-So M2 owns real work, not wiring:
-
-```java
-public interface PullRequestSink { // new port, three implementations
- ScmType type();
- PullRequestRef open(RepoRef repo, NewPullRequest request);
- Optional findByHead(RepoRef repo, String headBranch); // idempotency
-
- record NewPullRequest(String headBranch, String baseBranch, String title, String bodyMd) { }
- class NothingToPropose extends RuntimeException { } // the agent changed nothing
-}
-```
+### 3.4 `PullRequestSink` — observed pull-request delivery
-Built in M2 and this is the shipped shape, not a sketch. Three differences from the draft above it
-are worth naming because each was forced by a forge rather than chosen:
+M2 introduced the [PullRequestSink port](../../spire-contract/src/main/java/dev/codespire/contract/port/PullRequestSink.java)
+with GitHub, GitLab and Bitbucket adapters. M3 adds explicit requested/observed draft state and
+recovers uncertain creation by matching both head and base branches. Unsupported draft capability
+blocks delivery. Three details matter to the caller:
- **`type()`**, like every other port, so a composition root can assert it selected the adapter it
meant to.
@@ -170,8 +149,7 @@ are worth naming because each was forced by a forge rather than chosen:
and reads like a failure on all four. Naming it in the PORT is what lets a caller tell it apart
from a permission fault without knowing which forge answered. See SCM-MAPPING.md §8.
-The **pull-request half above is still true**; the credential half is not, and was overtaken by M0.
-The FACTORY-role account's single token already clones AND pushes — `RunResource` packs it,
+The FACTORY-role account's single token clones and pushes — `RunResource` packs it,
`Credentials` unpacks it into read and write slots, and `PublishRepo.push` uses it against a real
remote. The per-forge question ("does one token serve both?") was answered before M0 and is recorded
in `ROADMAP.md`'s pre-M0 table: **yes on all three**. So M2 does not decide a push credential; it
@@ -270,26 +248,29 @@ intent journalling matters more here than it does for a review: after early ack,
response cannot be recovered by redelivery, so an ambiguous outcome must fail closed into
`dispatch_uncertain`.
-## 6. Command and event vocabulary (sketch)
+## 6. Command and event vocabulary
-**The run half is DELIVERED and its names are not the ones sketched here.** The catalogue never
-landed in `../CONTRACT.md`, which still carries no factory entry — read the types, not this table.
-The work-item half is still a sketch for M3–M4.
+The implemented M3 catalogue is also recorded in [CONTRACT](../CONTRACT.md). M4 specification
+generation, planning and verification remain outside this implementation.
| Kind | Names | State |
|---|---|---|
-| Integration events (gateway) | `WorkItemLabelled`, `WorkItemCommented`, `WorkItemClosed` | sketch (M3) |
-| Commands (orchestrator → run worker) | `ExecuteRun`, `CancelRun`, `SteerRun` | **delivered** — `PrepareWorkspace` and `FinalizeRun` were never built: the unit clones itself and salvage is a runtime call, not a command (ADR-039) |
-| Results (run worker → orchestrator) | `RunStarted`, `RunFinished`, `RunFailed` | **delivered** — `RunProgressed` became the `cs.run-events` transcript (ADR-034) and `BranchPushed` became fields on `RunFinished` |
-| Domain events (aggregate) | `WorkItemAdmitted`, `SpecDrafted`, `PlanProposed`, `StepDispatched`, `StepVerified`, `GateOpened`, `GateResolved`, `PullRequestOpened`, `WorkItemCompleted`, `WorkItemRefused` | sketch (M3–M4). **`DomainEvent` carries no run member today**: `factory_run` is projected straight from `cs.run-results` and the durable record of a run is `factory_run` + `llm_charge`, not the event store. A run aggregate is M3's decision, not something M0/M1 skipped |
+| Integration events (gateway) | `WorkSourceDelivery`, `RepositoryDelivery` carrying typed `RepositoryActivity` | Implemented signed tracker/SCM ingress; rechecked against source and repository ownership |
+| Commands (orchestrator → run worker) | `ExecuteRun`, `CancelRun`, `SteerRun`, `ExecuteWorkRun`, `PublishWorkRun`, `HoldWorkRun` | Implemented standalone and held execution paths; salvage remains a runtime operation |
+| Results (run worker → orchestrator) | `RunStarted`, `RunWorkReady`, `RunFinished`, `RunFailed` | Implemented durable readiness and terminal publication; transcripts remain separate |
+| Work-item decisions | `WorkItemEvent` with milestone, policy, preparation, gate, progress and control facts; `ResolveGate` unifies answer channels | Implemented work-item aggregate. Review `DomainEvent` remains separate. `factory_run` and `llm_charge` retain run/execution truth; M3 deliberately adds no run aggregate |
-`WorkItemRefused` carries a discriminated reason, in the same vocabulary shape as ADR-025's
-`CapRefusal`: ceiling clamp, unlisted labeller, entitlement missing, credentials exhausted, budget
-exceeded, gate expired.
+Work-item state carries explicit refusal/wait reasons and never turns a missing executor into
+success. Production VERIFY and LAND remain unavailable. The [acceptance record](M3-ACCEPTANCE.md)
+separates supported local journeys from the standalone live /fix proof and remaining live gaps.
## 7. Data
-New tables, in the schema of the service that owns them (schema-per-service, ADR-011).
+Tables live in the schema of the service that owns them (schema-per-service, ADR-011).
+The current migration inventory is in [DATA-MODEL](../DATA-MODEL.md). Work-item persistence includes
+durable source pages/effects, gates and phase attempts, run dispatch/results, delivery claims and
+takeover receipts/revocation outboxes. V72 drops only the unused account workspace column;
+repository ownership and immutable legacy mapping evidence remain.
**`orchestrator` schema**
diff --git a/docs/factory/AUTONOMY.md b/docs/factory/AUTONOMY.md
index 8b24cc20..0d3fa9d2 100644
--- a/docs/factory/AUTONOMY.md
+++ b/docs/factory/AUTONOMY.md
@@ -11,7 +11,7 @@ either the typo waits for a human or the auth change does not.
## 1. The eight phases
```
-intake ─► spec ─► plan ─► build ─► verify ─► review ─► deliver ─► land
+intake ─► spec ─► plan ─► build ─► verify ─► deliver ─► review ─► land
```
| Phase | What happens | Output |
@@ -21,10 +21,16 @@ intake ─► spec ─► plan ─► build ─► verify ─► review ─► d
| **plan** | specification → ordered steps, each a vertical slice — *one model call, no sandbox* | plan, ready for a gate |
| **build** | one sandboxed run per step — *harness* | commits on a branch |
| **verify** | the repository's own back-pressure runs — *harness* | pass / fail / **unverified** (fails the step, never the item) |
-| **review** | the existing reviewer reviews the branch | findings, reconciled across rounds |
| **deliver** | push and open a pull request | pull request URL |
+| **review** | the existing reviewer reviews the delivered pull request | findings, reconciled across rounds |
| **land** | merge and close the work item | merged, or handed to a human |
+Delivery precedes review because the existing reviewer consumes an actual pushed pull request
+(ADR-045). M3's [prepared task handoff](PREPARED-TASKS.md) accepts existing specification and
+single-step plan references; generation and verification remain M4 capabilities. Slice 8a proves
+the plan/build policy boundary with an explicit test transport. Production item builds remain
+unavailable until slice 8b supplies the publication hold; missing phases never report completion.
+
### Why steps are vertical slices
Models build **horizontally** — the whole data layer, then the whole API, then the whole UI — so
@@ -67,26 +73,29 @@ factory:
ceiling: assisted # no work item in this repository may exceed this
profiles:
suggest:
+ intake: auto
spec: auto
plan: auto
build: off
deliver: off
land: off
assisted:
+ intake: auto
spec: auto
plan: approve # a human approves the plan
build: auto
verify: auto
- review: auto
deliver: draft_pr
+ review: auto
land: approve
autonomous:
+ intake: auto
spec: auto
plan: auto
build: auto
verify: auto
- review: auto
deliver: pr
+ review: auto
land: auto_if_green # top rung; unwired until explicitly enabled
labels:
"spire:suggest": suggest
diff --git a/docs/factory/M3-ACCEPTANCE.md b/docs/factory/M3-ACCEPTANCE.md
new file mode 100644
index 00000000..977457f4
--- /dev/null
+++ b/docs/factory/M3-ACCEPTANCE.md
@@ -0,0 +1,73 @@
+# M3 acceptance record
+
+All seven acceptance criteria were independently verified before the final cleanup slice.
+Slice 9 was accepted without findings in round 16. The final slice retains these decisions;
+the pull request stays draft until the operator's final review. This record distinguishes
+accepted control-plane evidence, real local execution, and live-forge evidence.
+
+## Seven verified criteria
+
+| # | Accepted outcome and discriminating proof | Proving slice / review | Evidence |
+|---|---|---|---|
+| 1 | The same prepared task has three visible journeys: suggest stops before BUILD; assisted requires a PLAN answer before one build; autonomous admits one build immediately. Tests compare phases, gates, decisions and run counts rather than profile names. Artifact history stores references, and manual acceptance cannot invent PHASE_COMPLETED. | 8a / round 12 | [Journey evidence](../../.claude/reviews/global/factory-m3-slice8a.md), [Java journey](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/work/WorkItemJourneyIT.java), [UI journey](../../spire-ui/src/components/work-items/WorkItemJourney.test.tsx) |
+| 2 | An above-ceiling label is clamped and says so. Separate mutations remove the policy bound, persisted clamp and visible explanation. | 7 / round 11 | [Policy evidence](../../.claude/reviews/global/factory-m3-slice7.md), [Java policy](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/work/WorkItemPolicyIT.java), [UI policy](../../spire-ui/src/components/work-items/WorkItemPolicy.test.tsx) |
+| 3 | An unlisted applier selects no profile. Independently, an unattributed label selects nothing even when its actor hint passes membership. The positive control admits an allowed, attributed applier. | 5 / rounds 8–9 | [Intake evidence](../../.claude/reviews/global/factory-m3-slice5.md), [Intake tests](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/work/WorkItemIntakeIT.java) |
+| 4 | Lowering the ceiling stops an in-flight item at its next phase. The fixture keeps its source enabled and actor allowed; only the ceiling prevents continuation. A separate gate-answer case detects a changed policy revision. | 7 / round 11 | [Ceiling evidence](../../.claude/reviews/global/factory-m3-slice7.md), [Policy tests](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/work/WorkItemPolicyIT.java) |
+| 5 | Measured push access allows /fix with empty overrides and outside the legacy author list. Readers and unknown permission are refused; ALLOW grants a reader and DENY stops a writer. Six independently mutated cases enter the real saga with valid finding, target and budget prerequisites. | 4 / round 7 | [Permission evidence](../../.claude/reviews/global/factory-m3-slice4.md), [Saga tests](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/pipeline/FixPermissionSagaTest.java) |
+| 6 | Handle entry resolves and stores a stable ID, then renders the handle after reload. Unresolved input writes nothing. The persistence proof uses fresh JVM readers, and UI tests retain the round trip. | 3 / round 6 | [Identity evidence](../../.claude/reviews/global/factory-m3-slice3.md), [Resource tests](../../spire-orchestrator/src/test/java/dev/codespire/orchestrator/provider/ActorResolutionResourceTest.java), [Actor picker](../../spire-ui/src/components/ActorPicker.test.tsx) |
+| 7 | Repositories own workspace, selected accounts and one webhook per event kind. Account forms have no workspace. The migration and real-row continuity proofs retain existing credentials, identities and history. | 2 / round 5 | [Cutover evidence](../../.claude/reviews/global/factory-m3-slice2.md), [Repository detail](../../spire-ui/src/components/repositories/RepositoryDetail.test.tsx), [Account form](../../spire-ui/src/components/SettingsProviders.form.test.tsx) |
+
+## Execution and takeover evidence
+
+[Slice 8b](../../.claude/reviews/global/factory-m3-slice8b.md) adds real containers, a local Git
+origin and actual killed worker JVMs. Item builds checkpoint without pushing; a current delivery
+permit resumes only the publisher. Recovery observes a prior push without rebuilding or charging
+another build. Draft delivery and REVIEW tests supply explicit TEST-only prior verification.
+
+The separate live standalone /fix proof passed on
+[TEST PR #32](https://github.com/artyomsv/spire-test/pull/32): run `4003204361:1` automatically
+pushed `264ff858b538a3c779cf94161d3bc601bcfcf69a`. The next review resolved the intended GitHub
+thread and persisted verdict; the six other prior findings remained UNCHANGED. The TEST PR was
+closed and its exact branch deleted. The review/run audit was retained.
+
+[Slice 9](../../.claude/reviews/global/factory-m3-slice9.md) independently mutates ordinary
+approving prose, stale approval head, dismissed state, recorded bot identity after rename and
+rotation, and durable publication revocation after actual JVM death without an M1 cancel claim.
+A human wearing the bot's display name still takes over. Concurrent recovery records an
+already-observed PR once and keeps the item suspended.
+
+## Limits that remain
+
+- **Production VERIFY and LAND remain unavailable.** M4 owns the verifier; M3 does not ship one.
+- **No live item-build proof.** The live standalone /fix proof on TEST PR #32 does not prove an
+ item-linked build against a real forge.
+- **The automated GitLab run-unit gap is still open.** RunUnitSpec has no network field, so a
+ run unit cannot reach the e2e stack's GitLab. A live GitHub run does not close this gap.
+- **The two factory images are still not on GHCR.**
+- Per-forge identity and permission limits remain separate entries in [UNVERIFIED](../UNVERIFIED.md).
+ Native external gate answers and operator resume have no live deployment proof. GitLab and
+ Bitbucket native PR approvals are visibly unavailable; Jira Data Center comment polling is unavailable.
+- Remote publication already in progress cannot be recalled. No atomic ordering with a human's
+ remote push is claimed. Confirmed deletion/transfer payloads still need live provider evidence.
+
+## Mutation and validation records
+
+Counts belong to their recorded slice and source revision; they are not a globally deduplicated sum.
+Every ledger names its selected failure and scratch-restored passing case. The accepted slice 7
+ledger explicitly separates its one test-wiring parity check from production mutations.
+
+| Slice | Recorded mutation evidence |
+|---|---|
+| 1–4 | Selected-case tables in [slice 1](../../.claude/reviews/global/factory-m3-slice1.md), [slice 2](../../.claude/reviews/global/factory-m3-slice2.md), [slice 3](../../.claude/reviews/global/factory-m3-slice3.md), [slice 4](../../.claude/reviews/global/factory-m3-slice4.md) |
+| 5 | [154 checks / 153 distinct production mutations](../../.claude/reviews/global/factory-m3-slice5-mutations.json) |
+| 6 | [142 checks / 140 distinct production mutations](../../.claude/reviews/global/factory-m3-slice6-mutations.json) |
+| 7 | [123 checks: 122 production checks / 120 distinct production mutations, plus one authorized test-wiring check](../../.claude/reviews/global/factory-m3-slice7-mutations.json) |
+| 8a | [92 checks / 92 distinct production mutations](../../.claude/reviews/global/factory-m3-slice8a-mutations.json) |
+| 8b | [248 checks / 245 distinct production mutations](../../.claude/reviews/global/factory-m3-slice8b-mutations.json) |
+| Presentation | [30 shared-style and interaction checks](../../.claude/reviews/global/factory-m3-round14-mutations.json) |
+| 9 | [78 checks / 78 distinct production mutations](../../.claude/reviews/global/factory-m3-slice9-mutations.json) |
+| 10 | [Final migration and read/write guard evidence](../../.claude/reviews/global/factory-m3-slice10.md) |
+
+Final measured test counts, backup chronology, dev continuity and migration results are in the
+[slice 10 record](../../.claude/reviews/global/factory-m3-slice10.md). Repeatable upgrade steps
+are in [the smoke-test runbook](../SMOKE-TEST.md#m3-final-upgrade-slice-10).
diff --git a/docs/factory/MODULES.md b/docs/factory/MODULES.md
index 42b7003c..bf813755 100644
--- a/docs/factory/MODULES.md
+++ b/docs/factory/MODULES.md
@@ -239,8 +239,23 @@ images, two credentials, no overlap.
**Purpose.** Read a tracker as a work queue and write back to it.
-**Owns.** `WorkSource`, `WorkSourceType`, `WorkSourceCapabilities`, `WorkItemRef`, `WorkItem`,
-`WorkQuery`.
+**Owns (M3 slices 5–6).** `WorkSource`, `WorkSourceType`, nested capability/fetch/write result
+records, `WorkIssueRef`, `WorkIssueLocation`, `WorkTicket`, `WorkPage`, `LabelEvent`,
+`CurrentLabel`, `LabelReconciler`, `WorkSourceIngress` and `WorkSourceSignal`. The SPI is
+JDK-only and Apache-2.0; build purity checks reject framework imports. `WorkTicket` is transient
+tracker content. Durable workflow events and the pure lifecycle/policy are in `spire-contract`.
+
+`spire-worksource-github`, `spire-worksource-gitlab` and `spire-worksource-jira` are Apache-2.0
+reference arms. Each reuses its existing context client and API connection configuration; the
+distinct `PinnedJsonWriter` shares pinned transport and authentication without granting writes
+to the context interface. `WorkEffectMarker` binds comment recovery to one opaque effect identity.
+Read-only `findComment` and `transitionApplied` inspect an uncertain outcome without resending it.
+
+The orchestrator owns durable coordinate pages and the encrypted tracker outbox. Admission and
+candidate removal commit together; scan progress survives process death inside a page. Tracker
+effects claim `uncertain` before HTTP and re-read current policy before a pending write. Recovery
+can confirm an outcome or retain uncertainty; absent evidence never authorizes another send.
+Work-source settings select accounts, repositories and confirmed people and report capabilities.
**Relationship to `spire-context-*`.** The context modules already hold credentials for Jira,
Confluence, GitHub Issues and GitLab Issues and already speak those APIs through the SSRF-guarded
@@ -248,9 +263,11 @@ Confluence, GitHub Issues and GitLab Issues and already speak those APIs through
transport**: a context provider reads an issue as context; a work source also claims, comments and
transitions it. That distinction is what makes Knowledge and Build separate product packs.
-**Capability flags matter here.** Jira has transitions and a workflow; GitHub Issues has labels and
-state; GitLab has both plus epics. The domain reads capabilities and degrades, rather than assuming
-a workflow exists.
+**Capability flags matter here.** GitHub and GitLab expose issue audit, comments and state changes.
+Jira Cloud exposes complete changelog reads and real workflow transition IDs. The Data Center arm
+exposes polling and comments while attribution and recoverable transitions remain unavailable.
+Jira uses polling; GitHub and GitLab also normalize authenticated issue webhooks. These are
+implemented operation sets, not permission grants. Per-forge live gaps are in `docs/UNVERIFIED.md`.
---
@@ -293,6 +310,11 @@ delivered there would be read only after the run it cancels had finished.
### `spire-contract` (Apache-2.0, framework-free)
+**Implemented through M3 slice 7.** `WorkPolicy` selects the displayed profile by declared
+precedence and bounds every mode and numeric cap by all eligible labels, admission and the current
+ceiling. `WorkPolicyLimits` unions protected paths. `WorkItemLifecycle` is the pure phase decider;
+`WorkItemEvent`, `WorkGate` and `WorkProgress` carry durable policy, approval and usage facts.
+
New sealed hierarchy members for the commands, results and domain events in
[ARCHITECTURE.md](./ARCHITECTURE.md) §6, plus value types shared across services: `AutonomyProfile`,
`GateMode`, `PhaseName`, `Entitlements`, `RefusalReason`, and the `ArchivedNotice`-style constant
@@ -305,6 +327,13 @@ reviewed by hand until that gate recurses.
### `spire-orchestrator` (FSL)
+**Implemented through M3 slice 7.** `WorkItemTransitions` observes tracker evidence outside database
+locks, then checks registry revisions and serializes each aggregate decision. Intake, resume,
+phase results, dashboard answers and pending tracker writes use that policy boundary. The store
+atomically appends encrypted history, query projections and Kafka outbox rows. Gate expiry also
+releases reservations and cancels pending tracker effects. `WorkPhaseCapability` refuses execution
+until a real executor is bound; slice 8 supplies the prepared artifact/build integration.
+
`WorkItemLifecycle` decider; `RunSaga` owning staleness and retry; gate open/resolve/expire; the
entitlement check placed **beside** `SpendGate` and the priceability check, so every reason a
dispatch was refused reads in one place.
diff --git a/docs/factory/PRD.md b/docs/factory/PRD.md
index 37aaa884..73a2249d 100644
--- a/docs/factory/PRD.md
+++ b/docs/factory/PRD.md
@@ -21,7 +21,7 @@ present at the moment the finding is raised. Handing that to a human to retype i
**Second, the work item is the right unit, not the diff.** Real work arrives as a ticket, not as a
pull request. A factory that starts from a tracker issue can run the phases a team already runs —
-refine, plan, build, verify, review, deliver — with an agent in each, and a human only where judgment
+refine, plan, build, verify, deliver, review — with an agent in each, and a human only where judgment
is genuinely required.
The failure mode to avoid is equally clear from the prior art: an unsupervised agent fleet that
@@ -158,7 +158,9 @@ Tags: **[M0]**–**[M6]** map to the build order in [ROADMAP.md](./ROADMAP.md).
"labels the webhook happened to witness", and every label found by polling — after downtime, on a
backfill, on the first scan of an existing backlog — bypasses the check entirely.
- **FR-F17 — Eight phases [M3/M4].** A work item moves through `intake → spec → plan → build →
- verify → review → deliver → land`. Each phase is separately gateable.
+ verify → deliver → review → land`. Each phase is separately gateable. Delivery opens the pushed
+ pull request the existing reviewer needs (ADR-045). M3 can accept a human-prepared specification
+ and single-step plan; it does not claim that M4's generation or verification executors exist.
- **FR-F18 — Specification phase [M4].** A vague ticket is refined into an outcome, context and
acceptance criteria, **written back to the tracker as a comment** — never into the repository,
which would create a second source of truth and a diff the reviewer must review before any code
diff --git a/docs/factory/PREPARED-TASKS.md b/docs/factory/PREPARED-TASKS.md
new file mode 100644
index 00000000..5b30968e
--- /dev/null
+++ b/docs/factory/PREPARED-TASKS.md
@@ -0,0 +1,49 @@
+# Prepared tracker tasks
+
+M3 accepts an existing specification and a single-step plan. It stores their tracker identities,
+content digests and build coordinates, without copying their bodies into work-item history.
+The specification and plan must be tickets in the work item's registered source. Reference lookup
+resolves a ticket number or key to its stable provider identity through the selected source account.
+
+In the work-item detail, an administrator enters the specification ticket and selects **Read
+specification version**. The response shows its current SHA-256 and a single-step JSON plan template.
+Put that plan in another tracker ticket's body, with the actual instruction. Enter that plan's key,
+the build base branch and full commit, and the configured harness and model. **Check artifact
+references** reads both current versions; **Register these versions** validates the plan and stores
+the references with the verified operator identity and expected work-item revision.
+
+The plan body is a JSON object with integer `schemaVersion: 1`, `specificationSha256` matching the
+selected specification, and exactly one `steps` entry containing a nonempty `id` and `instruction`.
+The specification body is nonempty text. Each body is bounded to 49,152 characters; the assembled
+prompt is also checked against M2's 65,536-character limit. A failed or mismatched tracker read
+grants no authority. A changed artifact requires current registration and a new plan decision.
+
+The same prepared task follows the selected effective modes:
+
+| Profile example | Plan/build result |
+|---|---|
+| suggest | Records the references and stops before build; no run. |
+| assisted | Opens a durable plan gate; its recorded human approval admits one build. |
+| autonomous | Accepts the plan without a gate and admits one build. |
+
+Names select no implementation branch. Current labels, admission bounds and the ceiling determine
+the modes. The gate binds both artifact identities/digests and the base branch, commit, harness and
+model. Replacement supersedes the old gate. Artifacts and current authority are read again before
+an unstarted build claim is released.
+
+The dispatch transaction records the phase attempt, associated `factory_run` and uncertain effect
+claim before calling the M2 launcher. A definite broker miss may retry that same association; an
+unacknowledged send waits for its result or the existing authenticated run dispatch-resolution
+action. A terminal result has an encrypted durable inbox. Completion is deduplicated by attempt,
+including a crash after the aggregate commits and before the inbox acknowledgement commits.
+M2 accounts for one agent call per run that could have spent, not the model's internal call count.
+Its existing cause-and-usage classification also identifies failures before anything could be bought;
+those consume no call and can be readmitted after repair. Unmeasured potential spend blocks further
+work; a missing price or charge after execution is never treated as a known zero.
+
+**Slice 8a execution boundary:** tests replace only the final broker emitter and explicitly declare
+the test publication capability. They exercise real artifact reads, transitions, database claims,
+M2 assembly and acknowledgement classification. Production item builds remain
+`capability_unavailable` until slice 8b supplies a trusted publication hold. Standalone runs retain
+their existing path. Production without a verifier stays waiting at verify; test-supplied prior
+phase results do not establish an M4 verifier, draft publication or a live-forge journey.
diff --git a/docs/factory/README.md b/docs/factory/README.md
index 935c117c..416e2e88 100644
--- a/docs/factory/README.md
+++ b/docs/factory/README.md
@@ -23,7 +23,7 @@ own autonomy label says they should.
tracker issue
│
▼
- intake ─► spec ─► plan ─► build ─► verify ─► review ─► deliver ─► land
+ intake ─► spec ─► plan ─► build ─► verify ─► deliver ─► review ─► land
│ │ │ │ │ │ │ │
└───────┴───────┴───────┴────────┴─────────┴──────────┴─────────┘
each phase is a gate the work item's autonomy profile
diff --git a/docs/factory/ROADMAP.md b/docs/factory/ROADMAP.md
index 9275c0b9..6598d778 100644
--- a/docs/factory/ROADMAP.md
+++ b/docs/factory/ROADMAP.md
@@ -365,6 +365,14 @@ is in the table above.
**Goal:** work starts from a ticket, and autonomy is chosen per ticket.
+**Implementation complete; final review pending (2026-09-14, PR #153).** All seven criteria are
+independently verified; [M3-ACCEPTANCE](M3-ACCEPTANCE.md) maps each to its proving slice and evidence.
+Real local-origin tests prove item execution and held-publication recovery; TEST PR #32 separately
+proves live standalone /fix. **Production VERIFY and LAND remain unavailable; M4 owns the verifier.**
+**No live item-build proof is claimed.** The automated GitLab run-unit network gap remains open
+because RunUnitSpec has no network field, and **both factory images remain absent from GHCR**.
+Per-forge identity and permission limits remain separate [UNVERIFIED](../UNVERIFIED.md) entries.
+
**Delivers.** `spire-worksource` plus GitHub Issues, GitLab Issues and Jira arms, reusing the
existing context adapters' clients. Tracker webhooks on the gateway's keyed registry edge.
`work_item` bookkeeping — **not** a mirror. Autonomy profiles, label mapping, the operator ceiling,
diff --git a/docs/superpowers/plans/2026-09-12-factory-m3-work-items.md b/docs/superpowers/plans/2026-09-12-factory-m3-work-items.md
new file mode 100644
index 00000000..b03d2817
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-12-factory-m3-work-items.md
@@ -0,0 +1,653 @@
+# Factory M3 — work items, labels and gates — implementation plan
+
+**Date:** 2026-09-12
+
+**Status (2026-09-14):** Slices 1–9 are accepted, including the split 8a/8b and
+the operator-approved presentation correction. Round 12 independently verified the last of
+all seven acceptance criteria; round 16 accepted slice 9 with no findings. Slice 10 completes
+the explicit V72 drop, fresh pre-migration backup, dev continuity and final validation.
+[The acceptance record](../../factory/M3-ACCEPTANCE.md) maps each criterion to its proving
+slice and review. [Final evidence](../../../.claude/reviews/global/factory-m3-slice10.md)
+records measured counts, four additional production mutations and backup/migration chronology.
+PR #153 remains draft for final operator review; no merge is authorized.
+
+Production VERIFY and LAND remain unavailable. M4 owns the verifier. No live item-build
+proof exists; the accepted TEST PR #32 proof is standalone /fix. The automated GitLab
+run-unit network gap remains open, both factory images remain absent from GHCR, and
+separate per-forge identity/permission UNVERIFIED entries remain unchanged.
+
+**Goal:** Start factory work from a tracker ticket with explicit, bounded autonomy; make repository
+ownership, command authority and approval state visible and durable.
+
+**Issue:** [#114](https://github.com/artyomsv/code-spire/issues/114), fetched after its
+`2026-09-12T21:47:45Z` update.
+
+**Design:** [Factory M3 design](../specs/2026-09-12-factory-m3-work-items-design.md).
+
+**Architecture:** An event-sourced work-item lifecycle owns workflow decisions. Existing run
+records and the charge ledger retain execution truth; no run aggregate or transcript replay is
+introduced. Explicit repository-role bindings replace account-workspace lookup. The gateway owns
+keyed, authenticated webhook registrations. Work-source adapters reuse context transport with a
+separate write facade. Current policy is checked at each boundary before durable effects leave
+the orchestrator. The design's §2 explains the aggregate decision and its ADR-034 amendment.
+
+**Stack:** Java 25 / Quarkus / JDBC and Flyway; PostgreSQL + Kafka; React + TypeScript, vitest and
+Testing Library; existing Gradle split test tiers. Keep the versions already pinned by the repo.
+
+**Branch:** `feat/factory-m3-work-items`, already checked out in
+`E:\Projects\Stukans\code-spire-worktrees\feat-software-factory`, based on `origin/master` at
+`27fe17b`. Do not create or switch a branch. The analyst reviews this same worktree.
+
+## Global constraints
+
+- Round 1 changes exactly the design and this plan. **Slice 0 opens the draft PR**, before any
+ production slice. Opening a draft is explicitly authorized by the brief; no extra permission
+ round is needed. The draft stays draft for analyst review.
+- Commit each independently reviewable slice with imperative first line, maximum 72 characters,
+ and a body explaining nontrivial changes. No authoring attribution, coauthor trailers, model
+ names, vendor names or generated-by notices in commit/PR text. Requested PR title takes
+ precedence over generic commit-style templates.
+- The running `spire-dev` stack is shared; do not run compose down. On 2026-09-13 the analyst
+ stopped all four competing `quarkusDev` run workers. Keep them stopped: the analyst will start
+ a worker for slice 8b's live proof. Service tests use their own Testcontainers resources. If a
+ Docker-driving test loses a container unexpectedly, report possible external deletion to the
+ analyst before investigating a production defect; the Gradle lock does not cover dev workers.
+- **Never run concurrent Gradle test invocations in this worktree.** Run `./gradlew testFast`
+ then `./gradlew testServices`, sequentially, using `--rerun-tasks` for measured evidence.
+ On PowerShell use `.\gradlew.bat`. Targeted runs use `--tests` plus `--rerun-tasks`.
+- Mutation checks use a scratch snapshot of the exact pre-mutation file, never a git restore.
+ A mutation must change a production line, compile, and kill exactly one designated test in
+ the selected run. Zero or more than one failure is not the required proof. Details below.
+- Pure domain code in `spire-contract` and `spire-diff` stays framework-free. Extend module
+ purity/architecture checks for the new SPI; provider dispatch belongs only in ADR-020
+ composition roots. No forge-specific branches in policy, saga or resource code.
+- Preserve existing line endings. Java uses four-space indentation, TS two. Split new React
+ screens/components rather than growing the already-large settings components. Match existing
+ validation, icons, auth, encryption and CSS contract conventions.
+- No plausible synthetic rows. Isolated test fixtures use `TEST-`/`CANARY-` names. A live canary
+ requires announcing its actual ids and exact cleanup `DELETE` before insertion; track remote
+ issue/branch cleanup too. This round creates no data. Do not include generic destructive SQL
+ against user-owned rows in a runbook and call it cleanup.
+- Temporary files belong in the active session's scratchpad. Do not reuse a previous session's
+ path. Persistent database backups and encrypted continuity evidence belong in the worktree's
+ git-ignored `.handoff/` directory.
+- Unknown capabilities, missing external evidence and test skips are visible outcomes. No stub
+ phase reports success in production. Proposed tests below are not evidence until executed.
+
+## Slice order and runnable exits
+
+Each slice includes its own API/read surface, tests and applicable documentation. A migration-only
+or interface-only commit can exist inside a slice, but is not its review exit.
+
+| Slice | Depends on | Runnable exit | Review unit |
+|---|---|---|---|
+| 0 — Design and plan | None | These documents render, link to the updated issue, and are in an open draft PR. | **Opens the PR. Round 1 ends here.** |
+| 1 — Repository registration and migration bridge | 0 reviewed | Register/read a repository with explicit accounts; existing reviews and runs still resolve identically during bridge. | Schema, bootstrap exchange, repository API and first detail view. |
+| 2 — Repository cutover and per-kind webhooks | 1 | Repository screen is the entry point; workspace is absent from account form and runtime lookup; existing hook keys still verify. | All active resolvers, gateway kind routing and UI together. |
+| 3 — Resolve people and edit bidirectional overrides | 2 | Enter a resolvable handle, reload its id-backed display, reject unresolved input; edit allow/deny on repository. | Directory adapters, registry/API and account/repository person controls. |
+| 4 — Authorize `/fix` by effective push permission | 3 | A real inbound command reaches dispatch on measured write access with empty overrides; all four override cases work. | Permission adapters and both saga authorization layers. |
+| 5 — First ticket, durable intake and suggest policy | 2, 3 | A signed issue label or rescan admits one durable item, visible on Work items; unknown/unlisted actors select nothing. | GitHub source, minimal profile registry, lifecycle/store/outbox and UI. |
+| 6 — GitLab/Jira sources and recovery | 5 | Each source can admit a real fetched ticket through webhook or polling with the same actor rule. | Adapter parity, safe tracker writes and restart-safe scanning. |
+| 7 — Full policy and dashboard approvals | 5 | Label changes and ceiling edits affect the next phase; a dashboard gate survives restart, resolves or expires. | Complete profile vector, transition checks, gates, Approvals and attention. |
+| 8a — Prepared task to policy-controlled build | 4, 6, 7 | Three labelled prepared tasks stop, await approval or build; missing later capabilities stay visibly waiting. | Artifact handoff and dispatch/result join. |
+| 8b — Publication hold and draft delivery | 8a | Item-linked runs await a current delivery permit; standalone `/fix` still pushes automatically, re-proved live. | Worker/publisher/watchdog hold, native draft delivery and standalone regression proof. |
+| 9 — External answers and human takeover | 8b | Tracker/PR answers resolve the same gate; human activity suspends automation and holds publication through restart. | Authenticated ingress, gate channel adapters, run control/publisher hold and resume. |
+| 10 — Integrated evidence and release documentation | 1–9 | Acceptance proofs and mutation evidence are recorded; supported journeys demonstrated and remaining limitations named. | Final integration tests, runbook and measured status updates. |
+
+The §11.1 review dependency is discharged. Slices 9 and 10 retain their numbers; 8b sits between 8a and 9.
+
+Slices are sequential review boundaries, not a request to launch parallel test runs or delegated
+work. Some dependencies are independent for scheduling, but this plan requires no additional pane.
+
+## File map and contracts
+
+Abbreviations used only in the task file lists:
+
+- `C` = `spire-contract/src/main/java/dev/codespire/contract/`.
+- `O` = `spire-orchestrator/src/main/java/dev/codespire/orchestrator/`.
+- `G` = `spire-gateway/src/main/java/dev/codespire/gateway/`.
+- `U` = `spire-ui/src/`.
+- Java tests use the corresponding `src/test/java` package. Every new test class in this plan
+ lives there unless the `spire-e2e` module is named explicitly.
+
+| Surface | Create / modify |
+|---|---|
+| Repository | Create `O/repository/RepositoryRegistry.java`, `RepositoryAccounts.java`, `RepositoryResource.java`, `RepositoryMigrationBridge.java`; migrations in orchestrator `src/main/resources/db/migration/`. Modify `O/provider/ProviderRegistry.java`, `ProviderResource.java`, `ProviderInput.java`, `ProviderView.java`, `ScmProvider.java`, `ProviderClients.java`. |
+| Active account consumers | Modify `O/provider/ReviewProviderResolver.java`; `O/pipeline/IntegrationSaga.java`, `ReviewRerunService.java`; `O/ingress/ManualRegisterResource.java`; `O/prompt/PromptSampleRenderer.java`; `O/factory/MachineAccounts.java`, `RunResource.java`, `FixRunDispatcher.java`, `FactoryPullRequests.java`, relevant credential assemblers and non-secret serving views. Confirm exact call sites by search before edits. |
+| Gateway | Modify `G/RegistryWebhookEdge.java`, `WebhookProviders.java`, `WebhookCommands.java`, `registry/WebhookRepo*.java`; create registry snapshot outbox/publisher and work integration publisher; gateway migrations/configuration; all three SCM resource routes. |
+| Identity/permission | Create `C/port/ActorDirectory.java`, `RepositoryPermissionSource.java`; `C/scm/ResolvedActor.java`, `RepositoryPermission.java`; `O/provider/ActorResolutionResource.java`; `O/factory/FixAuthorization.java`, `FixPermissionService.java`; implementations in existing `spire-scm-{github,gitlab,bitbucket}` packages. |
+| Work-source SPI | New `spire-worksource/src/main/java/dev/codespire/worksource/WorkSource.java`, `WorkIssueRef.java`, `WorkIssueLocation.java`, `LabelEvent.java`, capabilities/paging/actor records. Three `spire-worksource-{github,gitlab,jira}` adapter modules; reuse/refactor existing issue clients and `spire-http/PinnedJsonClient` transport. |
+| Work-item lifecycle | Create `C/lifecycle/WorkItemLifecycle.java`, work-item state/command/value types; extend `C/event/DomainEvent.java`; create work integration/command wire hierarchies and `WorkItemIds`. Update envelope decoding, `O/pipeline/DomainEventSink.java`, `O/eventstore/JdbcEventStore.java`. |
+| Orchestration | Slice 5 uses `O/work/WorkItemStore.java` (including projection), `WorkItemIntake.java`, `WorkItemResource.java`, `WorkItemOutbox.java`, `WorkSourceRegistry.java`, `WorkSourceScanner.java` and administration/resources. Pure `WorkItemLifecycle`/`WorkPolicy` live in `spire-contract`; adapter composition stays in `ProviderClients`. Dispatch and phase completion follow in later slices. |
+| Policy/approval | Create `O/autonomy/AutonomyRegistry.java`, `AutonomyResource.java`; pure policy values/resolver in contract lifecycle package; `O/workitem/GateExpiry.java`, `GateResource.java`, `GateAnswerRouter.java`, `HumanTakeover.java`; expand attention queries. |
+| Run bridge | Modify `O/factory/RunResultSaga.java`, `FactoryRunProjection.java`, `FactoryPullRequests.java`, run dispatch assembly; contract run records/control; `spire-run-worker/.../RunControlListener.java`, `RunLauncher.java`, `OrphanWatchdog.java`; publication-hold handling in runtime/publisher. |
+| UI | Create focused `U/components/repositories/`, `workItems/`, `approvals/`, `autonomy/` components and API modules. Modify `App.tsx`, `api.ts`, `ProviderFormModal.tsx`, `AccountCredentialFields.tsx`, `ReviewerFieldsSection.tsx`, `AccountsCells.tsx`, `SettingsWebhookRepos.tsx`, serving hooks and `AttentionBell.tsx`. |
+| Build/docs | `settings.gradle.kts`, module builds, root test tiers, `spire-arch` tests, contract snapshots, Kafka provisioning, service `application.yml`, both packaged Compose variants and Helm/kustomize resources where channels/config require them; `docs/{DECISIONS,CONTRACT,DATA-MODEL,SCM-MAPPING,SECURITY,SMOKE-TEST,HISTORY,UNVERIFIED}.md`, factory docs and `CLAUDE.md`. |
+
+New filenames and method names are proposed contracts. Check repository state at the start of each
+slice; do not copy obsolete line numbers from historical plans. If a composition root moves, update
+the plan and its architecture allowlist together.
+
+## Slice 0 — publish this planning round
+
+**Files:** only this document and its linked design.
+
+- [x] Confirm branch and clean initial worktree, read the complete brief, re-fetch issue #114 and
+ inspect ADR-041, factory requirements and the actual run/account/webhook implementation.
+- [x] Write the aggregate decision and why; repository migration; policy/identity/gate rules;
+ ordered runnable slices; exact proof and mutation obligations for criteria 1–7.
+- [x] Record under-specification in design §11 rather than choosing silent fallback behavior.
+- [x] Check Markdown links, `git diff --check`, exact two-file scope and absence of secrets/data.
+ No Gradle/UI suites are warranted for this documentation-only change.
+- [x] Commit `Plan Factory M3 work items, labels and gates`, with a body explaining the new
+ registry and workflow decisions and the acceptance-proof plan. Push with
+ `git push -u origin feat/factory-m3-work-items`.
+- [x] Write the PR body as a real UTF-8 scratchpad file and use `gh pr create --draft --base master
+ --head feat/factory-m3-work-items --title "Factory M3 — work items, labels and gates"
+ --body-file `. Link issue #114 without claiming to close implementation work.
+- [x] Verify the remote head equals the commit, PR is draft against master, and only these two
+ documents are in its diff. Report the PR number and the design questions. **Stop Round 1.**
+
+## Slice 1 — register a repository while preserving existing resolution
+
+**Files:** repository registry/bridge/resource and migration files in the map; gateway snapshot
+outbox; first repository UI; ADR-042 draft in `docs/DECISIONS.md`.
+
+**Produces:** `RepositoryAccounts.resolve(repositoryId, role)` and a non-secret serving view;
+`POST/GET /api/repositories`; versioned metadata-only registration snapshots.
+
+- [x] **Before any new migration reaches dev:** take a full `pg_dump`, validate the archive and
+ record its path/hash. Use the exact PowerShell commands below; the binary dump never passes
+ through PowerShell text redirection. The running stack holds real accounts, encrypted source
+ credentials and review/run history. Preserve its existing matching keyset securely outside git.
+
+The existing `.handoff/spire-dev-pre-m3-2026-09-13.dump` satisfies this prerequisite (182 objects,
+492,480 bytes, verified 2026-09-13). **Do not take another dump for slice 1.** For a future upgrade,
+run this repeatable command from the worktree root; `.handoff/` is git-ignored and survives sessions.
+
+```powershell
+$m3Handoff = Join-Path (Get-Location).Path '.handoff'
+[void](New-Item -ItemType Directory -Force -Path $m3Handoff)
+$m3Dump = Join-Path $m3Handoff ('m3-before-slice1-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.dump')
+$m3Process = [Diagnostics.Process]::new()
+$m3Process.StartInfo = [Diagnostics.ProcessStartInfo]::new('docker')
+$m3Process.StartInfo.UseShellExecute = $false
+$m3Process.StartInfo.RedirectStandardOutput = $true
+@('exec','spire-postgres','sh','-c','exec pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc') | ForEach-Object { $m3Process.StartInfo.ArgumentList.Add($_) }
+$m3File = [IO.File]::Create($m3Dump)
+try { [void]$m3Process.Start(); $m3Process.StandardOutput.BaseStream.CopyTo($m3File); $m3Process.WaitForExit(); if ($m3Process.ExitCode -ne 0) { throw 'pg_dump failed' } } finally { $m3File.Dispose(); $m3Process.Dispose() }
+Get-FileHash -LiteralPath $m3Dump -Algorithm SHA256
+```
+
+Validate archive listing with `pg_restore --list` using a one-shot container with only this
+handoff directory mounted read-only, without changing the running stack:
+
+```powershell
+docker run --rm --mount "type=bind,source=$m3Handoff,target=/backup,readonly" postgres:18.4-alpine pg_restore --list "/backup/$([IO.Path]::GetFileName($m3Dump))"
+if ($LASTEXITCODE -ne 0) { throw 'Backup archive validation failed' }
+```
+
+- [x] Add `scripts/verify-dev-credential-continuity.ps1` as the read-only local operational probe.
+ It uses the actual dev keyset and `EncryptionService`, with `provider:` for account secrets
+ and `context-provider:` for remaining legacy context secrets. Capture encrypted baseline
+ evidence (including source→account references) into .handoff/; Compare re-decrypts actual rows
+ and checks equality in memory. Never write/log plaintext, keysets or unkeyed secret hashes.
+ Execute Capture before migration; slice 2 must execute Compare on the real dev rows:
+
+```powershell
+$m3Handoff = Join-Path (Get-Location).Path '.handoff'
+.\scripts\verify-dev-credential-continuity.ps1 -Mode Capture -Snapshot (Join-Path $m3Handoff 'm3-real-credentials.bin')
+.\scripts\verify-dev-credential-continuity.ps1 -Mode Compare -Snapshot (Join-Path $m3Handoff 'm3-real-credentials.bin')
+```
+
+Capture has already established the encrypted baseline for 9 real credential/reference entries.
+Reuse it for Compare; Capture deliberately refuses to overwrite existing evidence. The measured
+comparison in slice 1 is pre-upgrade only; slice 2's comparison after cutover remains required.
+
+- [x] Reconcile `CLAUDE.md` and `docs/UNVERIFIED.md` **in this slice**: the live M2 chain on
+ `artyomsv/spire-test#31`, runs `3987682681:1` and `3987682176:1`, resolved threads and persisted
+ verdicts was measured on 2026-09-12. Keep the automated GitLab gap as its own open entry:
+ `RunUnitSpec` has no network field, so run units cannot reach that test stack's GitLab.
+
+- [x] Apply the accepted bridge-only org enrollment decision. Write failing migration/service tests:
+ `RepositorySchemaMigrationTest.preservesAccountIdsCredentialsAndContextReferences`,
+ `RepositoryMigrationBridgeTest.replaysGatewaySnapshotWithoutDuplicateBindings`,
+ `RepositoryMigrationBridgeTest.leavesConflictingOriginsPending`, and
+ `RepositoryResourceTest.registersARepositoryWithExplicitRoleBindings`.
+- [x] Add repository and binding tables, revision checks, referenced-delete protection and
+ migration snapshot storage. Preserve UUID/AAD and V59 source recovery. Do not relax the old
+ account key before the bridge can preserve assignments.
+- [x] Publish/consume real gateway metadata with stable snapshot revision and outbox retries.
+ Provision `cs.registry-integration`, keyed by registration id. Do not read gateway SQL from the
+ orchestrator or send its webhook secret across this channel.
+- [x] Add repository registration/detail UI backed by the API, including empty/disabled/pending
+ roles. Show workspace on the repository. During this slice the old account field is explicitly
+ labelled legacy; it is removed at slice 2's cutover.
+- [x] Prove old review/run resolution equals new bindings for migrated fixtures, across restart,
+ multiple roles, hosts and nested namespaces. Duplicate source snapshots must not recreate
+ an account an operator has already rebound.
+ Migration requires origin evidence from explicit registration metadata or persisted PR URLs;
+ a workspace match alone cannot choose credentials. Unknown or conflicting origins remain
+ pending for explicit repair, including factory-only history without PR origin evidence.
+- [x] Mutation: omit factory-role filtering in the binding resolver; run only
+ `RepositoryAccountsTest.reviewerNeverReceivesTheFactoryCredential`. Expect one assertion failure
+ with distinct `TEST-` credentials, restore snapshot, rerun green. Also kill the origin-match
+ guard with `rejectsAnAccountFromAnotherOrigin` and migration AAD/id preservation with the
+ migration test above, each as a separate mutation.
+- [x] Run relevant service/UI tests sequentially, demonstrate registration through the real API
+ in the isolated test stack, update ADR-042/upgrade notes, commit the slice for review.
+
+Measured 2026-09-13: forced `testFast` then `testServices`, 3005 Java tests / 348 suites with zero
+failures and one existing Windows symlink skip; 620 UI tests and the UI build passed. Forty
+distinct production mutations failed exactly one targeted test and passed after restoration.
+Details: `.claude/reviews/global/factory-m3-slice1.md`. The dev stack was not rebuilt; slice 2's
+post-cutover comparison against the real encrypted baseline remains required.
+
+The blank-origin review correction adds four discriminating mutations (44 total): strict record
+validation, legacy consumer normalization and both database CHECK constraints. The real-consumer
+mutation fails the durable pending-mapping assertion after offset commit, so an escaped exception
+or a DLQ record alone cannot satisfy the proof.
+Final correction verification: 3009 Java tests across 348 suites, zero failures and the same one
+Windows symlink skip. Forced fast and service tiers ran sequentially; the final service invocation
+used `--no-parallel --max-workers=2` after Dev Services startup timeouts. The unchanged UI retains
+its slice 1 proof (620 tests and successful build).
+
+## Slice 2 — cut over to repository ownership and per-kind hooks
+
+The reviewed V60/V3 bridge mapped all 37 reviews and 14 runs to six repositories and eight
+bindings. Slice 2's V61/V4 cutover is now live, rebuilt gateway → orchestrator → UI. The required
+real-row encrypted comparison passed all 9 account/context and 12 webhook entries. Counts
+remain 6/37/85/14/3; populated workspaces and rejection metadata are unchanged. Three origin-unknown
+registrations remain explicit repair states because their actual origins have not been confirmed.
+
+3048 Java tests across 357 suites, zero failures and 1 existing Windows symlink privilege skip; forced fast/services tiers and packaging passed. UI passed 630 tests/77 files and
+build. Sixty-two isolated mutations are recorded in .claude/reviews/global/factory-m3-slice2.md,
+including the fresh-schema production-constraint mutation. No live run worker was started.
+
+**Files:** all active account consumers, gateway registry/edge/resources, account DTOs/forms,
+repository UI and migrations; ADR-042 final decision text.
+
+**Produces:** runtime resolution solely by explicit repository binding; no account workspace in
+new API/form; gateway key plus scope plus event-kind validation.
+
+- [x] Write failing `RepositoryResolverCutoverTest.allDispatchPathsUseTheSelectedRepository`,
+ `RepositoryResolverCutoverTest.unmappedLegacyReviewCannotDispatch`,
+ `RepositoryWebhookKindsTest.preservesLegacyKeyAndRejectsWrongKind`,
+ `RepositoryWebhookKindsTest.refusesASecondWebhookForTheSameKind`, and criterion 7 tests below.
+- [x] Change every resolver caller; search `resolveByWorkspace`, `registration`,
+ `providers.resolve`, `MachineAccounts.resolve` and serving API usages. Include manual/rerun,
+ prompt, fix and result-time PR proposal paths, not only the HTTP run endpoint.
+- [x] Drop the old UNIQUE and workspace-by-role CHECK; finish evidenced history mappings and
+ retain explicit unknown-origin repairs. Remove active account workspace reads. Keep the populated
+ column as rollback evidence until slice 10. Retain scalar
+ role checks. Refuse origin/type edits on referenced accounts. Do not add global role uniqueness.
+- [x] Upgrade all three keyed SCM edges to product event-kind filtering. Preserve keys, secrets,
+ scope and rejection history during migration. Wire FACTORY activity separately from REVIEWER
+ commands; reserve ISSUE scope for source registration. Unknown kinds fail closed. End org
+ auto-enrollment. Add `UnregisteredRepositoryAttentionTest.namesRepositoryOriginAndRegistration`:
+ a verified event for an unregistered repo raises attention and a Register action pre-filled
+ with repo, origin and incoming registration. Mutate its attention write, isolate the test,
+ expect exactly one failure, restore; a silent drop does not satisfy cutover.
+- [x] Replace the webhook-row screen with repository detail and one hook control per kind. Support
+ registration without hooks, retries after partial save and legacy org/deep-link navigation.
+ Remove workspace from `ProviderInput`/`View` and `AccountCredentialFields`, not merely hide CSS.
+- [x] Mutation checks for criterion 7 are specified in the matrix. Additionally kill the scope
+ comparison with `RepositoryWebhookKindsTest.validSignatureCannotCrossRepositoryScope`; use
+ the same provider and a valid signature so a different guard cannot mask the mutation.
+- [x] Run migration, gateway, orchestrator and UI verification in sequence. Search for remaining
+ active workspace-only lookups; historical docs/bridge mappings are the only permitted matches.
+ Add `spire-arch/AccountWorkspaceIsUnusedTest.noProductionCodeReadsLegacyAccountWorkspace`:
+ inspect production source/SQL including SELECT-star mappings so retained workspace cannot silently
+ re-enter resolution. Mutation: restore a workspace read in the repository resolver; exactly that
+ targeted test fails. Restore from scratch. Confirm real dev-row credential continuity with the
+ Compare command below after cutover; compare all baseline account ids and context references,
+ report added/removed rows separately, and never accept fixture-only evidence.
+ Update serving API/upgrade contracts and commit the runnable cutover.
+
+## Slice 3 — resolve a person with the selected account
+
+**Files:** directory SPI/adapters, actor-resolution resource, account policy storage, repository
+override registry, person controls; ADR-044 identity half.
+
+**Produces:** exact identity resolution with typed errors; stable-id persisted allowlists and
+`ALLOW|DENY` repository fix overrides with readable display metadata.
+
+- [x] Apply accepted capability errors and disambiguated selection for Bitbucket/Jira person lookup.
+ Write criterion 6 tests and `ActorResolutionResourceTest.usesOnlyTheSelectedAccountsCredential`,
+ `ActorResolutionResourceTest.refusesAnAmbiguousMatch`,
+ `ActorResolutionResourceTest.rechecksSubmittedIdentityOnSave`,
+ `ActorDisplayTest.renamedHandleKeepsTheStoredId`.
+- [x] Implement directory adapters via configured account origin/auth. Exact match and stable id
+ are required; no username-to-id string coercion and no first search result. Implement by-id
+ refresh with stale display metadata on failure; never send a secret in a response.
+- [x] Add the repository override table and UI controls. One actor has one override; a contradictory
+ edit is a version conflict, not “last array entry wins.” Migrate verified legacy stable-id grants
+ to their real repository mappings. Flag unresolved legacy handles without granting authority.
+- [x] Add account person pickers and the reusable source control with unresolved/error states. Work-source registration does not exist until slice 5: wire its person picker there using the source account, without inheriting account policy. Show handle plus policy
+ effect; a count may accompany people but cannot replace their identities.
+- [x] Kill criterion 6 mutations, then separately kill account credential selection and returned-id
+ verification with the corresponding targeted tests. Restore and rerun each case green.
+- [x] Run adapter unit tests, resource persistence test and UI form round-trip; update SCM-MAPPING
+ identity capability notes and per-forge UNVERIFIED entries (what was measured, which forge, and remaining proof), then commit. At this exit overrides are editable; slice 4 activates
+ their new permission fallback without changing unrelated review policy.
+
+## Slice 4 — measure repository push access for `/fix`
+
+**Files:** permission SPI/adapters, `FixAuthorization`, `FixPermissionService`, both guards in
+`IntegrationSaga`; authorization tests and ADR-044.
+
+**Produces:** explicit deny → grant → fresh effective push permission, still subject to existing
+target, identity, observe-mode, spending and fix-chain guards.
+
+- [x] Write the six distinct criterion 5 cases, plus inherited-rights/unknown-response adapter
+ tests. Read the official endpoint contracts linked by design §4.3 before writing fixtures.
+ Test Bitbucket's effective endpoint and pagination, not its explicit-grant endpoint.
+- [x] Implement adapters returning `CAN_PUSH|CANNOT_PUSH|UNKNOWN`, binding repository and stable
+ user. Enforce timeout/rate limits and origin-pinned requests; do not fall back to another token
+ when the assigned reviewer cannot inspect permission. No stale positive permission cache.
+- [x] Route `/fix` around the old common author-list guard into `FixAuthorization`. Keep self-loop,
+ observe-only and registration/target checks. Do not change `/review` or `/finding` semantics.
+- [x] Test through `IntegrationSaga.on` using the actual normalized command so a private
+ `FixAuthorization` unit test cannot conceal the outer guard. Assert dispatch count and the
+ refusal reason, with legitimate thread/finding/account prerequisites in every permission case.
+- [x] Prove override grant still cannot push a fork/trunk or exceed either FR-F32 cap. Retain
+ corresponding M2 regression suites; live-author permissions never substitute for push target
+ validation. Known stale PR-state/shared-branch debt stays documented unless explicitly fixed.
+- [x] Kill each criterion 5 mutation in its isolated method, then run the class green. Exercise all
+ three adapter contracts and inherited-access cases; record live-token limitations without
+ changing the operator's account privileges. Add each per-forge permission behavior as its own UNVERIFIED entry with measurement and forge. Commit the authorization slice.
+
+## Slice 5 — admit the first ticket and display durable bookkeeping
+
+**Files:** `spire-worksource` and GitHub arm, source registry/scanner, minimum profile registry,
+work-item lifecycle/store/saga/outbox/resource, wire/config changes, Work items screen; ADR-043.
+
+**Produces:** authenticated label intake and explicit rescan; attributable label reconciliation;
+one durable suggest item without a run or mirrored issue content.
+
+- [x] Write `WorkItemIntakeIT.signedLabelCreatesOneVisibleItemAcrossRedelivery`,
+ `WorkItemStoreTest.restartRehydratesOnlyWorkflowMilestones`,
+ `WorkItemStoreTest.rollbackLeavesNoGateEventOrOutboxEffect`,
+ `WorkItemProjectionTest.containsNoTrackerContentColumns`, and criterion 3 tests.
+- [x] Add the SPI/module dependencies and build purity/licensing checks. Reuse the issue client's
+ HTTP/auth implementation through a read facade and a separate work writer; no write methods on
+ a context-provider interface. GitHub candidates/fetch/label audit use bounded pagination.
+- [x] Add source registration with explicit account and repository, typed actor allowlist and
+ health/cursor state. Add minimum versioned profile/mapping/ceiling registry necessary for a
+ real suggest admission; do not embed profile behavior in a forge adapter.
+- [x] Implement work-item ids/generations, lifecycle decide/fold, typed event decoding and dedicated
+ work topics. Make JDBC event append, projection, dedupe and outbox one real transaction.
+ Route work events away from review history; test `WorkItemEventRoutingTest.neverWritesAReviewRow`.
+- [x] Extend the keyed gateway edge for a bound issue scope; signed delivery and scanner events
+ enter the same reconciliation path. Store control facts only. Start polling with conservative
+ unknown attribution when full history cannot be proven.
+- [x] Render a paginated Work items screen/detail from persisted workflow fields, ignored-label
+ reasons and a live tracker link; show tracker fetch errors separately from workflow state.
+- [x] Kill criterion 3 mutations, event-route isolation and rollback guards individually. For
+ rollback mutate the shared-transaction use and inject a failure after event append but before
+ projection/outbox completion; a compile failure is not a valid transaction test.
+- [x] Run SPI/adapter tests and service intake/restart/UI proofs; commit the first ticket slice.
+
+## Slice 6 — source parity, safe writes and downtime recovery
+
+**Files:** GitLab/Jira arms, shared provider transport, source clients/scanner, tracker ingress,
+source settings UI, source capability docs.
+
+**Produces:** same source contract for three trackers, resumable polling and idempotent comment/
+transition effects, with unsupported audit/approval channels visible.
+
+- [x] Write `GitLabWorkSourceTest.reconstructsCurrentLabelApplierAcrossPages`,
+ `JiraWorkSourceTest.attributesOnlyTheActualAddedLabel`,
+ `WorkSourceRecoveryIT.backfillWithoutAuditRemainsUnattributed`,
+ `WorkSourceRecoveryIT.removeThenReaddCannotReuseAnOldAllowedActor`,
+ `WorkSourceProcessRecoveryIT.restartResumesAfterCommittedCursor` and
+ `WorkSourceProcessRecoveryIT.restartResumesInsideAStagedPageWithoutRefetchingIt`.
+- [x] Implement candidate/read/comment/transition/label-event operations and supported capability
+ reports for each adapter. Validate real Jira transition ids rather than treating arbitrary
+ status names as commands. Respect origin/auth compatibility and source account disable/rotation.
+- [x] Add authenticated tracker webhook normalization with source-bound project checks. If the
+ deployed Jira hook cannot be authenticated using a supported mechanism, support polling and
+ report the webhook limitation. Never accept an unverified hook just because its URL has a key.
+- [x] Reconcile current label set with additions/removals and stable event ordering; exhaust required
+ history pages or return unknown. No actor fallback to issue reporter/editor. Commit scan cursors
+ with reconciliation and cap each sweep; retries cannot duplicate items or lose pages.
+- [x] Implement source comments/transitions through outbox effects with deterministic markers and
+ uncertain-write handling. `WorkSourceEffectsTest.retryFindsThePreviouslyWrittenComment` must
+ observe a successful remote write followed by a client timeout before retry.
+- [x] Kill audit-completeness, remove/re-add and source-scope guards in targeted tests. Test credential
+ errors as health failures, not issue deletions. Show the supported operations on source settings.
+- [x] Run all three arm suites plus service recovery tests sequentially; record which token families/
+ webhook variants have only documentation/WireMock evidence, and commit the parity slice.
+
+## Slice 7 — re-resolve policy and persist dashboard approvals
+
+**Files:** full policy resolver/registry/UI, transitions, gate storage/resource/expiry, attention,
+Approvals screen; ADR-045 policy decision.
+
+**Produces:** checked profile vectors, visible clamps, current-policy phase decisions, durable gates,
+expiry and operator answers.
+
+- [x] Apply the accepted profile precedence and meet rule. Write criterion 2 and 4 tests plus
+ `AutonomyProfileTest.requiresDistinctProfilePrecedence`,
+ `AutonomyProfileTest.incomparableVectorsMeetWithoutWideningEither`,
+ `AutonomyProfileTest.omittedPhaseIsOff`,
+ `WorkItemPolicyIT.lowestEligibleLabelWins`,
+ `WorkItemPolicyIT.profileEditCannotWidenAnAdmittedVersion`,
+ `WorkItemPolicyIT.removedLabelStopsTheNextTransition`.
+- [x] Implement versioned vector/precedence validation, current allowed label selection, pinned
+ admission version and component-wise restriction across EVERY eligible label, not only the lowest-precedence label. Record selection/clamp/reason with policy revision.
+ Include source disabled/allowlist removed, stricter caps and protected-path floor cases.
+- [x] Call the transition service from every entry point named in design §6.2. Re-read evidence
+ outside the transaction and compare registry revision inside it; stale external data must not
+ become authority after a newer local edit. Include outbox retries and operator resume.
+- [x] Implement gate open/resolve/expiry atomically with event/outbox and reservations. Write
+ `GateResourceTest.concurrentAnswersProduceOneResolution`,
+ `GateExpiryTest.exactDeadlineRefusesALateApproval`,
+ `GateProcessRecoveryIT.restartExpiresOpenGateAndReleasesReservation`,
+ `GateResourceTest.viewerCannotResolveAGate` and `GateResourceTest.replayedAnswerIsIdempotent`.
+- [x] Render Approvals and integrate attention using current OPEN/expired/clamped conditions. A
+ resolved gate disappears from open views. UI submits expected version and displays 409/503
+ honestly; status union, renderer and filters change together.
+- [x] Kill criterion 2/4 mutations and each concurrency/expiry/auth guard with isolated tests; use
+ an injected clock and real PostgreSQL interleavings, not sleeps against the live scheduler.
+- [x] Run Java/Gradle test invocations sequentially and verify the UI; demonstrate restart and ceiling downgrade
+ through real APIs in the test stack; update ADR-045 and commit.
+
+## Slice 8a — prepared task to policy-controlled build
+
+**Files:** artifact reference handoff, dispatcher, run-result bridge, item/run FK metadata,
+`FactoryPullRequests`, all three work-source arms, work-item UI. Sink draft support belongs to 8b.
+
+**Produces:** criterion 1's accepted M3 journeys; actual one-task build and policy-aware delivery
+boundaries. No production verifier is invented to reach the delivery test cases.
+
+- [x] Apply the accepted manual-artifact/plan-build proof boundary. Missing capabilities remain
+ waiting. Correct the eight-phase diagram in `docs/factory/AUTONOMY.md` and affected PRD/
+ architecture diagrams in this slice: `intake → spec → plan → build → verify → deliver → review
+ → land`. Record the order in ADR-045; its acceptance dependency is discharged.
+- [x] Write `WorkItemJourneyIT.threeProfilesProduceDifferentVisibleJourneys` and UI journey test
+ from the acceptance matrix. Use real persisted policy/source/item data; scripted execution is
+ permitted only in tests and identified as such. Also write
+ `WorkItemRunBridgeTest.itemRunCannotUseStandaloneAutomaticProposal`,
+ `WorkItemRunBridgeTest.duplicateResultAdvancesTheItemOnlyOnce`,
+ `WorkItemRunBridgeTest.ceilingChangesBeforeDeliveryPreventTheProposal`.
+- [x] Fetch human-supplied tracker artifact references/digests and bind gates to them. Missing or
+ changed artifacts require input/new approval. Dispatch through the existing run assembly/caps
+ using the repository's selected FACTORY identity and stable item/attempt linkage.
+- [x] Persist an effect claim before dispatch, recheck current policy before publishing and make
+ run/result association recoverable after crash. Do not reset attempts on re-admission or charge
+ the same run result twice. Existing standalone runs remain outside work-item gates.
+- [x] Commit the artifact handoff and dispatch/result join for review. Before 8b provides a
+ trustworthy publication hold, item-linked real execution stays capability-unavailable; the
+ runnable 8a policy proof uses the explicit test execution boundary, never an auto-pushing M2
+ run advertised as held. Slice 8b closes the real-container execution proof.
+
+## Slice 8b — publication hold and draft delivery
+
+**Depends on:** 8a. Slices 9 and 10 keep their numbers.
+
+**Two-part exit:** item-linked runs hold publication until a current delivery permit, through
+restart and orphan recovery; **standalone `/fix` still pushes automatically**, re-proved live on
+`artyomsv/spire-test`. Unit/fixture tests do not discharge the second half.
+
+**Files:** work-ready/control/results, worker durable state, publisher/runtime finalization,
+orphan salvage, item delivery orchestration, sink draft support and UI states.
+
+- [x] Implement the accepted work-ready/delivery-permit handshake from design §6.3. An item-linked
+ run starts with publication held, checkpoints without pushing, persists awaiting-delivery and
+ releases active compute. Resume only the trusted publisher on a current delivery permit; keep
+ workspace and hold through restart. Add contract/result/control fields, runtime lifecycle and
+ UI states together; never send a permit through a repository-writable file. Deduplicate charge
+ reporting across work-ready and terminal results. Add
+ `WorkItemDeliveryIT.deliverOffNeverPushesTheBuiltBranch`,
+ `WorkItemDeliveryIT.deliveryPermitPublishesWithoutRebuilding`, and
+ `WorkItemDeliveryIT.workReadyAndFinishedDoNotDoubleCharge` in the worker service tier.
+- [x] Prevent item-linked BUILD results from falling through `FactoryPullRequests.propose` before
+ their deliver transition. Implement policy-controlled PR opening, observed reviewer result and
+ land readiness according to the accepted order. Never mark missing review/verify as passing.
+- [x] Extend the sink with explicit draft capability/request semantics and update constructors,
+ withers, snapshots and each adapter. Unsupported `draft_pr` visibly blocks delivery. Do not
+ send a regular PR and label it a draft. Preserve find-by-head idempotency and existing FIX
+ source-branch semantics.
+- [x] Prove actual run execution separately in `WorkItemRunJourneyIT.preparedItemBuildsAndWaitsForVerification`
+ (`spire-run-worker` service tier): real containers and local test origin plus provider fixture,
+ distinct from a live-forge proof. This suite must share the existing Docker serialization lock.
+ Delivery tests supply valid prior phase results through an explicitly test-only phase driver;
+ the production handler for an unavailable verify capability continues to block honestly.
+- [x] Kill criterion 1 mutations and the standalone-proposal bypass separately. Run relevant run,
+ orchestrator, sink adapter and UI suites sequentially. Also remove the initial publication hold
+ and isolate `WorkItemDeliveryIT.deliverOffNeverPushesTheBuiltBranch`: exactly one test must fail
+ on the real remote's changed head. Restore and rerun green.
+- [x] Re-prove a standalone `/fix` live on `artyomsv/spire-test`, using an actual open finding and
+ the same command → worker → publisher → next review → resolved thread/persisted verdict chain
+ proved by runs `3987682681:1` and `3987682176:1`. Record actual new run/PR ids, source head before/
+ after and verdict observations. A synthetic fixture or unit test is not this proof. Announce
+ any TEST-/CANARY-prefixed setup and its exact cleanup first. Do not close 8b without this result.
+- [x] Commit 8b independently after both exit obligations pass.
+
+## Slice 9 — answer outside the dashboard and take over safely
+
+**Accepted without findings in round 16.**
+[Slice 9 evidence](../../../.claude/reviews/global/factory-m3-slice9.md) and its
+[78-mutation ledger](../../../.claude/reviews/global/factory-m3-slice9-mutations.json)
+record the actual test selectors, replacing the provisional names in this plan.
+
+- [x] One ResolveGate boundary for dashboard, explicitly bound tracker commands and supported
+ native PR review answers. Ordinary approving prose cannot approve; a current approval on
+ an old head and a dismissed named review independently refuse.
+- [x] Stable recorded machine identities survive rename/rotation. A human wearing the bot's
+ display name still takes over. Unknown origin suspends; unrelated activity does not target an item.
+- [x] Takeover supersedes gates and invalidates unstarted effects transactionally. Authenticated
+ operator resume requires revision, note and fresh evidence; retired items cannot resume.
+- [x] Durable publication revocation survives real JVM death without an M1 cancellation claim.
+ A fresh permit and watchdog cannot publish. In-flight PR recovery records its observed outcome
+ once and keeps the item suspended; remote publication already in progress cannot be recalled.
+- [x] Independent production mutations, full sequential Java/service/package tiers, UI and
+ scanner evidence are recorded. Native provider capability and live-proof limits remain named.
+
+## Acceptance proof matrix
+
+All seven criteria are independently verified, with the slice 2–8a mutation ledgers. Round 8 accepted the distinct membership and attribution proofs; round 11 verified both visible clamping and current-ceiling continuation. Round 12 verified criterion 1's five-axis journey proof and its artifact-reference/manual-acceptance honesty assertions. `O-test`
+means `spire-orchestrator/src/test/java/dev/codespire/orchestrator/`. Tests exercise the public
+resource/consumer path plus persisted outcomes; helpers may stub external HTTP at adapter edges.
+Every integration proof has a visible UI assertion or a matching component test where required.
+
+| # | Ticket exit criterion and exact proof | Production mutation and isolated expected failure |
+|---|---|---|
+| 1 | **Three visibly different journeys.** `O-test/workitem/WorkItemJourneyIT.java#threeProfilesProduceDifferentVisibleJourneys`: label three otherwise identical `TEST-` prepared tasks under suggest/assisted/autonomous with ceiling autonomous, inspect persisted timeline/API: suggest stops before build; assisted waits at plan approval with zero runs; autonomous starts one build without approval. Then approve assisted and assert its recorded human decision and single dispatch. Assert exact phase/gate/effect fields; missing verify remains waiting. `U/components/workItems/WorkItemJourney.test.tsx` → `renders distinct suggest assisted and autonomous journeys` proves visible differences. Draft/regular PRs are separate delivery tests with a test-only prior-phase driver. Scope accepted in Round 2; runtime publication proof belongs to 8b. | First mutant: in the real transition policy branch change `approve` to proceed without opening its gate; run only the Java method, expect exactly one failed test. Restore. Second mutant: render all journey status labels as the same label; run only the named vitest case, expect one failure. Restore. A test that only compares three profile names is insufficient. |
+| 2 | **Above-ceiling label clamped and says so.** `O-test/workitem/WorkItemPolicyIT.java#aboveCeilingLabelRecordsAndDisplaysClamp`: request autonomous at assisted ceiling; assert effective vector, durable clamp event after reload, attention API row and detail reason. `U/components/workItems/WorkItemPolicy.test.tsx` → `shows requested and effective profiles with the clamp reason`. | Mutate the effective-profile meet to retain requested authority; isolate Java method, one failure. Restore. Separately omit the clamp event/attention projection write; same isolated method must fail once. Restore. Mutate the UI clamp message to empty and run the named UI case for one failure. Each proves a different half of “and says so.” |
+| 3 | **Unlisted and unattributable appliers ignored.** `O-test/workitem/WorkItemIntakeIT.java#unlistedLabellerSelectsNoProfile` and `#unattributedCurrentLabelSelectsNoProfile`, each using a mapped label that would otherwise dispatch, valid source/account/ceiling and assertions of ignored reason plus no run effect. Add `#allowedAttributedLabellerCanSelect` as positive control. | Delete the actor-membership check; run only `unlistedLabellerSelectsNoProfile`, one failure. Restore. Delete the attribution check; run only `unattributedCurrentLabelSelectsNoProfile`, one failure. Fixture for the latter has an actor hint that would pass membership but origin UNATTRIBUTED, so the origin guard alone distinguishes it. Use additional no-id test for real missing actor. Never combine both negative cases into one count. |
+| 4 | **Lowering ceiling stops an in-flight item at next phase.** `O-test/workitem/WorkItemPolicyIT.java#loweredCeilingStopsAnInFlightItemAtTheNextPhase`: admit and start under higher policy, commit a lower ceiling with next phase off, then send the prior phase's result. Assert no next effect/PR and persisted stop/reason on detail. `#ceilingChangeBeforeGateAnswerRequiresANewDecision` covers waiting items. | Replace the transition's current ceiling lookup with admission-time ceiling, preserving all other checks; run only the first method, one failure. Restore. Separately bypass policy recheck in gate resolution and isolate the second method, one failure. The mutation must not be masked by also removing a label or disabling an account in the fixture. |
+| 5 | **Push access without a list, refusal without access, overrides both ways.** `O-test/pipeline/FixPermissionSagaTest.java` methods `writerWithEmptyOverridesDispatches`, `writerOutsideTheLegacyAuthorListDispatches`, `readerWithoutOverrideIsRefused`, `explicitGrantLetsAReaderDispatch`, `explicitDenyStopsAWriter`, `unknownPermissionCannotDispatch`. Invoke the real saga entry; assert one/zero dispatch and exact decision reason. Each forge also adds `*RepositoryPermissionSourceTest#inheritedWriteAccessIsRecognized` and `#unknownResponseCannotGrant`. | Six separate compiling mutants: default unlisted to deny; restore the legacy outer author check for `/fix`; grant the reader result; skip explicit ALLOW; skip explicit DENY; map UNKNOWN to allow. Select the corresponding single method for each mutant; exactly one failure each, snapshot restoration and baseline pass between them. Test fixture is same-repository/open valid finding with available caps, so unrelated guards do not kill the proof. |
+| 6 | **Type handle, store id, render handle; unresolved refused.** `O-test/provider/ActorResolutionResourceTest.java#handleEntryStoresStableIdAndReturnsHandle` sends a `TEST-` handle via resolve/save and asserts actual DB id (not its textual handle), then reads after restart. `#unresolvedHandleIsRejectedWithoutWriting` asserts 422 and unchanged rows. `U/components/accounts/ActorPicker.test.tsx` → `saves a resolved handle and renders it after reload`, plus `refuses unresolved input`. Resolved provider fixture data is explicitly synthetic and never inserted into the dev stack. | Replace the repository/account actor-id binding with input handle and isolate the first Java method, one failure. Restore. Remove refusal on not-found and attempt a raw text write; isolate the second, one failure. Restore. Render stored id/count in place of handle and isolate the named successful UI round-trip, one failure. Refuse a mutant that only changes the mocked response instead of production code. |
+| 7 | **Repository shows workspace/accounts/one hook per kind; accounts have no workspace.** `O-test/repository/RepositoryResourceTest.java#repositoryOwnsWorkspaceAndRoleBindings`; gateway `dev.codespire.gateway.registry.RepositoryWebhookKindsTest#refusesASecondWebhookForTheSameKind`; `U/components/repositories/RepositoryDetail.test.tsx` → `shows workspace selected accounts and one webhook per event kind`; `U/components/SettingsProviders.form.test.tsx` → `does not offer workspace on an account`. UI uses distinct configured reviewer/factory fixtures, missing/disabled states and all enabled hook kinds. | Drop repository-account binding filter and isolate the Java test, one failure. Restore. Drop only the per-kind uniqueness constraint in an isolated migrated PostgreSQL test schema; allow duplicate insertion through the real registry API, isolate gateway test, one failure. Restore migration snapshot and recreate isolated schema. Hide the workspace/account/hook section, each as a separate UI mutant of the same named case, one failure each. Reintroduce workspace input on account form; run the named account-form case, one failure. |
+
+Criterion 7's database mutant must be a change to production migration/constraint code applied to a
+fresh test schema, not a manual ALTER of the shared dev database. If an application guard masks the
+constraint mutation, target the repository method directly within the same test while keeping
+valid input; prove exactly the constraint being claimed. Record that selection explicitly.
+
+## Mutation protocol and additional guard obligations
+
+Each slice is responsible for every guard it adds, not only the seven headline criteria. Maintain
+an evidence table with production path/line, snapshot hash, changed line, exact test selector,
+baseline result, mutated failure name/count, restored hash and restored pass. The evidence belongs
+in the slice's review notes, with a concise PR checklist pointer; no invented pass counts.
+
+1. Ensure no other Gradle test invocation is running. Copy the current source/migration to a unique
+ file under the session scratchpad and record its hash. The snapshot includes uncommitted work.
+2. Run the selected test green with tasks forced. For example:
+ `.\gradlew.bat :spire-orchestrator:test --tests
+ 'dev.codespire.orchestrator.pipeline.FixPermissionSagaTest.writerWithEmptyOverridesDispatches'
+ --rerun-tasks`. Pass the actual argument as one shell token; line wrapping here is prose.
+3. Edit exactly one production guard. Verify the intended text changed (match `\r?\n` if needed).
+ Re-run the exact method. Inspect JUnit XML for exactly one failure, the designated assertion,
+ no compilation errors, no setup/container failure, and no skipped target. Targeting one method
+ is intentional; this does not claim the full suite contains only one affected assertion.
+4. Restore by copying the scratch snapshot back. Check its hash and run the same method green.
+ Use a `finally` restoration in scripted checks. Never use `git checkout`, `git restore`, reset
+ or a cached Gradle result as restoration/evidence.
+5. For UI mutations use `npx vitest run -t ''` from `spire-ui`; inspect the
+ executed case and failure count. Restored case must pass. Run the whole affected class/file
+ after its individual mutations to catch fixture leakage.
+6. If a second guard masks the target, improve the discriminating fixture; do not delete several
+ guards together. A surviving/non-compiling mutant is not “killed.” Record failures honestly.
+
+Additional required isolated guards and their exact proposed witnesses:
+
+| Guard | Witness | Mutation |
+|---|---|---|
+| Same-origin account binding | `RepositoryAccountsTest.rejectsAnAccountFromAnotherOrigin` | Remove origin equality, retain matching kind/role. |
+| Credential rotation/reference safety | `RepositoryAccountsTest.disabledAccountCannotServeAnExistingBinding` | Remove enabled filter after a valid binding was created. |
+| Host-qualified item identity | `WorkItemIdsTest.samePathsOnDifferentOriginsHaveDifferentIds` | Omit origin from derived identity. |
+| Per-source actor namespace | `WorkItemPolicyIT.sameIdOnAnotherSourceDoesNotAuthorize` | Resolve actor membership from the other source. |
+| Lowest eligible label | `WorkItemPolicyIT.lowestEligibleLabelWins` | Select highest eligible label instead. |
+| Pinned immutable profile | `WorkItemPolicyIT.profileEditCannotWidenAnAdmittedVersion` | Use latest version rather than pinned at transition. |
+| Current label removal | `WorkItemPolicyIT.removedLabelStopsTheNextTransition` | Reuse intake labels for continuation. |
+| Omitted phase refusal | `AutonomyProfileTest.omittedPhaseIsOff` | Default an absent phase to auto. |
+| Gate deadline | `GateExpiryTest.exactDeadlineRefusesALateApproval` | Change `now >= expiresAt` to `now > expiresAt`. |
+| Concurrent answer | `GateResourceTest.concurrentAnswersProduceOneResolution` | Remove expected-version/OPEN compare at the transaction write. |
+| Dashboard authority | `GateResourceTest.viewerCannotResolveAGate` | Permit viewer on mutation resource. |
+| Gate PR scope | `GateChannelsIT.prReviewCannotApproveAPlanGate` | Ignore required land phase when translating approval. |
+| Artifact/head freshness | `GateChannelsIT.staleHeadAndDismissedReviewCannotApprove` | Accept approval without current-head verification; test stale-head branch alone. Use a second isolated method for dismissal mutation. |
+| Retirement | `WorkItemRetirementIT.transferRetiresOldIdentityAndInvalidatesOpenGate` | Continue old item after confirmed identity transfer. |
+| Machine identity in takeover | `HumanTakeoverIT.knownMachinePushDoesNotTakeOver` | Treat matching recorded factory actor as human. |
+| Publication hold after restart | `PublicationHoldIT.orphanRecoveryKeepsTheDurablePublicationHold` | Omit durable hold read before orphan finalization. |
+| Event/projection/outbox atomicity | `WorkItemStoreTest.rollbackLeavesNoGateEventOrOutboxEffect` | Append on a separate autocommit connection before the forced transaction failure. |
+| Idempotent result continuation | `WorkItemRunBridgeTest.duplicateResultAdvancesTheItemOnlyOnce` | Remove consumed-result dedupe; preserve valid active state for both deliveries. |
+| No early item PR | `WorkItemRunBridgeTest.itemRunCannotUseStandaloneAutomaticProposal` | Remove the item-association exclusion in automatic M2 proposal. |
+
+Tests with multiple scenarios should be split into individually selectable methods before their
+mutations if one scenario would mask another. Every guard discovered during review is added to
+this table with its discriminating witness before that slice is called complete.
+
+## Slice 10 — integrated proof and handoff
+
+- [x] Run AccountWorkspaceIsUnusedTest before adding the explicit V72 migration; independently
+ kill its read, INSERT and UPDATE arms. Kill the real DROP in a populated private V71 schema.
+- [x] Take and validate a fresh .handoff pg_dump before migration reaches dev. Preserve the
+ original encrypted baselines. Apply V71/V72 and verify the column is absent, all five full row
+ counts unchanged and both credential/webhook comparisons passing. Reconcile the retained
+ PR #32 audit with the original baseline explicitly; delete no accepted proof history.
+- [x] Force testFast, then testServices, then packaging with no second Gradle test invocation.
+ Derive and inspect XML totals, including the whole criterion classes and actual JVM recovery.
+- [x] Run the full UI suite, TypeScript and production build, retaining route/style/interaction guards.
+- [x] Assess the optional warm GitLab e2e tier: no warm stack was available. No WorkItemGateJourneyTest
+ or new live item-build result is claimed. RunUnitSpec still lacks a network field, so automated
+ GitLab run-unit connectivity remains open. Accepted local-origin execution and the standalone
+ TEST PR #32 proof remain separate evidence; no new live canary was needed.
+- [x] Consolidate all seven independently accepted API/UI criteria in one acceptance record,
+ with proving slices, exact witnesses, mutation ledgers and unchanged proof boundaries.
+- [x] Reconcile accepted ADRs, architecture, topics, data, SCM/security and upgrade runbook.
+ Rewrite CLAUDE.md Status with actual measured results and append the M3 HISTORY entry.
+- [x] Record remaining capabilities and separate per-forge UNVERIFIED entries without softening them.
+- [ ] Final operator review of slice 10. PR remains draft until that review authorizes readiness;
+ merging remains the operator's decision. The slice 10 brief explicitly authorized this dev upgrade.
+
+## Round 1 report content
+
+Report the draft PR number/link, the two document paths, the commit and push verification, and
+that this round ran documentation checks only. Explicitly call out the analyst decisions in design
+§11: M3/M4 journey and phase-order boundary, ordering incomparable profiles, permission/handle
+portability, legacy org/source cardinality, native drafts and takeover precedence. Those are
+review inputs, not reasons to withhold the requested draft PR.
diff --git a/docs/superpowers/specs/2026-09-12-factory-m3-work-items-design.md b/docs/superpowers/specs/2026-09-12-factory-m3-work-items-design.md
new file mode 100644
index 00000000..52b42305
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-12-factory-m3-work-items-design.md
@@ -0,0 +1,623 @@
+# Factory M3 — work items, labels and gates — design
+
+**Date:** 2026-09-12
+
+**Status:** Accepted with amendments in [Round 2 review](https://github.com/artyomsv/code-spire/pull/153#pullrequestreview-5188278775). Implementation and test evidence remain per-slice obligations.
+
+**Issue:** [#114](https://github.com/artyomsv/code-spire/issues/114), re-read from GitHub,
+updated `2026-09-12T21:47:45Z`.
+
+**Plan:** [Ordered slices and proof obligations](../plans/2026-09-12-factory-m3-work-items.md).
+
+## 1. Scope and evidence
+
+M3 makes a tracker ticket the entry point to the factory, with operator-owned policy, attributable
+labels, durable approvals and human takeover. It also moves workspace ownership to repositories,
+replaces `/fix`'s list-only authorization with repository push permission plus explicit overrides,
+and lets people enter handles while authorization continues to use stable provider ids.
+
+The ticket records the live M2 loop on `artyomsv/spire-test#31`, runs `3987682681:1` and
+`3987682176:1`: command, dispatch, push to the existing source branch, another review, resolved
+thread and persisted verdict. That is the issue's reported evidence, not a measurement made in
+this planning round. M3 is unblocked. The older contrary statements in `CLAUDE.md` and
+`docs/UNVERIFIED.md` are reconciled in slice 1; the automated GitLab run-unit
+network gap remains a separate claim and must not be deleted on the strength of a live GitHub run.
+
+Read alongside [AUTONOMY](../../factory/AUTONOMY.md), [factory PRD](../../factory/PRD.md),
+[factory architecture](../../factory/ARCHITECTURE.md), [decisions](../../DECISIONS.md),
+[unverified claims](../../UNVERIFIED.md), and the
+[Accounts design](2026-09-07-accounts-and-roles-design.md) and
+[Accounts plan](../plans/2026-09-07-accounts-and-roles.md). The September 7 documents establish
+format and review depth; ADR-041 and the updated issue supersede their deferred account design.
+
+### What exists at branch base `27fe17b`
+
+| Observation | Implementation evidence | Consequence |
+|---|---|---|
+| Only review events implement `DomainEvent`. | `spire-contract/.../event/DomainEvent.java` | There is no run or work-item aggregate to extend by assumption. |
+| Run results update the run projection, charges, credential feedback and PR proposal directly. | `spire-orchestrator/.../factory/RunResultSaga.java` | Keep delivered run durability; introduce workflow ownership deliberately. |
+| PR proposal is now called after a finished run. | `factory/FactoryPullRequests.java` | Item delivery must intercept this path or a gated item will open a PR early. |
+| Account lookup is by type, workspace and scalar role; a workspace-only fallback still exists. | `provider/ProviderRegistry.java`, `factory/MachineAccounts.java`, migration V44 | Changing only the form or UNIQUE key cannot move workspace ownership. Every resolver needs repository coordinates. |
+| CONTEXT rows have null workspace; REVIEWER/FACTORY rows must have one. | Orchestrator V59, `scm_provider_workspace_by_role` | Both this CHECK and the old UNIQUE constraint need migration. Account ids and encrypted credentials can stay intact. |
+| Gateway owns `webhook_repo`, including secrets and org/repo scope. | Gateway V1; `registry/WebhookRepoRegistry.java` | No cross-schema FK, SQL join, or orchestrator credential lookup at webhook ingress. |
+| The keyed edge verifies signature, then every event's scope, then publishes. | Gateway `RegistryWebhookEdge.java` | Add tracker scope and event-kind validation here; Jira cannot be forced into `RepoRef` parsing. |
+| `/fix` has two list barriers. | `IntegrationSaga.onManualCommand` and `requestFix` | Replacing only `allowedById` still denies a push-authorized person before the switch. |
+| Context clients expose only `getJson`, sharing `PinnedJsonClient`. | `GitHubIssueClient`, `GitLabIssueClient`, `JiraClient` | Reuse transport and auth configuration, but keep writes off the context-reader API. |
+| The existing sink API has no draft flag. | `PullRequestSink.NewPullRequest` | `draft_pr` is actual adapter work, not a label to paint on a regular PR. |
+
+Paths abbreviated with `...` above are under `src/main/java/dev/codespire//`.
+The implementation plan names exact source roots for new files.
+
+## 2. Decisions and ADRs
+
+These decisions are accepted by the review; ADR records land with the implementing slices.
+Reserve the next available numbers at
+implementation time; `042`–`045` are the expected sequence after ADR-041.
+
+| ADR | Decision to record | Existing decisions affected |
+|---|---|---|
+| ADR-042 — Repositories own workspace and account bindings | Account identity is its UUID; repository identity includes forge origin. Remove UNIQUE `(type, workspace, role)` and the workspace-by-role CHECK. Explicit repository-role bindings replace workspace resolution. Gateway owns webhook registrations independently. | Completes ADR-041 deferrals; preserves scalar roles and separate identities under ADR-038. |
+| ADR-043 — A work item owns workflow milestones; a run remains a durable execution record | Add an event-sourced `WorkItemLifecycle`, transactional milestone/gate/outbox persistence, and separate work-item keys. Do not invent or backfill a run aggregate. | Clarifies ADR-034's aspirational milestone catalogue; generalizes ADR-010's single-writer rule to one writer per aggregate stream. |
+| ADR-044 — Command authority uses stable identities and effective repository permission | `/fix`: explicit deny, explicit grant, then measured push permission. Handle resolution uses the selected account's credential; display names grant nothing. | Replaces the temporary M2 list-only guard; does not alter reviewer eligibility or tracker actor authority. |
+| ADR-045 — Profiles have explicit precedence; authority is bounded per dimension | Versioned immutable profiles, operator-declared precedence, component-wise restriction, current labels/allowlist/ceiling at every boundary, version-bound gates and takeover precedence. | Makes ADR-033's “lowest” and “ceiling” implementable for vectors; records the M3/M4 boundary after analyst resolution. |
+
+### 2.1 The aggregate decision
+
+**A work-item aggregate will exist. A separate run aggregate will not.** A run remains the delivered
+`factory_run` record plus `llm_charge`; `run_event` remains a bounded, encrypted transcript, never
+state. Do not create synthetic historical `RunStarted` domain events or replay transcripts to
+reconstruct runs. Amend ADR-034 to distinguish its intended milestones from the shipped run tier.
+
+The work item owns admission, the pinned policy version, phase cursor, gates, run associations,
+retirement, takeover and completion. These decisions must survive restart, concurrent approvals,
+duplicate deliveries and policy edits. A pure `WorkItemLifecycle.decide(state, command)` is their
+single domain-event writer. Rehydration folds only recorded milestones and never calls a tracker,
+checks today's policy, spends money or emits commands. A new decision receives current authorized
+facts separately. Workers and webhooks still emit integration events; the saga converts them into
+aggregate commands.
+
+Add work-item milestone records to `DomainEvent` and implement the lifecycle under
+`spire-contract/.../lifecycle/`. `EventEnvelope` already has a generic stream id and payload.
+Add typed work-item payload decoding and routing: today's `DomainEventSink` writes every envelope
+to review history before its switch, so its default branch is not sufficient isolation.
+`ReviewLifecycle` must reject work-item payloads and vice versa. Pure modules gain no framework
+imports; extend the contract snapshots to the new nested wire types explicitly.
+
+**Why not just mutate `work_item` and emit an audit row?** That makes gate answers and phase changes
+two sources of truth unless every writer implements the same concurrency discipline. A small pure
+aggregate provides one decision table and replayable authority changes. Conversely, introducing a
+second aggregate for every run would duplicate the delivered run lifecycle without solving a new
+M3 invariant. Work-item events refer to run outcomes by id and result identity; the run record stays
+authoritative for execution details and charges.
+
+### 2.2 Transactions and transport
+
+Use `event_log` for work-item streams, with a `work-item::` discriminator in the derived stream id.
+Use separate `cs.work-integration` and `cs.work-commands` topics keyed by workItemId; `cs.events`
+retains envelopes keyed by their own stream id. Run commands/control/results remain keyed by runId.
+Update topic provisioning, serializers, retention and `spire-arch` checks together. Never put a
+work-item command on the review worker's `ActionCommand` consumption path.
+
+One orchestrator transaction locks the item/version, checks the policy revision used to decide,
+appends the expected event sequence, updates `work_item` and `work_item_gate`, records the input
+deduplication key, and writes outgoing effects to a work-item outbox. A conflict reloads and
+re-decides; it does not reuse a stale decision. The transaction must share one JDBC connection:
+calling today's independently connected `JdbcEventStore.append` beside another repository write
+does not make them atomic. Introduce a connection-scoped append implementation and retain the
+existing `EventStore` adapter for review callers. Test actual rollback with PostgreSQL.
+
+Outbox delivery is at least once with stable effect ids. Mark sent after broker acknowledgement.
+Consumers deduplicate by effect/delivery id, not receipt timestamp. A committed gate survives a
+crash before publishing; a redelivery cannot open a second gate or dispatch a second run. Current
+item and policy are checked again before releasing an unstarted effect. Disabled sources, unknown
+policy, stale observations and transferred issues cannot authorize new effects.
+
+Repository registration snapshots need their own registry channel, keyed by gateway registration
+id rather than workItemId. Add `cs.registry-integration` and its durable gateway outbox in slice 1;
+do not force configuration snapshots through either a review or a work-item aggregate stream.
+Gateway webhook success still waits for broker acknowledgement, as `IntegrationPublisher` does
+today. A failed publish returns a retryable failure, never an accepted-but-lost delivery.
+
+Tracker comments and PR creation also need recoverable side-effect records. Use deterministic
+comment markers and read-before-retry, with bounded retries and an explicit uncertain state when
+the tracker cannot establish whether a timed-out write succeeded. Do not claim remote exactly-once
+behavior. Existing M2 standalone run proposal behavior remains; item-linked runs are proposed only
+by the item's delivery effect, not `FactoryPullRequests.propose`'s unconditional BUILD path.
+
+## 3. Repository and account ownership
+
+### 3.1 Registry model
+
+In the orchestrator schema introduce:
+
+| Table | Essential fields and constraints |
+|---|---|
+| `repository` | `id UUID`, `scm_type`, canonical `forge_origin`, `workspace`, `slug`, provider repository id when known, enabled, revision, timestamps. UNIQUE `(scm_type, forge_origin, workspace, slug)`. One workspace per repository now. |
+| `repository_account` | repository id, account id, role; UNIQUE `(repository_id, role)` for REVIEWER and FACTORY. An account can serve many repositories. References block account deletion. |
+| `repository_fix_actor` | repository id, stable actor id, effect `ALLOW` or `DENY`, observed handle and resolution time; one effect per actor. |
+
+`scm_provider.id` stays the credential identity and Tink AAD stays `provider:`. Do not copy or
+re-encrypt account secrets for this key change. Do not replace the old uniqueness with
+`UNIQUE(type, role)` or with a uniqueness on handle, bot id or token: multiple credentials at the
+same host and role are legitimate. Keep immutable scalar roles. Account kind/origin changes with
+references are refused pending reassignment; token rotation updates all consumers as in ADR-041.
+
+Binding checks enforce matching forge kind and normalized API origin, the selected account's role,
+enabled state at use, and distinct resolved reviewer/factory identities when both are known.
+No resolver silently selects the first account with working credentials. Repository views name
+the configured account even if disabled or unreachable and distinguish selection from measured
+reachability/permission. CONTEXT sources continue to use their explicit account references.
+
+Repository lookup must carry host as well as type and path; a self-managed forge and its cloud
+counterpart may contain the same namespace. Existing ambiguous legacy review coordinates are
+shown as needing repository assignment and cannot dispatch a factory run. Do not broaden this
+milestone into rewriting every historical review id or charge reference.
+
+Replace `resolve(type, workspace, role)`, `registration(...)` and `resolveByWorkspace` on active
+paths with `RepositoryAccounts.resolve(repositoryId, role)`. `ReviewProviderResolver`, manual
+registration/rerun, prompts, `IntegrationSaga`, `MachineAccounts`, run resources, fix dispatch and
+PR proposal must all use it. Serving chips call the same resolver's non-secret view.
+
+### 3.2 Migration and rollout
+
+Use an expand/bridge/contract migration, with independently numbered orchestrator and gateway
+Flyway files (next orchestrator number is expected to be V60; verify before allocating).
+
+1. Create repository/binding tables while old workspace resolution still serves existing traffic.
+ Snapshot old `(account id, type, origin, workspace, role)` assignments into a migration-only
+ mapping table. Keep credentials and ids unchanged. Add nullable repository references to
+ existing review/run records; do not invent a host when historic records cannot establish it.
+2. Bootstrap repositories from real known review/run coordinates and gateway registrations.
+ Gateway publishes a versioned, non-secret registration snapshot through a durable outbox;
+ orchestrator consumes it idempotently. No cross-schema SQL access. Bind a role only when the
+ old assignment and host identify exactly one account. Ambiguous rows remain pending with an
+ attention entry; count and report them. No placeholders that look like actual repositories.
+3. Preserve org webhook coverage during the bridge. A verified event for a previously unseen
+ repository may materialize a real repository using the snapshotted legacy assignment, with
+ origin supplied by its registration. It must not inherit a different host or newer account by
+ workspace alone. Org auto-enrollment ends at cutover. Afterwards a verified event naming an
+ unregistered repository raises an attention row naming repository, forge origin and incoming
+ registration id, with a Register action pre-filled from those three. No silent drop and no
+ automatic inheritance of workspace accounts. One source/ISSUE hook per repository and the
+ REVIEWER/FACTORY/ISSUE kinds are confirmed.
+4. Cut over every runtime resolver and the repository screen together after mapping checks pass.
+ Unresolved repositories fail closed for action with a named repair path. Remove account
+ workspace input and validation, drop the old UNIQUE and workspace-by-role CHECK, but keep
+ `scm_provider.workspace` populated with its existing values until slice 10 as rollback evidence.
+ No production code may read it after slice 2; a build guard enforces that rule. Slice 10 owns
+ the explicit column-drop migration. The migration-only snapshot is
+ explicitly excluded from account selection after cutover. Preserve source credential recovery
+ columns from V59; their removal is unrelated to this migration.
+5. Update both packaged compositions, local dev configuration and upgrade instructions for the
+ new wire fields. Gateway/orchestrator upgrades need a compatible overlap. A new consumer can
+ read legacy registrations during the bridge; after cutover an old payload with ambiguous
+ repository identity is refused. No claim of binary downgrade after contracting columns.
+
+Test migration from V59 with real PostgreSQL/Flyway, distinct hosts, nested GitLab namespaces,
+multiple roles, disabled accounts, context references, an org registration and an interrupted
+snapshot exchange. Prove identical credential decryption before and after, exact row mappings,
+idempotent restart and refusal of ambiguous mappings. Do not exercise this on the running dev DB
+in a test. The bridge must preserve its existing webhook keys and encrypted secrets.
+
+Before any new migration can reach the real dev stack, slice 1 takes and validates a full
+`pg_dump` into the worktree's git-ignored `.handoff/` directory. The plan includes the exact binary-safe command. Preserve
+the matching existing keyset outside git and capture a credential-continuity proof using real
+rows and their actual Tink AADs. Slice 2 compares decrypted credentials against that baseline on
+the real dev rows after cutover; matching fixture data or matching ciphertext alone is insufficient.
+Only counts/ids and comparison outcomes are reported, never plaintext credentials or keysets.
+
+### 3.3 Repository screen and webhook model
+
+`#/settings/repositories` becomes a repository list and detail, not a list of webhook rows. Register
+a repository by selecting forge/host, entering workspace and slug, and selecting accounts for the
+roles that may act. A repository may exist with no webhook or no factory account; its state says so.
+Its detail shows **Workspace**, **Accounts**, **Webhooks**, **Work sources** and **Autonomy**.
+
+Gateway retains webhook ownership and its own admin API. Extend registrations with a logical
+repository id (no cross-service FK), source id where applicable, canonical origin, revision and
+event kind. UNIQUE `(repository_id, event_kind)` means one active registration for each product
+kind, not one hook per low-level forge action. Proposed kinds:
+
+| Kind | Accepted events and effect |
+|---|---|
+| `REVIEWER` | Existing review PR lifecycle and discussion commands, including `/fix`. |
+| `FACTORY` | Branch pushes, PR human activity, PR approvals and delivery/merge observations for linked work items. |
+| `ISSUE` | Tracker issue/label/comment/transfer events for the repository's work source; the future product-owner role is not introduced here. |
+
+Separate normalized event types prevent a duplicated PR comment delivered to both hooks from
+dispatching twice: the reviewer route parses commands; the factory route observes activity. Keep
+source delivery identity across both routes and deduplicate logical effects. A command or gate
+answer recognized on one channel must not later be interpreted as unrelated takeover (§8).
+
+The gateway rejects a validly signed payload for the wrong repository, tracker project or event
+kind before publishing anything. A Jira project is validated against its work-source scope and
+mapping, not compared with an SCM workspace. Webhook keys are routing identifiers, never a
+substitute for provider-supported signature/token verification. A Jira installation that cannot
+authenticate deliveries safely requires polling; it does not get a secret-in-URL bypass.
+
+The UI composes the two authenticated APIs; no browser or orchestrator receives a webhook secret
+on read. Creation shows its secret once. Because registration spans two services, save the repository
+first, then create each hook idempotently. Partial failure leaves an honest “webhook setup pending”
+row with Retry, rather than rolling back a repository that may already be referenced. Legacy routes
+and `?edit=` continue to open the matching repository or a clearly labelled legacy org registration.
+
+## 4. People, handles and `/fix`
+
+### 4.1 A stable id with a readable label
+
+Add an account-scoped identity directory port alongside `IdentitySource`: exact handle lookup
+and stable-id lookup return `{providerUserId, handle, displayName}` with typed not-found,
+ambiguous, unavailable and unsupported outcomes. Composition belongs in `ProviderClients`; adapter
+URLs, escaping and response parsing stay in the three SCM modules. Work-source identities use
+their own adapter and source account. No email is persisted or logged.
+
+Existing account policy entries are edited through an authenticated admin endpoint such as
+`POST /api/providers/{id}/actors/resolve {handle}`. New accounts can first be saved as credentials
+and then have policy entries added. Repository fix overrides resolve using its selected reviewer's
+account. The server performs the resolution again on save, or verifies an account/revision-bound
+resolution token; it does not trust a submitted id/handle pair from the browser. Failed or ambiguous
+lookup returns 422 and writes nothing; upstream unavailability is a retryable 503, never a text entry.
+
+Authorization stores and compares the stable id in the provider's actual namespace. An observed
+handle and timestamp are display metadata, refreshed by id; a rename updates the display, a
+reassigned handle never changes the stored id. If a refresh fails, show the last known handle as
+stale or a labelled unresolved id, never `1` as a substitute for identity. Legacy raw numeric ids
+remain valid ids; legacy raw handles are flagged for explicit re-resolution and do not acquire
+`/fix` authority merely because the string now resolves to someone.
+
+For GitHub and GitLab, exact `@handle` input is meaningful. Bitbucket privacy-era nicknames and
+Jira display names need not be unique, resolvable handles. Do not implement a first-search-hit
+fallback. Those forms need a credential-backed, disambiguated person selection when exact
+resolution is unavailable. This is an explicit portability qualification for criterion 6 (§11).
+
+### 4.2 Authorization decision
+
+Use a pure `FixAuthorization` decision over actor identity, explicit repository override and a
+measured `RepositoryPermission` result. Decision order:
+
+1. Reject unknown actor, unresolved repository/account, self-command and ordinary existing
+ observe/archive/target precondition failures. No author-equals-PR-author shortcut.
+2. Explicit `DENY` refuses even a repository owner. Explicit `ALLOW` authorizes even a reader.
+ Overrides grant the command only, never permission to bypass forks, trunk protection, caps,
+ observe-only mode, entitlement or invalid findings.
+3. Without an override, authorize only `CAN_PUSH`; `CANNOT_PUSH` refuses and `UNKNOWN` refuses
+ with a permission-unavailable explanation. Use the repository's assigned reviewer credential,
+ without falling back to a stronger factory token or another host's token.
+
+`/fix` must leave the common legacy author-list path in `onManualCommand` and go through this
+decision instead. The common self-loop and observe checks still apply. `/review`, `/finding`,
+review eligibility and conversation policy retain their current behavior. Keep their old account
+list separate from the new bidirectional fix overrides. Migrate verified stable-id entries as
+explicit grants to repositories previously served by that reviewer; an empty list produces no
+overrides, so actual push permission decides. Present those migrated grants in the repository UI.
+
+Check permission at dispatch time, not merely during account Check. Cache display observations
+only; a prior success during an outage is not new authority. Bind lookup to repository origin,
+account id/revision and actor stable id. If the provider accepts a handle in its permission URL,
+verify that the returned user is the same stable actor before accepting its permission.
+
+### 4.3 Provider endpoints and limits
+
+Checked against official API documentation on 2026-09-12; contract tests and live probes are still
+required. Repository push permission means general code-write access, not a promise that a
+particular protected branch accepts a push. The existing target and publisher guards still apply.
+
+| Forge | Permission read | Interpretation |
+|---|---|---|
+| GitHub | `GET /repos/{owner}/{repo}/collaborators/{username}/permission` | Use the effective `permission` base role: `write` or `admin`; maintain maps to write, triage to read. Verify returned user id. [Official contract](https://docs.github.com/en/rest/collaborators/collaborators#get-repository-permissions-for-a-user). |
+| GitLab | `GET /projects/{id}/members/all/{user_id}` | Includes inherited/invited membership. Known active Developer/Maintainer/Owner levels allow general push; read roles refuse. Unknown/custom capabilities require evidence, not numeric guesswork. [Official contract](https://docs.gitlab.com/api/project_members/#retrieve-a-member-of-a-project). |
+| Bitbucket Cloud | `GET /workspaces/{workspace}/permissions/repositories/{repo_slug}` | Read the matching stable user through all pages; `write`/`admin` are effective rights including groups. This endpoint requires repository-admin access from the caller. The `permissions-config/users` endpoint measures explicit grants only and is unsuitable. [Effective-permission contract](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-workspace-permissions-repositories-repo-slug-get). |
+
+403, timeout, rate limit, incomplete pagination and malformed/identity-mismatched responses cannot
+grant the command. A 404 is not automatically proof that a person has no rights: adapters must
+distinguish an unreadable repository from a known absent member where the API permits it. Both
+refuse; the displayed reason differs. For Bitbucket a reviewer lacking the documented admin access
+will report unknown; the accepted design requires an explicit capability error and an operator-facing credential prerequisite.
+Do not increase any live account's rights as part of implementation.
+
+## 5. Work sources and label evidence
+
+Create pure SPI module `spire-worksource`, with arms `spire-worksource-github`,
+`spire-worksource-gitlab` and `spire-worksource-jira`. Reuse each context adapter's client/auth
+configuration and `spire-http` transport. Refactor common provider transport into a reusable
+internal component and expose a distinct write-capable facade to the work-source arm. Context
+providers must still be unable to comment or transition through their public read-only interface.
+Use the existing module licensing split and add the new modules to build and architecture checks.
+
+`WorkSource` offers capabilities, paginated candidates, fetch, comment, transition and paginated
+`labelEvents`. A fetch returns transient ticket content and canonical identity. It is never a row
+to persist wholesale. A source registration owns type, origin, external project/repository scope,
+target repository id, explicit account reference, enabled state, revision, scan cursor and stable
+tracker actor allowlist. Matching kind, origin and auth are checked like context sources. A forge
+source can use its factory account for writes; an Atlassian source references the existing
+Atlassian account. No new “product owner” role is needed to call a work-source port.
+
+One work-source registration targets one repository in M3, matching one ISSUE webhook per
+repository. GitHub/GitLab issue coordinates must agree with that source. Jira's project mapping
+is operator-owned and may target an SCM repository on a different service; the tracker credential
+never travels there. Multi-repository issue routing is a future schema extension, not a label trick.
+
+### 5.1 Identity and bookkeeping
+
+Derive workItemId from versioned, length-safe encoding of `(scm type, forge origin, workspace,
+slug, work-source type, tracker origin, external project id, stable issue id)`. Do not use account
+id, handle, mutable issue key, source display name or title in the key. Duplicate registrations
+for the same source/target are refused. Re-admission of the same identity advances a generation;
+run attempts never reset to an already used id. A bounded hash/encoded subject links through the
+existing `RunIds` contract; add `factory_run.work_item_id`, generation and phase references, not
+a second incompatible parser for legacy run ids.
+
+`work_item` contains only coordinates, generation, admitted profile/version, selected/effective
+policy references, phase, workflow state/reason, revision and timestamps. It has **no issue title,
+body or tracker status column**. Workflow state is named `workflow_status` to prevent that
+confusion. Branch/PR/run links and human-supplied artifact references are workflow bookkeeping.
+An optional live title/body on detail is fetched from the tracker for that request; a failed fetch
+shows “tracker unavailable” while the durable workflow remains inspectable.
+
+Raw webhook bodies and fetched tickets must not leak into a generic durable inbox or timeline.
+Persist only normalized control facts. Notes that may quote ticket/code text, gate notes and outbox
+payloads carrying such text are Tink-encrypted with item/gate/effect AAD. Clear identifiers and
+reason codes remain queryable. Existing run task storage retains its encryption boundary.
+
+### 5.2 Labels have authors, removals and provenance
+
+Proposed `LabelEvent`: stable source event id, issue ref, label, `ADD|REMOVE`, tracker actor id,
+occurred time, provider ordering token when available and origin `WEBHOOK|AUDIT_TRAIL|UNATTRIBUTED`.
+The earlier sketch omitted removal and event identity; both are needed to prevent a replayed old
+addition from resurrecting authority. The **applier of the current addition** matters, not the
+issue reporter, assignee, most recent issue editor or person who created the label definition.
+
+Reconcile current labels with paginated label audit and verified webhook evidence. A later remove
+invalidates earlier attribution; a re-add needs its own actor. An audit gap or ambiguous ordering
+produces `UNATTRIBUTED`, never attribution borrowed from an earlier incarnation of the label.
+Polling after downtime and initial backlog scan run the same policy path as webhook intake.
+Checkpoint only after admission/reconciliation commits; redelivery is safe. Avoid resetting the
+scan cursor on every restart or persisting fetched ticket content to make polling easier.
+
+Adapter implementation must verify GitHub issue timeline label events, GitLab resource label
+events, and Jira changelog label deltas (including pagination and attribution) against their
+documented contracts. Source capabilities report where audit or transitions are unavailable.
+Unsupported audit is an honest loss of automation: present labels without a proven applier select
+nothing. Deletion/move is distinguished from token outage; inaccessible is suspended, not retired.
+A confirmed transfer retires the old item, closes gates, cancels unstarted effects and requires
+explicit admission under the new repository's policy; it never silently continues.
+
+## 6. Policy, versions and the phase boundary
+
+### 6.1 Named precedence and bounded vectors
+
+Store `autonomy_profile` and immutable `autonomy_profile_version` rows, repository ceiling and
+label mappings in the operator registry. Profiles carry the eight phase modes, gate TTL, run/step/
+wall-clock/cost/call caps and protected paths. Omitted phases are `off`. Validate phase-specific
+vocabulary: ordinary phases `off|approve|auto`, deliver `off|draft_pr|pr`, land
+`off|approve|auto_if_green`. Reject unknown modes, invalid caps, absent referenced versions and
+labels mapped to deleted profiles. Unknown wire modes render unknown/refused, never green.
+
+“Lowest wins” needs an order; names are not comparable and vectors can be incomparable. Even the
+published examples cross: suggest has `plan:auto`, assisted has `plan:approve`. A globally monotone
+chain would reject those examples. Propose an explicit unique precedence number per profile,
+owned/versioned by the operator, for selecting among labels and identifying an above-ceiling label.
+The three examples order suggest, assisted, autonomous; their names are not dispatch cases.
+
+Precedence never substitutes for a permission bound. Effective modes are the component-wise meet
+of selected, pinned and ceiling vectors: `off < approve < auto` for ordinary phases,
+`off < draft_pr < pr` for delivery and `off < approve < auto_if_green` for land. Numeric maxima
+take the minimum; protected paths take the union plus the immutable CI floor. No glob containment
+solver is required. Where the result is a composite vector, display the selected profile and the
+limiting ceiling plus actual phase modes, not a claim that it equals an unmodified named profile.
+Reject duplicate precedence, missing versions and invalid modes; accept cross-cutting vectors only
+with this meet. The review accepted this ordering/composition rule. **The effective vector is
+never above any applied label in any component.** Here applied means current mapped labels with
+proven, allowed appliers; ignored labels have no authority. Meet every eligible label's vector,
+not just the lowest-precedence display selection, plus the pinned admission vector and ceiling.
+
+Initial examples explicitly declare `intake: auto`; copying the abbreviated AUTONOMY YAML without
+that field would correctly default intake to off and admit nothing. Use these complete vectors:
+
+| Profile | intake | spec | plan | build | verify | review | deliver | land |
+|---|---|---|---|---|---|---|---|---|
+| suggest | auto | auto | auto | off | off | off | off | off |
+| assisted | auto | auto | approve | auto | auto | auto | draft_pr | approve |
+| autonomous | auto | auto | auto | auto | auto | auto | pr | auto_if_green |
+
+These are desired permissions, not claims that M4 executors exist. Missing verify/review/land
+capabilities still block their transitions; the M3 journey proof must show that boundary honestly.
+
+At admission pin the chosen profile id/version and the mapping revision used. Compute the most
+restrictive eligible label selection and the current ceiling; persist requested/effective selection
+and why a clamp occurred. At later transitions re-read current labels, source allowlist, enabled
+states, mappings and ceiling. Intersect the admitted version with current restrictions. A removed
+label, disallowed applier or new lower label can narrow/stop an item. A higher label, raised ceiling
+or edited version cannot widen its admitted authority. An operator explicitly re-admits to move to
+a new version/generation. A lower ceiling's current version restricts the pinned vector; it never
+silently replaces the admitted version's more restrictive fields.
+
+Store the applied policy revision and provenance at each decision so the screen can explain it.
+Ceiling clamps produce a durable timeline entry and condition-based attention row while the
+current selection is clamped. Deduplicate repeated observations of the same clamp. Invalid labels
+each have an ignored reason; if another valid label remains, it can select a profile. If none
+remain, `not_eligible` stops automation with the specific underlying reason visible.
+
+### 6.2 Every transition is a real check
+
+One `WorkItemTransitions` service is called for admission, phase completion, gate approval,
+retry, run-result continuation, delivery, land, operator resume and explicit re-admission.
+Expiry, takeover and retirement invalidate pending effects as well. Scheduled/outbox retries
+cannot bypass this service. Fetch external evidence outside a DB lock, then compare its source/
+repository/policy revisions under lock; stale or failed reads cause waiting, not permission.
+External revocation and a local dispatch cannot be globally atomic: the guarantee is a fresh
+observation at each transition, not instantaneous revocation of a push already accepted remotely.
+
+A lowered ceiling stops advancement at the next phase. If its mode becomes `approve`, open a new
+version-bound gate before proceeding; if `off`, record `not_eligible` and stop. It need not kill
+the phase already running. A gate approved against old policy or an older artifact/head is stale
+and cannot authorize the new transition. Failed verify is not item success; M4 owns retries and
+step verification. Budget limits narrow existing SpendGate/FR-F32 checks and include call count
+on unmetered deployments. Reserve a dispatch slot atomically and release it on refusal/expiry;
+do not claim hard monetary reservations eliminate the documented in-flight spend softness.
+
+### 6.3 M3 journeys versus M4 execution — accepted boundary
+
+FR-F17 spans M3/M4. M4 explicitly owns generating specifications/plans, multi-step execution and
+verification. M3 cannot label no-op phase handlers “complete” to manufacture three green journeys.
+Accepted M3 boundary: implement the real phase state machine and manual tracker-artifact handoff,
+then reuse M2 for **one already specified build task**. Humans can register references/digests to
+a specification and a single-step plan actually present in the tracker. Those artifacts are fetched
+and validated; they are not copied into `work_item`. Missing execution capabilities show
+`awaiting_input` or `capability_unavailable`, never a fake successful phase.
+
+With the same prepared task and three profile labels the runnable control-plane proof is:
+
+| Profile | Visible journey in M3 |
+|---|---|
+| suggest | Admit; record the human-provided specification/plan references; stop before build (`off`), zero runs, no PR. |
+| assisted | Admit; visibly wait on a durable plan gate with zero runs. Approval admits one build; then wait for any missing verification capability. Its eventual permitted delivery is a draft PR. |
+| autonomous | Admit; plan proceeds without approval and starts one build immediately; then wait for any missing verification capability. Its eventual permitted delivery is a regular PR. |
+
+No `auto_if_green` implementation or automatic tracker closure is implied by a green unit test.
+Criterion 1 is proved at the real plan/build boundary: suggest stops, assisted waits for approval,
+autonomous builds. After approval, assisted's history still records its distinct human decision.
+Draft/regular delivery tests use an explicitly identified test phase driver to supply verification
+evidence; this is adapter/control-plane coverage, not proof of a shipped M4 verifier. Production
+with no verifier remains waiting. The review accepted this plan/build-boundary proof; generated
+specification, multi-step planning and verification executors remain M4 work.
+
+Delivery/review ordering is corrected by the review: the published eight-phase diagram places
+review before deliver, but the existing reviewer requires a pushed PR. Proposed execution records
+PR opening as the delivery effect, then observes the existing reviewer before any land decision;
+it does not report a review that could not have run. Preserve phase identifiers in the policy
+vector; record `intake → spec → plan → build → verify → deliver → review → land` in ADR-045 and
+fix the eight-phase diagram in `docs/factory/AUTONOMY.md` in slice 8a, together with affected
+architecture/PRD diagrams. The diagram is wrong; the implemented reviewer is not changed to fit it.
+
+**Publication is part of that decision too.** M2 builds already push before `RunFinished`; merely
+gating the later PR API call cannot enforce a deliver mode of off. Proposed item-linked execution
+starts with publication held, checkpoints local work and emits a durable `RunWorkReady` integration
+result before push. The worker releases active compute while preserving the workspace and a
+durable awaiting-delivery record. Only a current, item/generation-bound delivery permit resumes
+trusted publisher finalization; it cannot rerun the build or accept a repository-authored permit.
+Standalone M2 runs retain their existing automatic push. Expiry/retirement/takeover leave work
+preserved without publishing, and orphan recovery honors the hold. This introduces a run state
+and control/result messages, not a run aggregate. The design must establish charge reporting at
+work-ready/final completion without double counting, and the artifact/verification boundary before
+granting delivery. This is its own slice **8b**, following 8a; slices 9 and 10 keep their numbers.
+Its two-part exit requires item-linked publication hold through restart **and** a standalone
+`/fix` still pushing automatically, re-proved live on `artyomsv/spire-test`. Unit tests cannot
+replace that second proof. The current M2 worker does not already support the hold.
+
+## 7. Durable approvals
+
+`work_item_gate` records id, item/generation, phase, expected item/policy version, artifact digest
+or PR head, opened/expiry timestamps, status (`OPEN|APPROVED|REJECTED|EXPIRED|SUPERSEDED`), resolver
+stable identity/channel, deduplication key and encrypted note. One current open gate per item,
+generation and phase. It is a synchronous transactionally maintained query of aggregate state;
+the event log remains rebuildable truth. Concurrent responses use expected version and a
+conditional OPEN transition. One wins; replay of its idempotency key returns the stored outcome;
+a conflicting answer returns 409. At `now >= expiresAt` expiry wins even if a scheduler is late.
+
+| Channel | Authority and binding |
+|---|---|
+| Dashboard | Existing authenticated operator authorization (`spire-admin` for gate mutations initially); server derives resolver from verified OIDC subject. Viewer can read only within existing access rules. |
+| Tracker | Allowlisted actor in this work source; authenticated delivery; explicit command such as `/approve ` or `/reject ` binds the generation and artifact. Ordinary comments do not approve. |
+| PR review | Current approval of this linked PR's current head by a human with measured repository push permission and no deny override. It can answer only a land gate, never a plan gate. Re-read review state; dismissed/stale approvals do not count. |
+
+All channels become the same `ResolveGate` command and `GateResolved` milestone. Tracker label
+answers are deferred unless a gate-specific label can carry unambiguous generation and attributable
+actor; “a comment or a label” does not require implementing an unsafe generic approve label.
+If a forge cannot prove a PR approval, its capabilities disable that channel visibly; dashboard
+and tracker remain usable. Expiry is a persisted `WorkItemRefused(gate_expired)` with reservation
+release in the same transaction. A restarted sweeper expires overdue gates. Retrying a refused
+item needs explicit re-admission, not reopening the old approval.
+
+## 8. Human takeover
+
+Signed push/PR activity on an item-linked branch/PR records `human_takeover` and suspends new
+automation until an operator resumes. Compare the actor's stable id against the item's recorded
+factory identity and assigned reviewer identity, not display names, author strings in commits or
+the current factory account after it has been rotated. Unknown origin suspends conservatively.
+Repo/head links must match; unrelated branches cannot suspend an item. A bot's observed push
+does not count as a person, even after an account has been renamed.
+
+Gate answers and authorized `/fix` commands are deliberate workflow actions; classify and
+deduplicate them before generic comment takeover. This is a proposed precedence rule resolving
+FR-F22's literal “commenting” against FR-F25's tracker/PR answer channels; record it in ADR-045 with the FR-F22/FR-F25 conflict named.
+Normal human comments and pushes take over. A PR approval is processed as a gate response only
+when it actually matches an open gate; it cannot accidentally resume a suspended item.
+
+Takeover cancels unstarted outbox effects, supersedes pending gates and requests active-run stop.
+**Normal M1 cancel salvages and may push. It is insufficient for takeover.** Introduce a durable
+publication hold for item-linked runs: preserve local work while suppressing further pushes and
+PR creation after the hold is observed. Carry the hold through run control, worker durable state,
+publisher finalization and orphan salvage; a restart must not restore publication authority.
+Publication already in progress cannot be recalled; record its outcome and keep the item
+suspended. Never claim atomic ordering between a remote human push and our webhook receipt.
+The run-plane mechanics and the interaction with continuous checkpoints need explicit tests in
+slice 9, not just a saga fake. Existing standalone cancellation retains its salvage contract.
+
+Resume is an authenticated operator action with expected version and a note. Re-fetch repository/
+issue/head, re-resolve policy and open any new gate before continuation. A retired item cannot
+resume; it requires a new identity/admission. No automatic resume on a bot comment or on a new label.
+
+## 9. Screens, resources and observability
+
+| Route / API family | Behavior |
+|---|---|
+| `#/settings/repositories`, `/api/repositories` | Repository registration/detail, workspace, exact role bindings, per-kind hooks, source/policy setup and fix overrides. Non-secret account views. |
+| `#/settings/accounts`, `/api/providers` | Identity, credential, scalar role and Used by; no workspace control. Handle entry renders people, not a count alone. |
+| `/api/work-sources` | Admin source registration, actor allowlist, Check and bounded rescan. Uses an existing account, never a second token form. |
+| `/api/autonomy-profiles`, repository policy subresource | Versioned profiles, mappings and ceiling; optimistic revisions on edits. |
+| `#/work-items`, `/api/work-items` | Paged durable list by repository/source/workflow state/profile. Detail: tracker link, current phase, requested/effective profile, ignored labels, clamp reason, gates and links to real runs/PR/review. |
+| `#/approvals`, `/api/approvals` | Open approvals with expiry, phase, artifact/head and decision note; authorized approve/reject. Separate history query for resolved gates. |
+| Existing attention API | Current open gates, effective clamps, unknown permission/account mapping and failed source health. Resolving the condition removes its row. |
+
+Resources enqueue durable commands and return 202 plus a command/item id; they do not hold an HTTP
+request open for a phase. Configuration writes retain ordinary synchronous registry semantics.
+Read APIs expose a revision for bounded polling/live updates and explicit errors on source fetch
+failure. UI statuses, labels, pipeline renderer, filters and unknown-state handling land together.
+Keep new React components below 250 lines and eight state hooks, using existing controls/icons.
+Never label a scheduled test fixture or an unsupported phase as a live completed item.
+
+## 10. Proof strategy and excluded work
+
+The [plan's acceptance matrix](../plans/2026-09-12-factory-m3-work-items.md#acceptance-proof-matrix)
+names an executable test for each of the seven ticket criteria and an isolated, compiling mutation
+that must kill exactly one discriminating test. It also covers migration, replay, concurrency,
+expiry, transfer, channels, takeover and missing capabilities. Test names are proposed additions;
+none are represented as tests that exist or have passed today.
+
+M3 excludes M4-generated specification/plan, multi-step continuity, repository verification runners,
+automatic merge implementation without a separately accepted scope decision, model quality claims,
+new runtime/harness arms, a product-owner role, context auto-discovery, account-role merging and
+general remediation of historical review-id host collisions. It includes the seams and honest
+unavailable states needed so those features can arrive without bypassing policy.
+
+No production code, migrations, dev data, Gradle execution or runtime restarts belong to Round 1.
+Commit only this design and its plan, push the existing branch, open the requested draft PR.
+
+## 11. Review decisions — settled in Round 2
+
+The [review summary and six inline comments](https://github.com/artyomsv/code-spire/pull/153#pullrequestreview-5188278775)
+settled all five questions. These are requirements for implementation, not pending approvals.
+
+1. **Journeys/order:** accepted real state machine, manual tracker-artifact handoff and one
+ prepared M2 build, proved at plan/build. Missing later capabilities wait honestly. Correct
+ delivery-before-review in ADR-045 and the eight-phase diagram in slice 8a.
+2. **Profiles:** accepted operator precedence plus the meet of every eligible applied label,
+ pinned vector and ceiling. ADR-045 states: **the effective vector is never above any applied
+ label in any component.** Precedence selects display/clamp wording, never authority by itself.
+3. **Identity/permission:** explicit capability errors and disambiguated selection are accepted;
+ do not guess or raise token authority. State credential prerequisites in operator-facing text.
+ Add each per-forge identity behavior as its own UNVERIFIED entry in its introducing slice,
+ identifying the forge, measurement and remaining proof. This includes Bitbucket's admin-only
+ effective-permission query and provider-specific handle resolution behavior.
+4. **Repositories:** one source/ISSUE hook per repository, with REVIEWER/FACTORY/ISSUE kinds.
+ Org auto-enrollment exists only during the bridge. At cutover an unregistered repository event
+ raises attention naming repo, origin and incoming registration, with all three pre-filled in
+ the Register action. Retain populated account workspace evidence, unused after slice 2, until
+ slice 10. Back up the actual dev database before migrations and compare actual credential
+ decryption after slice 2, using the exact commands in the plan.
+5. **Drafts/takeover:** native drafts or explicit refusal, never a title prefix. Commands and
+ gate answers precede generic comment takeover; ADR-045 names the FR-F22/FR-F25 conflict.
+ Slice 8b separately owns publication hold and draft delivery after 8a. Its exit also requires
+ a live standalone `/fix` on `artyomsv/spire-test` still pushing automatically. Slices 9 and 10
+ retain their numbers.
diff --git a/docs/superpowers/specs/2026-09-15-factory-operator-experience-design.md b/docs/superpowers/specs/2026-09-15-factory-operator-experience-design.md
new file mode 100644
index 00000000..45df9a09
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-15-factory-operator-experience-design.md
@@ -0,0 +1,805 @@
+# Factory operator experience — findings analysis and improvement specification
+
+**Date:** 2026-09-15. **Input:** the operator's ten findings after the first live item-linked build
+(`artyomsv/spire-test` issue #36, run `run::github:artyomsv/spire-test:work-c77f4b21-…:1`), the
+manual test script they followed, and the code on `feat/factory-m3-work-items`. **Scope:** the
+Work items list and detail, the prepared-task registration form, the Approvals page, the sidebar,
+the repository Factory tab, and the pricing gap that stopped the run.
+
+**The product goal this specification must reach**, in the operator's words: *"I have one ticket,
+I mark it for work and I receive a PR at the end, or for some projects the PR is even auto-merged."*
+It is treated here as the target, not as a wish.
+
+Every statement about current behaviour carries a `file:line` or a document section. Anything not
+measured is marked **not verified**. Nothing here proposes removing a guard; each manual step is
+named with the guard it protects and the way the system can satisfy that guard on its own.
+
+---
+
+## 0. Summary
+
+| # | Finding | Root cause (short) | Coverage | Group |
+|---|---|---|---|---|
+| 1 | Work item detail is a mess | One flat card, sections in component-arrival order, raw enum values, two Refresh buttons, form always inline (`WorkItemDetail.tsx:27-56`) | M3 spec §9 lists content, not layout — **not covered** as a redesign | a (structure), b (mockups) |
+| 2 | Back and forth to find ticket IDs | Spec and plan are *other* tickets typed as free text (`WorkItemPreparation.tsx:39-40`); the item's own title loads last (`WorkItemDetail.tsx:49-54`) | Consequence of 3; M4 FR-F18 removes the separate tickets — **partly covered** | b |
+| 3 | Three issues for one task | M3 accepted a "manual tracker-artifact handoff" in which spec and plan must be tickets of the same source (`PREPARED-TASKS.md`, `WorkArtifacts.java:53-55`) | **Covered by M4** (FR-F18/F19: generated spec and plan, written back to the *same* ticket) | c, plus a bridge in b |
+| 4 | Copying JSON into a plan ticket | The plan contract is a JSON body a human must author (`WorkArtifacts.java:37-47`; template at `WorkItemPreparation.tsx:44-45`) | **Covered by M4** for generated plans; the human path is **not covered** | b |
+| 5 | Base commit, harness and model as free text | `WorkPreparationResource.Input` takes four strings (`:21-22`); the form renders six ``s (`WorkItemPreparation.tsx:39-40`); no repository default exists — `/fix` has env defaults, items have none (`.env.example:41-48`, `FixRunDispatcher.java:272-293`) | **Not covered** | a (selects), b (defaults, base head) |
+| 6 | 409 `single_step_plan_required` | One reason for three rules (`WorkArtifacts.java:39,43,47`); raw JSON error surfaced (`workPreparationApi.ts:22`); stale references kept after a failed register (`WorkItemPreparation.tsx:24-33,46`); plan SHA never shown (`:46`) | **Not covered** | a |
+| 7 | No processing indicator | Buttons disable but never say what is happening; reads show a bare ``; "Scan now" has no pending state at all (`SourceStep.tsx:25-29,46-47`) | WIDGETS.md asks for lock-out and success feedback, not for progress words — **not covered** | a |
+| 8 | Approvals nav has no icon | `App.tsx:228` renders the link without the `ic` icon every other entry has (`:229-232`; WIDGETS.md row "Navigation") | **Not covered** | a (S) |
+| 9 | Approvals page unusable | Cards show internal fields (generation, policy revision, ISO expiry, raw digest) and none of the evidence the gate binds (`Approvals.tsx:43-57`) | M3 spec §9 lists fields, not the decision's evidence — **not covered** | a (content), b (mockups) |
+| 10 | API or subscription? | Measured: harness credential `factory-openai`, type `openai`, base URL `https://api.openai.com/v1` — an **API key**, billed per token (ADR-031; EXECUTION-LAYER §3.2) | Answered; the UI does not say it — **not covered** | a (S) |
+| — | Too many manual steps | M3's accepted boundary is a human-prepared task (M3 spec §6.3); nothing resolves base, harness, model or artifacts for the operator; no verifier exists, so the PR can never open (`WorkPhaseCapability.java:11-16`) | M4 covers spec/plan/verify; the *defaults and composition* are **not covered** | b (new slice "M3.5"), c |
+
+The measured stop: work item 36 ended at `verify | awaiting_input | run_usage_unknown` (dev DB,
+2026-09-15). `gpt-5.5` carries rates for `INPUT,OUTPUT` only (dev DB `llm_model_rate`, measured
+2026-09-15); the codex adapter also reports `CACHED_INPUT` and `REASONING`
+(`CodexAdapter.java:276-279`), so the run's cost was unknown and the lifecycle refused to continue
+(`WorkItemLifecycle.java:65`). The pre-dispatch check only asks for `INPUT` and `OUTPUT`
+(`LlmModelPricer.java:78-85`, `LlmModelPricingValidator.java:21`). Section 5 owns this.
+
+---
+
+## 1. Findings, one by one
+
+### Finding 1 — "Work item detail view is a mess"
+
+**What is on screen.** `WorkItemDetail.tsx:27-56` renders one `card` with, in order: back link,
+`
{issueKey}
` (a bare number on GitHub), tracker link, the journey block *or* a workflow
+block, the policy block (raw mode table, `WorkItemPolicy.tsx:9-11`; limits as raw seconds and
+millicents, `:14-17`), a one-line gate link, the admin action buttons, the full "Register a prepared
+task" form inline (`WorkItemPreparation.tsx:35-50`), a second Refresh button (`:39`), applied
+labels with raw actor ids and `origin.toLowerCase()` (`:41`), ignored labels, a history `` of
+`"Workflow updated: "` lines (`:48`), and finally the ticket title and body (`:49-54`).
+
+**Root cause.** The screen grew one section per slice (policy in slice 7, journey/preparation in
+slice 8a, control in slice 9) and each slice appended a block; nothing ordered them by what a
+reader needs first: *where is this item, what is it waiting for, what do I do now.* The design
+brief for the screen (M3 spec §9: "Detail: tracker link, current phase, requested/effective
+profile, ignored labels, clamp reason, gates and links to real runs/PR/review") lists content and
+says nothing about hierarchy, so every block was placed as prose. ADR-045's consequence ("The UI
+displays requested profile, ceiling, actual modes, limits and clamp reason") is met literally and
+unreadably.
+
+**Coverage.** Not covered by any later milestone. M4 adds more content (spec text, plan steps,
+verification result, step summaries — FR-F31), which makes a redesign more urgent, not less.
+
+**Why it is this way / guards.** No guard lives in the layout. The one structural rule worth
+keeping is ADR-043's split: durable workflow facts come from the aggregate, tracker content is a
+separate fetch that may fail without hiding history (`WorkItemDetail.tsx:50`). The redesign keeps
+that: the ticket panel fails alone.
+
+**Proposed change.** Section 3 gives the full brief. In one line: a status band with the one next
+action, a phase strip with the current phase marked, then evidence cards (build, approval, policy,
+labels, ticket), the history as a timeline, and the preparation form in a `SidePanel` opened by the
+next-action button rather than always inline.
+
+**Acceptance check.** A viewer opening item 36 reads, above the fold and without scrolling: the
+ticket title, the phase (`verify`), the status in words ("Blocked: the run's usage could not be
+priced"), and the single action the operator can take. `WorkItemDetail.test.tsx` (new) asserts the
+order of landmarks by `aria-label`; a mutation that moves the ticket panel above the status band
+fails exactly that test.
+
+### Finding 2 — "I need to go forth and back to know what is the ticket ID"
+
+**Root cause.** Two causes, both in the form at `WorkItemPreparation.tsx:39-40`: the specification
+and plan are separate tickets the operator created outside the dashboard (test script steps 3–4),
+and their keys are typed into free-text inputs with no lookup. The work item itself is shown by
+its key only (`WorkItemDetail.tsx:30`); its title arrives at the bottom of the page after a second
+fetch (`:49-54`), so even the item's identity is hard to confirm.
+
+**Coverage.** Consequence of finding 3. Once the specification and the plan live with the ticket
+(M4 FR-F18: "written back to the tracker as a comment") there is no second or third ID to remember.
+
+**Guard protected.** The reference lookup (`WorkArtifacts.resolve`, `:19-26`) resolves a key to a
+stable provider identity through the *selected source account* and refuses a ticket from another
+project (`:53-55`). That is what stops a plan from an unrelated repository being bound to this
+item. The guard is about identity and origin, not about who types the number.
+
+**Proposed change.** (1) Show the ticket title in the page heading, with the key as a mono
+subtitle. (2) Remove the two ticket fields from the human path by the design in finding 3; where a
+reference to another ticket is still wanted, replace the free-text input with a search-as-you-type
+lookup that calls `preparation/reference` and shows the resolved title before the key is accepted
+(same shape as `AllowPerson` in `PeopleStep.tsx:19-59`: find, confirm, then act).
+
+**Acceptance check.** The registration path in section 2 needs zero ticket IDs typed by a person.
+
+### Finding 3 — "Why we created 3 issues for a work? One task/epic is not enough?"
+
+**Root cause.** By design for M3. `PREPARED-TASKS.md`: "The specification and plan must be tickets
+in the work item's registered source." `WorkArtifacts.fetch` enforces same type, origin and project
+(`:53-55`) and reads the *body* of the referenced ticket (`:56-60`). The M3 design accepted this as
+the boundary between M3 and M4: "implement the real phase state machine and manual
+tracker-artifact handoff, then reuse M2 for one already specified build task" (M3 spec §6.3). The
+three tickets are the price of proving the plan/build boundary without an M4 generator.
+
+**Coverage.** **Covered by M4.** ROADMAP M4: "The `spec` phase — a vague ticket refined into
+outcome, context and acceptance criteria, written back to the tracker." Design question 1 (ROADMAP
+"Design questions — closed"): "A comment on the work item, plus the structured form on `work_item`
+for the pipeline's own use." FR-F18 and FR-F19 generate both artifacts from the *one* ticket.
+
+**Guard protected.** Pinned artifact digests. The plan gate binds the specification and plan
+digests plus base branch, commit, harness and model (ADR-045; `WorkPreparation.binding()`,
+`:31-40`), and every continuation re-reads the artifacts and refuses on a changed digest
+(`WorkArtifacts.observe`, `:34-36`; `WorkItemTransitions.answer`, `:231`). The guard needs
+*something with a digest that the gate can bind and the build can re-verify*. It does not need
+that something to be a separate ticket.
+
+**Proposed change.** Keep the digest guard; change what it hashes:
+
+- **M4 (already designed):** the generated specification and plan are stored on the work item in
+ their structured form (encrypted, under the existing Tink boundary — DATA-MODEL, ADR-043 last
+ paragraph) and a readable copy is commented on the ticket. The digest is taken over the stored
+ form. Editing the tracker comment does not change what the gate bound; re-generating does.
+- **Bridge before M4 (section 2.4):** for a ticket that already contains its acceptance criteria,
+ the system composes the preparation itself: specification := the ticket's current body (digest
+ pinned exactly as today), plan := one step whose instruction is the ticket body or an operator
+ sentence typed in the dashboard, stored in the same structured slot M4 will use. No second or
+ third ticket exists. This is the path the operator's test actually exercised, minus the typing.
+
+**Acceptance check.** `WorkItemJourneyIT` gains a case that admits a ticket, composes the
+preparation from the ticket alone, opens the plan gate and dispatches one build after approval —
+with exactly one tracker issue in the fixture. A mutation that skips the digest pin on the stored
+plan fails the existing `artifacts_changed` case.
+
+### Finding 4 — "It is not good design that I need to copy some JSON to a plan ticket"
+
+**Root cause.** The plan is a machine contract — `{schemaVersion, specificationSha256, steps[1]}` —
+validated at `WorkArtifacts.java:37-47`, and the UI hands the human that JSON to paste
+(`WorkItemPreparation.tsx:44-45`). The `specificationSha256` field is what forces the operator to
+"Read specification version" first, then copy, then edit a ticket, then come back (test script
+step 8.2–8.3). The live 409 was a newline inside the instruction string, i.e. the human was doing
+a serializer's job.
+
+**Coverage.** Generated plans (M4 FR-F19) never show the JSON to a person. The human-authored path
+is not covered.
+
+**Guard protected.** `specificationSha256` binds the plan to the exact specification version it was
+written against, so an edited spec cannot ride under an old plan; "exactly one step" bounds M3 to
+the one-build case the run assembly supports (`WorkRunAssembly.java:33`).
+
+**Proposed change.** The system composes the JSON. The human supplies at most one sentence — the
+step instruction — in a dashboard field; the server fills `schemaVersion` and the SHA of the
+specification it just pinned, validates, stores and pins. The JSON stays the internal wire form and
+the audit record; it is never displayed except behind a "show the stored plan" disclosure.
+
+**Acceptance check.** No "Single-step plan format" disclosure remains in the form. A plan whose
+instruction contains a newline registers successfully.
+
+### Finding 5 — "Base commit, harness as free text, models"
+
+**Root cause.** `WorkPreparationResource.Input` (`:21-22`) takes `baseBranch`, `baseCommit`,
+`harness` and `model` as strings; `WorkPreparation` only checks the commit is 40 hex characters
+(`:26-28`). The form renders all four as `` (`WorkItemPreparation.tsx:39-40`). Nothing in
+the registry knows a repository's default branch head, allowed harnesses, or a default model for
+items: the harness names are the keys of `FactoryConfig.agentImage()` (`FactoryConfig.java:24`),
+the models are the LLM catalog (`llm_model`), and `/fix` gets its pair from two environment
+variables that deliberately have no default (`.env.example:41-48`,
+`FixRunDispatcher.refuseIfUnconfigured`, `:272-293`). Work items have no equivalent, so the human
+types what `/fix` reads from config.
+
+**Coverage.** Not covered by M4–M6. M4's plan phase needs the same coordinates and would inherit
+the same form.
+
+**Guards protected, and how each stays satisfied automatically.**
+
+| Field | Guard | Automatic satisfaction |
+|---|---|---|
+| `baseCommit` | The gate binds a commit (ADR-045: "binds both artifact identities/digests and the base branch, commit, harness and model"), and the build clones at an explicit commit (FR-F2) — an approval is for a change against a known tree | The server resolves the head of the base branch through the repository's SCM client at registration, shows "`main` @ `a0f8a41` (head when prepared)", and pins that. A person may override with a full SHA in an "advanced" disclosure. The same check that today refuses a moved artifact can, later, tell the approver the base is behind head (**not verified**: needs a compare call per forge) |
+| `baseBranch` | Same | Default to the repository's default branch (the `.codespire` rule already reads the target branch; the SCM model knows the default branch — **not verified** that `Repository` stores it) |
+| `harness` | Must be a key of `agentImage` or dispatch refuses (`FactoryConfig.java:51`; `DispatchRequestParser`) | A `