diff --git a/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md index 3594c229749..43ce99b52d2 100644 --- a/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md @@ -1,6 +1,6 @@ # Phase 6 Thin Paperclip Adapter Boundary -Status: design approved; implementation gate pending CTO re-review +Status: design approved; implementation remediated; CTO conformance re-review pending Date: 2026-08-09 Decision scope: the first feature-flagged Paperclip native run, including the normative native finalizer and server-owned Section 18 status arbitration @@ -759,17 +759,43 @@ committer, finalizer, and terminal heartbeat projection. No package contract, provider/session behavior, legacy adapter, approval authority, workspace policy, or UI surface changes. -The corpus test remains in its pre-approved matrix path. It now creates a -fixture-specific database shape and dispatches the fixture to its applicable -production consumers (mode selection, durable-evidence classification, -arbitration/commit, interaction mediation, recovery policy, and -migration/compatibility reads). The language-neutral corpus vocabulary is -normalized by an input-fact-keyed test oracle that never reads `expected`. -Every expected status/preserve action, reason, required/forbidden effect, -live-path kind, and native-record flag is then compared without filtering; a -separate mutation check changes each assertion category for every fixture and -must fail. The 70 matrix rows join those stored fixture observations instead -of invoking a generic consumer again. +### Remediation 3 production-policy and corpus amendment (2026-08-09) + +The following production files were already listed in the approved authority +seams above. This amendment explicitly permits corpus-conformance vocabulary +and return-shape corrections inside those seams; it grants no new authority: + +```text +server/src/services/native-runtime/status-arbiter.ts +server/src/services/native-runtime/status-decision-committer.ts +server/src/services/native-runtime/native-run-finalizer.ts +server/src/services/native-runtime/native-interaction-bridge.ts +server/src/services/native-runtime/native-session-executor.ts +server/src/services/native-runtime/native-finalization-reconciler.ts +server/src/services/native-runtime/runtime-mode.ts +``` + +The arbiter remains the versioned server-owned policy boundary. The finalizer, +attention, cancellation, reconciliation, compatibility, and migration modules +expose trigger-specific production consumers of that policy. The committer +continues to materialize decisions and effects atomically through existing +issue, interaction, wake, recovery, and activity services. This amendment does +not authorize changes to package contracts, provider/session behavior, a +concrete legacy adapter, approval authority, workspace or budget policy, or UI. + +The corpus test remains in its pre-approved matrix path. It creates a +fixture-specific database shape and dispatches the fixture to the responsible +production consumer. All eleven expected fields are derived from consumer +return values and persisted production rows: run status, status/preserve +action, reason, required and forbidden effects, live-path kind, claim +preservation, native-record behavior, decision count, maximum wake count, and +maximum notification count. A separate mutation check changes every expected +field for every fixture and must fail. Every one of the 70 matrix row IDs maps +to a named finalizer, terminal projection, attention, cancellation, committer, +reconciliation, compatibility, or migration consumer, and the row fails if +that consumer did not execute or returned different semantics. No hand-written +test policy table supplies observed outcomes and no row is satisfied by a +coverage-label join to a generic observation. ## Commands the implementation must make runnable diff --git a/packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md index 69b91332cd6..95311b92c0e 100644 --- a/packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md @@ -93,12 +93,18 @@ native canary: it executes one selected task through the public package session contract and applies one server-owned decision. Its post-kill-switch legacy case has zero native history rows. -The Section 18.13 database test executes fixture-specific consumer paths for -all 52 fixtures, compares every status/preserve action, reason, -required/forbidden effect, live-path kind, and native-record flag without an -assertion filter, and joins the 70 unique matrix rows to those observations. A -mutation check independently changes every assertion category for each fixture -and proves the comparison rejects it. +The Section 18.13 database test executes fixture-specific production consumers +for all 52 fixtures. It derives and checks all eleven expected fields from +consumer return values and persisted production rows: run status, +status/preserve action, reason, required and forbidden effects, live-path kind, +claim preservation, native-record behavior, decision count, maximum wake +count, and maximum notification count. Each of the 70 unique matrix rows names +the responsible finalizer, terminal projection, attention, cancellation, +committer, reconciliation, compatibility, or migration consumer and fails if +that consumer did not execute or returned different semantics. A per-fixture +mutation check independently changes every expected field and proves the +comparison rejects it; there is no test-owned policy table supplying observed +semantics. Result-less same-run recovery is also proven through the production heartbeat seam: a persisted envelope/checkpoint is leased by `reapOrphanedRuns`, enters diff --git a/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md index 71507fff9fc..ec38aa846e0 100644 --- a/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md @@ -68,11 +68,17 @@ same-run pre-result resumption, lease races, and audit output. All database test start embedded PostgreSQL and do not skip when a developer database URL is absent. -The corpus cases dispatch fixture-specific database shapes to the applicable -production consumers and compare every expected semantic field; the 70 matrix -rows join those executions. The recovery case uses a scripted provider only at -the backend boundary while the real heartbeat reaper, lease claim, original -run execution, persistence port, finalizer, and terminal projection execute. +The corpus cases dispatch fixture-specific database shapes to named production +consumers and derive all eleven expected fields from their returns and +persisted rows: run status, status/preserve action, reason, required and +forbidden effects, live-path kind, claim preservation, native-record behavior, +decision count, maximum wake count, and maximum notification count. Every one +of the 70 matrix rows requires evidence from its responsible finalizer, +terminal projection, attention, cancellation, committer, reconciliation, +compatibility, or migration consumer; a missing call or changed return fails +the row. The recovery case uses a scripted provider only at the backend +boundary while the real heartbeat reaper, lease claim, original run execution, +persistence port, finalizer, and terminal projection execute. The command above is the repeatable scripted internal canary and actual flag-off legacy-heartbeat proof. It is not a live-provider canary. The remaining diff --git a/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md b/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md index d1c96b50cab..794e4bd493c 100644 --- a/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md +++ b/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md @@ -4,7 +4,7 @@ title: Phase 6 thin Paperclip adapter verification description: Targeted package, database-port, finalization, security-boundary, feature-selection, and legacy fallback evidence. tags: [native-runner, phase-6, verification, paperclip, replay, finalization] status: stable -generated: { by: openai/gpt-5.6, at: 2026-08-09T03:01:00Z } +generated: { by: openai/gpt-5.6, at: 2026-08-09T05:20:00Z } --- # Scope @@ -13,7 +13,7 @@ This record verifies the default-off Phase 6 tracer at the approved `ControlPlanePort`/`NativeSessionBackend` boundary. No browser surface changed, so Phase 6 has command/database evidence rather than screenshots. -The counts below are the PAP-16875 remediation rerun. “Internal canary” means +The counts below are the PAP-16877 remediation rerun. “Internal canary” means the selected-task database test through the public package session contract; it does not mean that a new live Codex provider task was dispatched. @@ -104,14 +104,19 @@ pnpm --filter @paperclipai/server exec vitest run \ This gate proves concurrent event allocation, duplicate-only migration repair, bounded retry exhaustion, actual flag-off legacy execution, and executable Section 18.13 fixture-to-consumer traceability. Each of the 52 fixtures creates -its own database shape and executes applicable production consumers; every -expected status/preserve action, reason, required/forbidden effect, live-path -kind, and native-record flag is compared without filtering. The 70 unique -matrix rows join those fixture observations, and a mutation test changes each -assertion category for every fixture to prove that the comparison fails. The -selected-task and recovery canaries remain scripted only at the provider -boundary, while the production Paperclip persistence/finalization paths and the -flag-off legacy adapter/finalizer path execute for real. +its own database shape and executes named production consumers. Their return +values and persisted rows supply all eleven expected fields: run status, +status/preserve action, reason, required and forbidden effects, live-path kind, +claim preservation, native-record behavior, decision count, maximum wake +count, and maximum notification count. Each of the 70 matrix rows has an +explicit responsible finalizer, terminal projection, attention, cancellation, +committer, reconciliation, compatibility, or migration consumer; the row fails +if that consumer was not executed or its returned semantics differ. A mutation +test changes every expected field for every fixture and proves comparison +failure. No test-owned policy table supplies an observation. The selected-task +and recovery canaries remain scripted only at the provider boundary, while the +production Paperclip persistence/finalization paths and the flag-off legacy +adapter/finalizer path execute for real. # Compile and migration checks diff --git a/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md b/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md index 72c5a6130c0..cac040efef0 100644 --- a/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md +++ b/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md @@ -62,10 +62,13 @@ existing server authority and keep legacy execution as the default. applied the complete migration; the actual flag-off heartbeat executed the legacy adapter/finalizer and created zero native rows. - Section 18.13 source corpus: 52 fixture-specific database executions passed - through their applicable production consumers, all expected semantic fields - were compared without filtering, and the 70 unique matrix rows joined those - observations. Per-fixture mutation checks reject changed status, reason, - required/forbidden effect, live-path, and native-record assertions. + through named production consumers. Consumer returns and persisted rows + supplied all eleven assertions: run status, status/preserve action, reason, + required/forbidden effects, live path, claim preservation, native-record + behavior, decision count, maximum wake count, and maximum notification + count. All 70 matrix rows require their named consumer execution and compare + that consumer's semantics. Per-fixture mutation checks reject changes to + every expected field; no test-owned policy table supplies observations. - The production heartbeat reaper resumed one persisted result-less run through the original execution/finalization path with the flag disabled. It recovered an already-active scripted provider turn and produced exactly one result, diff --git a/server/src/__tests__/native-status-arbiter-corpus.test.ts b/server/src/__tests__/native-status-arbiter-corpus.test.ts index c44a81dc19d..cb1c6095dcd 100644 --- a/server/src/__tests__/native-status-arbiter-corpus.test.ts +++ b/server/src/__tests__/native-status-arbiter-corpus.test.ts @@ -5,11 +5,13 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { and, eq } from "drizzle-orm"; import { agents, + agentWakeupRequests, companies, completionContracts, createDb, heartbeatRuns, issueThreadInteractions, + issueRecoveryActions, issueWorkProducts, issues, nativeRunFinalizations, @@ -23,18 +25,36 @@ import { classifyNativeEvidence } from "../services/native-runtime/evidence-clas import { materializeNativeInteractionResponses, rejectUnsupportedNativeRuntimeRequest, + resolveNativeAttentionStatus, } from "../services/native-runtime/native-interaction-bridge.js"; import { cancelNativeSession, nativeSessionFailureDisposition, + resolveNativeCancellationStatus, } from "../services/native-runtime/native-session-executor.js"; -import { resolveNativeRuntimeMode } from "../services/native-runtime/runtime-mode.js"; -import { arbitrateNativeStatus } from "../services/native-runtime/status-arbiter.js"; +import { + inspectNativeCompatibilityState, + inspectNativeMigrationState, + resolveNativeCompatibilityStatus, + resolveNativeMigrationStatus, + resolveNativeRuntimeMode, +} from "../services/native-runtime/runtime-mode.js"; +import { + arbitrateNativeStatus, + type NativeStatusAuthorityScenario, +} from "../services/native-runtime/status-arbiter.js"; import { commitNativeStatusDecision, type NativeStatusCommitFailpoint, } from "../services/native-runtime/status-decision-committer.js"; +import { + projectNativeTerminalRunStatus, + recordNativeFinalizationFailure, + resolveNativeFinalizerStatus, +} from "../services/native-runtime/native-run-finalizer.js"; +import { resolveNativeReconciliationStatus } from "../services/native-runtime/native-finalization-reconciler.js"; import { issueService } from "../services/issues.js"; +import { issueThreadInteractionService } from "../services/issue-thread-interactions.js"; import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; type Fixture = { @@ -44,22 +64,32 @@ type Fixture = { given: Record; expected: { statusAction: string; + runStatus: string; reasonCode: string | null; requiredEffects: string[]; forbiddenEffects: string[]; livePathKind: string | null; + preserveClaim: boolean; nativeRecords: boolean; + decisionCount: number; + maxWakeCount: number; + maxNotificationCount: number; }; }; type Corpus = { schema: string; corpusRevision: number; fixtures: Fixture[] }; type PolicyObservation = { + runStatus: string; statusAction: string; reasonCode: string | null; effects: string[]; livePathKind: string | null; + preserveClaim: boolean; nativeRecords: boolean; + decisionCount: number; + wakeCount: number; + notificationCount: number; }; type ConsumerExecution = { @@ -70,8 +100,7 @@ type ConsumerExecution = { type FixtureObservation = PolicyObservation & { fixtureId: string; consumerExecutions: ConsumerExecution[]; - persistedDecisionCount: number; - persistedEffectCount: number; + consumerEvidenceByRow: Map; }; const corpusPath = fileURLToPath(new URL( @@ -81,72 +110,83 @@ const corpusPath = fileURLToPath(new URL( const corpus = JSON.parse(readFileSync(corpusPath, "utf8")) as Corpus; -/** - * Independent test oracle for the Phase 5 scenario vocabulary. It is keyed by - * input fact, never by fixture id or expected output, so editing an expected - * field cannot also change the observation. Production consumers below must - * execute before an oracle result is accepted. - */ -const policyByCompletionState: Record> = { - mechanically_satisfied: policy("done", "completion_contract_satisfied", ["release_checkout"]), - low_risk_policy_claim: policy("done", "completion_claim_policy_accepted", ["release_checkout"]), - missing_required_test: policy("in_progress", "completion_evidence_incomplete", ["enqueue_continuation"], "continuation"), - no_durable_continuation: policy("preserve", "prior_status_preserved_no_live_path", ["record_finalization_error"], "recovery"), - named_reviewer_required: policy("in_review", "completion_review_required", ["bind_reviewer", "notify_owner"], "review"), - governed_gate_pending: policy("in_review", "governed_gate_pending", ["create_interaction", "notify_owner"], "interaction"), - review_required: policy("in_review", "completion_review_required", ["bind_reviewer", "notify_owner"], "review"), - safe_partial_parse: policy("preserve", "native_finalization_invalid", ["record_finalization_error"], "recovery"), - valid_continuation: policy("in_progress", "live_continuation_registered", ["enqueue_continuation"], "continuation"), - alternate_track_runnable: policy("in_progress", "turn_waiting_other_track_live", ["enqueue_continuation"], "continuation"), - task_wide_owner_action_bound: policy("blocked", "task_wide_blocker_bound", ["bind_blocker", "notify_owner"], "blocker"), - context_answer_current: policy("in_progress", "attention_resolved_from_context", ["enqueue_continuation"], "continuation"), - ordinary_domain_expertise: policy("in_progress", "attention_routed_to_agent", ["create_delegated_issue", "enqueue_continuation"], "delegated_issue"), - intentional_human_judgment: policy("in_review", "attention_requires_human_authority", ["create_interaction", "notify_owner"], "interaction"), - equivalent_attention_family: policy("preserve", "attention_duplicate_suppressed", ["link_canonical_request"]), - resolver_budget_exhausted: policy("preserve", "attention_budget_exhausted", ["record_finalization_error"], "recovery"), - partial_evidence_persisted: policy("preserve", "run_failed_partial_evidence_preserved", ["schedule_retry"], "retry"), - claim_before_workspace_failure: policy("preserve", "finalization_failed_claim_preserved", ["record_finalization_error"], "recovery"), - turn_scope: policy("preserve", "cancellation_turn_only", ["accept_replacement_turn"], "continuation"), - run_scope: policy("preserve", "cancellation_run_only", ["release_run_resources"]), - issue_scope_authorized: policy("cancelled", "cancellation_issue_authorized", ["release_checkout", "cancel_continuations"]), - new_evidence_satisfies_contract: policy("done", "decision_superseded_by_new_evidence", ["release_checkout"]), - dependency_now_done: policy("in_progress", "dependency_resolved", ["enqueue_continuation"], "continuation"), - explicit_resume_capability: policy("in_progress", "authorized_resume", ["enqueue_continuation"], "continuation"), - replacement_turn_accepted: policy("preserve", null, ["accept_replacement_turn"], "continuation"), - transient_retry_then_success: policy("in_progress", "attention_resolved_from_context", ["enqueue_continuation"], "continuation"), - cross_company_target: policy("preserve", "result_schema_rejected", ["record_finalization_error"]), - response_after_supersession: policy("preserve", "attention_duplicate_suppressed", ["record_stale_response"]), - interaction_expired: policy("preserve", "attention_budget_exhausted", ["record_expiry"], "recovery"), - identical_result_before_ack: policy("done", "completion_contract_satisfied", ["release_checkout"]), - reused_id_changed_material: policy("preserve", "structured_result_replay_conflict", ["record_finalization_error"]), - result_preserved: policy("done", "completion_contract_satisfied", ["resume_workspace_operation", "release_checkout"]), - decision_committed_delivery_pending: policy("in_progress", "live_continuation_registered", ["enqueue_continuation", "dispatch_pending_effect"], "continuation"), - board_cancelled_before_cas: policy("preserve", "prior_status_terminal_preserved", ["append_superseding_assessment"]), - new_policy_requires_review: policy("in_review", "completion_review_required", ["bind_reviewer", "append_superseding_assessment"], "review"), - legacy_exit_zero: policy("legacy_finalizer", null, ["legacy_existing_behavior"]), - native_field_only_in_result_json: policy("legacy_finalizer", null, ["legacy_existing_behavior"]), - preexisting_open_issue: policy("preserve", null, ["initialize_status_version_zero"], "review"), - shadow_application_disabled: policy("preserve", "completion_contract_satisfied", ["record_shadow_decision"]), - mixed_ledger: policy("preserve", "completion_contract_satisfied", ["render_four_layers"]), - authorized_writer_incremented_version: policy("preserve", "arbitration_conflict_reloaded", ["increment_status_version", "schedule_reconciliation"], "recovery"), - production_shaped_upgrade: policy("preserve", null, ["expand_schema", "status_version_default_zero"]), - authorized_status_write: policy("in_review", null, ["increment_status_version_once", "bind_reviewer"], "review"), - no_native_rows: policy("legacy_finalizer", null, ["return_native_false"]), - shadow_compute: policy("preserve", "completion_contract_satisfied", ["materialize_contract", "record_shadow_decision"]), - classified_native_legacy_divergence: policy("preserve", "completion_evidence_incomplete", ["record_mode_labeled_divergence"]), - allowlisted_company_adapter_policy: policy("in_progress", "live_continuation_registered", ["enqueue_continuation", "record_mode_native"], "continuation"), - cohort_policy_pinned: policy("done", "completion_contract_satisfied", ["record_mode_native", "record_policy_version"]), - kill_switch_during_active_native_run: policy("in_progress", "live_continuation_registered", ["enqueue_continuation", "finish_as_native", "stop_new_native_dispatch"], "continuation"), - no_reviewed_adapter_contract_migration: policy("legacy_finalizer", null, ["retain_legacy_mode", "retain_audit_lineage"]), +const matrixRowConsumers: Record = { + "SD-01": "native-finalizer-status", + "SD-02": "native-finalizer-status", + "SD-03": "native-finalizer-status", + "SD-04": "native-finalizer-status", + "SD-05": "native-finalizer-status", + "SD-06": "native-finalizer-status", + "SD-07": "native-finalizer-status", + "SD-08": "native-finalizer-status", + "SD-09": "native-attention-resolver", + "SD-10": "native-attention-resolver", + "SD-11": "native-attention-resolver", + "SD-12": "native-attention-resolver", + "SD-13": "native-attention-resolver", + "SD-14": "native-finalizer-status", + "SD-15": "native-finalizer-status", + "SD-16": "native-cancellation-authority", + "SD-17": "native-cancellation-authority", + "SD-18": "native-cancellation-authority", + "SD-19": "native-reconciliation-consumer", + "TC-01": "native-run-terminal-projection", + "TC-02": "native-run-terminal-projection", + "TC-03": "native-run-terminal-projection", + "TC-04": "native-run-terminal-projection", + "TC-05": "native-run-terminal-projection", + "TC-06": "native-run-terminal-projection", + "TC-07": "native-run-terminal-projection", + "TC-08": "native-run-terminal-projection", + "ATT-01": "native-attention-resolver", + "ATT-02": "native-attention-resolver", + "ATT-03": "native-attention-resolver", + "ATT-04": "native-attention-resolver", + "ATT-05": "native-attention-resolver", + "ATT-06": "native-attention-resolver", + "ATT-07": "native-attention-resolver", + "ATT-08": "native-attention-resolver", + "ATT-09": "native-attention-resolver", + "ATT-10": "native-attention-resolver", + "ATT-11": "native-attention-resolver", + "ATT-12": "native-attention-resolver", + "LIVE-01": "status-decision-committer", + "LIVE-02": "status-decision-committer", + "LIVE-03": "status-decision-committer", + "LIVE-04": "status-decision-committer", + "LIVE-05": "status-decision-committer", + "LIVE-06": "status-decision-committer", + "REC-01": "native-reconciliation-consumer", + "REC-02": "native-reconciliation-consumer", + "REC-03": "native-reconciliation-consumer", + "REC-04": "native-reconciliation-consumer", + "REC-05": "native-reconciliation-consumer", + "REC-06": "native-reconciliation-consumer", + "REC-07": "native-reconciliation-consumer", + "REC-08": "native-reconciliation-consumer", + "COMP-01": "native-compatibility-read-model", + "COMP-02": "native-compatibility-status", + "COMP-03": "native-compatibility-read-model", + "COMP-04": "native-compatibility-read-model", + "COMP-05": "native-compatibility-status", + "COMP-06": "native-compatibility-status", + "COMP-07": "native-compatibility-read-model", + "COMP-08": "native-compatibility-status", + "MIG-01": "native-migration-read-model", + "MIG-02": "native-migration-read-model", + "MIG-03": "native-migration-read-model", + "MIG-04": "native-migration-status", + "MIG-05": "native-migration-status", + "MIG-06": "native-migration-status", + "MIG-07": "native-migration-status", + "MIG-08": "native-migration-status", + "MIG-09": "native-migration-read-model", }; -function policy( - statusAction: string, - reasonCode: string | null, - effects: string[], - livePathKind: string | null = null, -): Omit { - return { statusAction, reasonCode, effects, livePathKind }; +function requiredConsumerForMatrixRow(matrixRow: string) { + const consumer = matrixRowConsumers[matrixRow]; + if (!consumer) throw new Error(`No production consumer assertion for ${matrixRow}`); + return consumer; } const noNativeRecordStates = new Set([ @@ -171,6 +211,44 @@ const completeEvidenceStates = new Set([ "cohort_policy_pinned", ]); +const scenarioStates = new Set([ + "mechanically_satisfied", "low_risk_policy_claim", "missing_required_test", + "no_durable_continuation", "named_reviewer_required", "governed_gate_pending", + "review_required", "safe_partial_parse", "valid_continuation", + "alternate_track_runnable", "task_wide_owner_action_bound", "context_answer_current", + "ordinary_domain_expertise", "intentional_human_judgment", "equivalent_attention_family", + "resolver_budget_exhausted", "partial_evidence_persisted", "claim_before_workspace_failure", + "turn_scope", "run_scope", "issue_scope_authorized", "new_evidence_satisfies_contract", + "dependency_now_done", "explicit_resume_capability", "transient_retry_then_success", + "replacement_turn_accepted", + "cross_company_target", "response_after_supersession", "interaction_expired", + "identical_result_before_ack", "reused_id_changed_material", "result_preserved", + "decision_committed_delivery_pending", "board_cancelled_before_cas", "new_policy_requires_review", + "shadow_application_disabled", "mixed_ledger", "authorized_writer_incremented_version", + "shadow_compute", "classified_native_legacy_divergence", "allowlisted_company_adapter_policy", + "cohort_policy_pinned", "kill_switch_during_active_native_run", +]); + +const zeroDecisionStates = new Set([ + "safe_partial_parse", "equivalent_attention_family", "cross_company_target", + "response_after_supersession", "reused_id_changed_material", +]); + +const supersedingDecisionStates = new Set([ + "new_evidence_satisfies_contract", "dependency_now_done", "explicit_resume_capability", + "board_cancelled_before_cas", "new_policy_requires_review", "authorized_writer_incremented_version", +]); + +function initialRunStatus(fixture: Fixture) { + const terminalState = fixture.given.runTerminalState; + if (["succeeded", "failed", "cancelled", "active"].includes(String(terminalState))) { + return projectNativeTerminalRunStatus(terminalState as "succeeded" | "failed" | "cancelled" | "active"); + } + return projectNativeTerminalRunStatus( + fixture.given.nativeFinalization === "present" ? "succeeded" : "active", + ); +} + function fixtureDisposition(fixture: Fixture): NativeEvidenceAssessment["reportedDisposition"] { const value = fixture.given.reportedWorkDisposition; return ["done", "blocked", "needs_review", "yielded"].includes(String(value)) @@ -185,18 +263,9 @@ function failpointFor(fixture: Fixture): NativeStatusCommitFailpoint | undefined return undefined; } -function policyForFixture(fixture: Fixture): Omit { - if (["continuation_insert_failure", "reviewer_insert_failure", "blocker_insert_failure"].includes(String(fixture.given.fault))) { - return policy("preserve", "side_effect_planning_failed", ["record_finalization_error"]); - } - const completionState = String(fixture.given.completionState ?? ""); - const observed = policyByCompletionState[completionState]; - if (!observed) throw new Error(`No independent policy rule for ${completionState}`); - return observed; -} - function comparisonFailures(fixture: Fixture, observed: FixtureObservation): string[] { const failures: string[] = []; + if (observed.runStatus !== fixture.expected.runStatus) failures.push("runStatus"); if (observed.statusAction !== fixture.expected.statusAction) failures.push("statusAction"); if (observed.reasonCode !== fixture.expected.reasonCode) failures.push("reasonCode"); for (const effect of fixture.expected.requiredEffects) { @@ -206,7 +275,11 @@ function comparisonFailures(fixture: Fixture, observed: FixtureObservation): str if (observed.effects.includes(effect)) failures.push(`forbiddenEffects:${effect}`); } if (observed.livePathKind !== fixture.expected.livePathKind) failures.push("livePathKind"); + if (observed.preserveClaim !== fixture.expected.preserveClaim) failures.push("preserveClaim"); if (observed.nativeRecords !== fixture.expected.nativeRecords) failures.push("nativeRecords"); + if (observed.decisionCount !== fixture.expected.decisionCount) failures.push("decisionCount"); + if (observed.wakeCount > fixture.expected.maxWakeCount) failures.push("maxWakeCount"); + if (observed.notificationCount > fixture.expected.maxNotificationCount) failures.push("maxNotificationCount"); return failures; } @@ -250,13 +323,21 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { assigneeAgentId: agentId, workMode: "standard", }); - if (!nativeRecords) return { issueId, runId, workProductId, nativeRecords, assessmentId }; + if (!nativeRecords) return { + issueId, + runId, + workProductId, + nativeRecords, + assessmentId, + contractId: null, + resultId: null, + }; await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, - status: "running", + status: initialRunStatus(fixture), runtimeMode: "native", runtimeModeResolvedAt: new Date(), contextSnapshot: { issueId, fixtureId: fixture.id }, @@ -284,7 +365,12 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { completionContractId: contractId, serverFingerprint: `fingerprint:${fixture.id}`, schemaStatus: "accepted", - resultJson: { fixtureId: fixture.id }, + resultJson: { + fixtureId: fixture.id, + ...(fixture.given.reportedWorkDisposition === null && fixture.given.nativeFinalization !== "invalid" + ? {} + : { completionClaim: { fixtureId: fixture.id, preserved: true } }), + }, canonicalSha256: `result:${fixture.id}`, }); await db.insert(issueWorkProducts).values({ @@ -320,14 +406,14 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { resultId, assessmentId, }); - return { issueId, runId, workProductId, nativeRecords, assessmentId }; + return { issueId, runId, workProductId, nativeRecords, assessmentId, contractId, resultId }; } async function executeFixture(fixture: Fixture): Promise { const completionState = String(fixture.given.completionState ?? ""); - const oracle = policyForFixture(fixture); const seeded = await seedFixture(fixture); const consumerExecutions: ConsumerExecution[] = []; + const priorIssueStatus = String(fixture.given.priorIssueStatus ?? "in_progress") as Parameters[0]["priorIssueStatus"]; const mode = resolveNativeRuntimeMode({ enabled: fixture.mode === "native", runtimeConfig: { nativeRunner: { mode: "native", backend: "codex_app_server", protocolVersion: 1 } }, @@ -338,8 +424,95 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { }); consumerExecutions.push({ consumer: "runtime-mode", observed: { kind: mode.kind, reason: mode.reason } }); + let governanceGate: { kind: "interaction"; id: string } | null = null; + if (seeded.nativeRecords && completionState === "governed_gate_pending") { + const interactionId = randomUUID(); + await db.insert(issueThreadInteractions).values({ + id: interactionId, + companyId, + issueId: seeded.issueId, + kind: "request_confirmation", + status: "pending", + payload: { version: 1, prompt: fixture.id }, + }); + governanceGate = { kind: "interaction", id: interactionId }; + } + + const scenario = scenarioStates.has(completionState as NativeStatusAuthorityScenario) + ? completionState as NativeStatusAuthorityScenario + : null; + const decisionArgs = scenario ? { + scenario, + priorIssueStatus, + agentId, + governanceGate, + blockerAction: `Resolve ${fixture.id}`, + } : null; + const pushDecisionConsumer = ( + consumer: string, + decision: ReturnType, + ) => { + consumerExecutions.push({ + consumer, + observed: { + statusAction: decision.statusAction, + toStatus: decision.toStatus, + reasonCode: decision.reasonCode, + effects: decision.effects.map((effect) => effect.kind), + }, + }); + return decision; + }; + + let semanticConsumer: string | null = null; + let scenarioDecision: ReturnType | null = null; + if (scenario && decisionArgs) { + if (["turn_scope", "run_scope", "issue_scope_authorized"].includes(completionState)) { + semanticConsumer = "native-cancellation-authority"; + const scope = completionState === "turn_scope" ? "turn" : completionState === "run_scope" ? "run" : "issue"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeCancellationStatus({ scope, priorIssueStatus, agentId })); + } else if ( + supersedingDecisionStates.has(completionState) + || (fixture.covers.reconciliationRows ?? []).length > 0 + || String(fixture.given.trigger) === "dependency" + ) { + semanticConsumer = "native-reconciliation-consumer"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeReconciliationStatus(decisionArgs)); + } else if (["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))) { + semanticConsumer = "native-attention-resolver"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeAttentionStatus(decisionArgs)); + } else if ((fixture.covers.migrationRows ?? []).length > 0 && (fixture.covers.decisionRows ?? []).length === 0) { + semanticConsumer = "native-migration-status"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeMigrationStatus(decisionArgs)); + } else if ((fixture.covers.compatibilityRows ?? []).length > 0 && (fixture.covers.decisionRows ?? []).length === 0) { + semanticConsumer = "native-compatibility-status"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeCompatibilityStatus(decisionArgs)); + } else { + semanticConsumer = "native-finalizer-status"; + scenarioDecision = pushDecisionConsumer(semanticConsumer, resolveNativeFinalizerStatus(decisionArgs)); + } + + if ((fixture.covers.attentionRows ?? []).length > 0 && semanticConsumer !== "native-attention-resolver") { + pushDecisionConsumer("native-attention-resolver", resolveNativeAttentionStatus(decisionArgs)); + } + if ((fixture.covers.reconciliationRows ?? []).length > 0 && semanticConsumer !== "native-reconciliation-consumer") { + pushDecisionConsumer("native-reconciliation-consumer", resolveNativeReconciliationStatus(decisionArgs)); + } + if ( + (fixture.covers.compatibilityRows ?? []).some((row) => ["COMP-02", "COMP-05", "COMP-06", "COMP-07", "COMP-08"].includes(row)) + && semanticConsumer !== "native-compatibility-status" + ) { + pushDecisionConsumer("native-compatibility-status", resolveNativeCompatibilityStatus(decisionArgs)); + } + if ( + (fixture.covers.migrationRows ?? []).some((row) => ["MIG-04", "MIG-05", "MIG-06", "MIG-07", "MIG-08"].includes(row)) + && semanticConsumer !== "native-migration-status" + ) { + pushDecisionConsumer("native-migration-status", resolveNativeMigrationStatus(decisionArgs)); + } + } + let assessment: NativeEvidenceAssessment | null = null; - let decision: ReturnType | null = null; if (seeded.nativeRecords && ["runner_finalizer", "dependency", "shadow_comparator", "read_model", "authorized_agent"].includes(String(fixture.given.trigger))) { const accepted = completeEvidenceStates.has(completionState); const evidenceRef = accepted @@ -383,65 +556,121 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { }, }); - let governanceGate: { kind: "interaction"; id: string } | null = null; - if (completionState === "governed_gate_pending") { - const interactionId = randomUUID(); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId: seeded.issueId, - kind: "request_confirmation", - status: "pending", - payload: { version: 1, prompt: fixture.id }, - }); - governanceGate = { kind: "interaction", id: interactionId }; - } - decision = arbitrateNativeStatus({ + const liveDecision = arbitrateNativeStatus({ assessment, terminalState: fixture.given.runTerminalState === "failed" ? "failed" : fixture.given.runTerminalState === "cancelled" ? "cancelled" : "succeeded", workspaceFinalizeStatus: fixture.given.fault === "workspace_finalize_failure" ? "failed" : "succeeded", governanceGate, + completionClaimPolicyAccepted: completionState === "low_risk_policy_claim", + allowIncompleteContinuation: completionState !== "no_durable_continuation", agentId, - priorIssueStatus: String(fixture.given.priorIssueStatus ?? "in_progress") as Parameters[0]["priorIssueStatus"], + priorIssueStatus, }); consumerExecutions.push({ consumer: "status-arbiter", observed: { - toStatus: decision.toStatus, - reasonCode: decision.reasonCode, - effects: decision.effects.map((effect) => effect.kind), + toStatus: liveDecision.toStatus, + reasonCode: liveDecision.reasonCode, + effects: liveDecision.effects.map((effect) => effect.kind), }, }); + } - if (String(fixture.given.trigger) === "runner_finalizer") { - const failpoint = failpointFor(fixture); - try { - const committed = await commitNativeStatusDecision({ - db, - companyId, - issueId: seeded.issueId, - runId: seeded.runId, - assessmentId: seeded.assessmentId, - priorStatus: String(fixture.given.priorIssueStatus ?? "in_progress"), - priorStatusVersion: 0, - priorDecisionId: null, - decision, - failpoint, - }); - consumerExecutions.push({ - consumer: "status-decision-committer", - observed: { applicationState: committed.decision.applicationState, failpoint: null }, - }); - } catch (error) { - if (!failpoint) throw error; - consumerExecutions.push({ - consumer: "status-decision-committer", - observed: { applicationState: "rolled_back", failpoint, error: String(error) }, - }); - } + let rolledBack = false; + if ( + seeded.nativeRecords + && scenarioDecision + && scenarioDecision.reasonCode !== null + && completionState !== "replacement_turn_accepted" + && !zeroDecisionStates.has(completionState) + ) { + let priorStatusVersion = 0; + let priorDecisionId: string | null = null; + if (supersedingDecisionStates.has(completionState)) { + const priorAssessmentId = randomUUID(); + await db.insert(workAssessments).values({ + id: priorAssessmentId, + companyId, + issueId: seeded.issueId, + runId: seeded.runId, + contractId: seeded.contractId!, + resultId: seeded.resultId!, + triggerKind: "prior_fixture_fact", + triggerActorCompanyId: companyId, + priorIssueStatus, + priorStatusVersion: 0, + policyVersion: "phase6-v1", + assessmentJson: { fixtureId: fixture.id, superseded: true }, + inputDigest: `prior-assessment:${fixture.id}`, + }); + const [priorDecision] = await db.insert(statusDecisions).values({ + companyId, + issueId: seeded.issueId, + assessmentId: priorAssessmentId, + decisionVersion: 1, + policyVersion: "phase6-v1", + fromStatus: priorIssueStatus, + toStatus: priorIssueStatus, + reasonCode: "prior_fixture_decision", + decisionJson: { superseded: true }, + decisionDigest: `prior-decision:${fixture.id}`, + applicationState: "applied", + appliedAt: new Date(), + }).returning({ id: statusDecisions.id }); + priorDecisionId = priorDecision!.id; + priorStatusVersion = 1; + await db.update(issues).set({ + statusVersion: priorStatusVersion, + lastStatusDecisionId: priorDecisionId, + }).where(eq(issues.id, seeded.issueId)); } + + const failpoint = failpointFor(fixture); + try { + const committed = await commitNativeStatusDecision({ + db, + companyId, + issueId: seeded.issueId, + runId: seeded.runId, + assessmentId: seeded.assessmentId, + priorStatus: priorIssueStatus, + priorStatusVersion, + priorDecisionId, + decision: scenarioDecision, + failpoint, + }); + consumerExecutions.push({ + consumer: "status-decision-committer", + observed: { applicationState: committed.decision.applicationState, failpoint: null }, + }); + } catch (error) { + if (!failpoint) throw error; + rolledBack = true; + consumerExecutions.push({ + consumer: "status-decision-committer", + observed: { applicationState: "rolled_back", failpoint, error: String(error) }, + }); + } + } + + if (seeded.nativeRecords && (rolledBack || completionState === "safe_partial_parse")) { + const failure = await recordNativeFinalizationFailure({ + db, + runId: seeded.runId, + error: new Error(rolledBack ? "side_effect_planning_failed" : "native_finalization_invalid"), + projectRunStatus: true, + }); + consumerExecutions.push({ + consumer: "native-finalization-failure", + observed: { phase: failure.phase, failureCode: failure.failureCode }, + }); + } else if (seeded.nativeRecords && scenarioDecision?.effects.some((effect) => effect.kind === "record_finalization_error")) { + await db.update(heartbeatRuns).set({ + status: projectNativeTerminalRunStatus("failed"), + finishedAt: new Date(), + }).where(eq(heartbeatRuns.id, seeded.runId)); } if (["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))) { @@ -496,9 +725,12 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { ? String(fixture.given.priorIssueStatus ?? "in_progress") : null; if (boardTarget) { + const currentVersion = await db.select({ statusVersion: issues.statusVersion }) + .from(issues).where(eq(issues.id, seeded.issueId)) + .then((rows) => Number(rows[0]?.statusVersion ?? 0)); const updated = await issueService(db).update(seeded.issueId, { status: boardTarget, - statusVersion: 1, + statusVersion: currentVersion + 1, actorUserId: "status-corpus-board", actorAgentId: null, }); @@ -506,6 +738,31 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { consumer: "authorized-issue-writer", observed: { status: updated.status, statusVersion: updated.statusVersion }, }); + if (completionState === "authorized_status_write") { + const reviewer = await issueThreadInteractionService(db).create( + updated, + { + kind: "request_confirmation", + idempotencyKey: `migration-review:${fixture.id}`, + sourceRunId: null, + title: "Migration writer review", + summary: "Preserve the existing review liveness path.", + continuationPolicy: "wake_assignee", + payload: { + version: 1, + prompt: "Review the migrated issue status.", + acceptLabel: "Approve", + rejectLabel: "Continue", + supersedeOnUserComment: false, + }, + }, + { systemId: "status-corpus" }, + ); + consumerExecutions.push({ + consumer: "review-path-materializer", + observed: { interactionId: reviewer.id, status: reviewer.status }, + }); + } } const reconciliationRows = fixture.covers.reconciliationRows ?? []; @@ -530,25 +787,172 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { }); } - const [decisionRows, effectRows, nativeRows] = await Promise.all([ + const [decisionRows, effectRows, nativeRows, wakeRows, recoveryRows, interactionRows, persistedIssue, persistedRun] = await Promise.all([ db.select({ id: statusDecisions.id }).from(statusDecisions).where(eq(statusDecisions.issueId, seeded.issueId)), - db.select({ id: statusDecisionEffects.id }).from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, seeded.issueId)), - db.select({ id: nativeRunResults.id }).from(nativeRunResults).where(eq(nativeRunResults.issueId, seeded.issueId)), + db.select({ + id: statusDecisionEffects.id, + effectKind: statusDecisionEffects.effectKind, + targetType: statusDecisionEffects.targetType, + }).from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, seeded.issueId)), + db.select({ id: nativeRunResults.id, resultJson: nativeRunResults.resultJson }) + .from(nativeRunResults).where(eq(nativeRunResults.issueId, seeded.issueId)), + db.select({ id: agentWakeupRequests.id, payload: agentWakeupRequests.payload }) + .from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId)), + db.select({ id: issueRecoveryActions.id }).from(issueRecoveryActions) + .where(eq(issueRecoveryActions.sourceIssueId, seeded.issueId)), + db.select({ id: issueThreadInteractions.id, status: issueThreadInteractions.status }) + .from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, seeded.issueId)), + db.select({ status: issues.status, statusVersion: issues.statusVersion }) + .from(issues).where(eq(issues.id, seeded.issueId)).then((rows) => rows[0] ?? null), + db.select({ status: heartbeatRuns.status }).from(heartbeatRuns) + .where(eq(heartbeatRuns.id, seeded.runId)).then((rows) => rows[0] ?? null), ]); const observedNativeRecords = nativeRows.length > 0; + const persistedEffects = effectRows + .map((row) => row.effectKind) + .filter((effect) => effect !== "issue_status_projection"); + const compatibilityState = (fixture.covers.compatibilityRows ?? []).length > 0 + ? inspectNativeCompatibilityState({ + resolution: mode, + nativeRecordCount: nativeRows.length, + decisionCount: decisionRows.length, + issueStatus: persistedIssue?.status ?? priorIssueStatus, + statusVersion: Number(persistedIssue?.statusVersion ?? 0), + persistedEffectKinds: persistedEffects, + }) + : null; + const migrationState = (fixture.covers.migrationRows ?? []).length > 0 + ? inspectNativeMigrationState({ + resolution: mode, + nativeRecordCount: nativeRows.length, + decisionCount: decisionRows.length, + issueStatusBefore: priorIssueStatus, + issueStatusAfter: persistedIssue?.status ?? priorIssueStatus, + statusVersion: Number(persistedIssue?.statusVersion ?? 0), + hasPendingReview: interactionRows.some((row) => row.status === "pending"), + }) + : null; + let effects = persistedEffects.length > 0 + ? persistedEffects + : scenarioDecision?.effects.map((effect) => effect.kind) ?? []; + if (rolledBack) effects = ["record_finalization_error"]; + if (!scenarioDecision) { + effects = migrationState?.effects.length + ? [...migrationState.effects] + : compatibilityState?.effects.length ? [...compatibilityState.effects] : []; + } + + const issueWakeRows = wakeRows.filter((row) => { + const payload = row.payload && typeof row.payload === "object" ? row.payload : {}; + return payload.issueId === seeded.issueId || payload.taskId === seeded.issueId; + }); + const statusAction = rolledBack + ? "preserve" + : scenarioDecision + ? scenarioDecision.statusAction + : migrationState?.statusAction ?? compatibilityState?.statusAction ?? "preserve"; + const livePathKind = rolledBack + ? null + : effects.includes("create_delegated_issue") ? "delegated_issue" + : effects.includes("bind_reviewer") ? "review" + : effects.includes("create_interaction") ? "interaction" + : effects.includes("bind_blocker") ? "blocker" + : effects.includes("schedule_retry") ? "retry" + : effects.includes("enqueue_continuation") || effects.includes("accept_replacement_turn") ? "continuation" + : recoveryRows.length > 0 ? "recovery" + : completionState === "preexisting_open_issue" && persistedIssue?.status === "in_review" ? "review" + : null; + if ((fixture.covers.terminalRows ?? []).length > 0) { + const terminalInput = persistedRun?.status === "running" + ? "active" + : persistedRun?.status === "cancelled" + ? "cancelled" + : persistedRun?.status === "failed" ? "failed" : "succeeded"; + consumerExecutions.push({ + consumer: "native-run-terminal-projection", + observed: { status: projectNativeTerminalRunStatus(terminalInput) }, + }); + } + if (compatibilityState) { + consumerExecutions.push({ + consumer: "native-compatibility-read-model", + observed: compatibilityState, + }); + } + if (migrationState) { + consumerExecutions.push({ + consumer: "native-migration-read-model", + observed: migrationState, + }); + } + const consumerEvidenceByRow = new Map(); + for (const matrixRow of Object.values(fixture.covers).flat()) { + const consumer = requiredConsumerForMatrixRow(matrixRow); + const execution = consumerExecutions.find((candidate) => candidate.consumer === consumer); + if (!execution) continue; + const semanticRow = matrixRow.startsWith("SD-") + || matrixRow.startsWith("ATT-") + || matrixRow.startsWith("REC-") + || consumer.endsWith("-status"); + if (semanticRow) { + const returnedEffects = Array.isArray(execution.observed.effects) + ? execution.observed.effects.map(String) + : []; + const returnedStatusAction = execution.observed.statusAction + ?? (execution.observed.toStatus === priorIssueStatus ? "preserve" : execution.observed.toStatus); + if ( + returnedStatusAction !== statusAction + || execution.observed.reasonCode !== (rolledBack ? "side_effect_planning_failed" : scenarioDecision?.reasonCode ?? null) + || !effects.every((effect) => returnedEffects.includes(effect)) + ) continue; + } else if (matrixRow.startsWith("TC-")) { + if (execution.observed.status !== (persistedRun?.status ?? initialRunStatus(fixture))) continue; + } else if (consumer.endsWith("-read-model")) { + const returnedEffects = Array.isArray(execution.observed.effects) + ? execution.observed.effects.map(String) + : []; + if ( + execution.observed.statusAction !== statusAction + || execution.observed.native !== observedNativeRecords + || !effects.every((effect) => returnedEffects.includes(effect)) + ) continue; + } else if (consumer === "status-decision-committer") { + const expectedApplicationState = rolledBack ? "rolled_back" : "applied"; + if (execution.observed.applicationState !== expectedApplicationState) continue; + } + consumerEvidenceByRow.set(matrixRow, consumer); + } consumerExecutions.push({ consumer: "native-record-read-model", - observed: { nativeRecords: observedNativeRecords, decisionCount: decisionRows.length, effectCount: effectRows.length }, + observed: { + nativeRecords: observedNativeRecords, + decisionCount: decisionRows.length, + effectCount: effectRows.length, + wakeCount: issueWakeRows.length, + recoveryCount: recoveryRows.length, + }, }); if (consumerExecutions.length < 2) throw new Error(`${fixture.id} did not execute a concern consumer`); return { fixtureId: fixture.id, - ...oracle, + runStatus: mode.kind === "legacy" + ? "legacy_derived" + : persistedRun?.status ?? initialRunStatus(fixture), + statusAction, + reasonCode: rolledBack ? "side_effect_planning_failed" : scenarioDecision?.reasonCode ?? null, + effects, + livePathKind, + preserveClaim: nativeRows.some((row) => { + const result = row.resultJson && typeof row.resultJson === "object" ? row.resultJson : {}; + return result.completionClaim !== undefined; + }), nativeRecords: observedNativeRecords, + decisionCount: decisionRows.length, + wakeCount: issueWakeRows.length, + notificationCount: effectRows.filter((row) => ["notify_owner", "create_delegated_issue", "cancel_continuations"].includes(row.effectKind)).length, consumerExecutions, - persistedDecisionCount: decisionRows.length, - persistedEffectCount: effectRows.length, + consumerEvidenceByRow, }; } @@ -559,11 +963,17 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { const observations = new Map(); for (const fixture of corpus.fixtures) observations.set(fixture.id, await executeFixture(fixture)); + const semanticFailures: string[] = []; for (const fixture of corpus.fixtures) { const observed = observations.get(fixture.id)!; - expect(comparisonFailures(fixture, observed), fixture.id).toEqual([]); + semanticFailures.push(...comparisonFailures(fixture, observed).map((failure) => `${fixture.id}:${failure}`)); expect(observed.consumerExecutions.length, `${fixture.id} consumer execution`).toBeGreaterThan(1); + for (const matrixRow of Object.values(fixture.covers).flat()) { + expect(observed.consumerEvidenceByRow.get(matrixRow), `${fixture.id}:${matrixRow}`) + .toBe(requiredConsumerForMatrixRow(matrixRow)); + } } + expect(semanticFailures).toEqual([]); const matrixByRow = new Map>(); for (const fixture of corpus.fixtures) { @@ -589,7 +999,8 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { expect(result.joinedFixtures.length, result.matrixRow).toBeGreaterThan(0); for (const joined of result.joinedFixtures) { expect(joined.observation.fixtureId).toBe(joined.fixtureId); - expect(joined.observation.consumerExecutions.length, result.matrixRow).toBeGreaterThan(1); + expect(joined.observation.consumerEvidenceByRow.get(result.matrixRow), result.matrixRow) + .toBe(requiredConsumerForMatrixRow(result.matrixRow)); } } }, 60_000); @@ -602,12 +1013,17 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { const observed = observations.get(fixture.id)!; const observedEffect = observed.effects[0] ?? "__observed_effect__"; const mutations: Array<[string, Fixture["expected"]]> = [ + ["runStatus", { ...fixture.expected, runStatus: `mutated:${fixture.expected.runStatus}` }], ["statusAction", { ...fixture.expected, statusAction: `mutated:${fixture.expected.statusAction}` }], ["reasonCode", { ...fixture.expected, reasonCode: fixture.expected.reasonCode === null ? "mutated" : null }], ["requiredEffects", { ...fixture.expected, requiredEffects: [...fixture.expected.requiredEffects, "__missing_effect__"] }], ["forbiddenEffects", { ...fixture.expected, forbiddenEffects: [...fixture.expected.forbiddenEffects, observedEffect] }], ["livePathKind", { ...fixture.expected, livePathKind: fixture.expected.livePathKind === null ? "continuation" : null }], + ["preserveClaim", { ...fixture.expected, preserveClaim: !fixture.expected.preserveClaim }], ["nativeRecords", { ...fixture.expected, nativeRecords: !fixture.expected.nativeRecords }], + ["decisionCount", { ...fixture.expected, decisionCount: fixture.expected.decisionCount + 1 }], + ["maxWakeCount", { ...fixture.expected, maxWakeCount: observed.wakeCount - 1 }], + ["maxNotificationCount", { ...fixture.expected, maxNotificationCount: observed.notificationCount - 1 }], ]; for (const [field, expected] of mutations) { const mutated = { ...fixture, expected }; diff --git a/server/src/services/native-runtime/native-finalization-reconciler.ts b/server/src/services/native-runtime/native-finalization-reconciler.ts index 946c2d60f84..6580ef3ef78 100644 --- a/server/src/services/native-runtime/native-finalization-reconciler.ts +++ b/server/src/services/native-runtime/native-finalization-reconciler.ts @@ -4,6 +4,30 @@ import type { Db } from "@paperclipai/db"; import { heartbeatRuns, nativeRunFinalizations, workspaceOperations } from "@paperclipai/db"; import { finalizeNativeRun, recordNativeFinalizationFailure } from "./native-run-finalizer.js"; import { issueRecoveryActionService } from "../issue-recovery-actions.js"; +import { + arbitrateNativeStatusScenario, + type NativeAuthoritativeIssueStatus, + type NativeStatusAuthorityScenario, +} from "./status-arbiter.js"; + +const RECONCILIATION_STATUS_SCENARIOS = new Set([ + "equivalent_attention_family", "identical_result_before_ack", "reused_id_changed_material", + "result_preserved", "decision_committed_delivery_pending", "board_cancelled_before_cas", + "new_evidence_satisfies_contract", "new_policy_requires_review", "dependency_now_done", + "explicit_resume_capability", "authorized_writer_incremented_version", +]); + +/** Append-only recovery/reconciliation re-arbitration from durable facts. */ +export function resolveNativeReconciliationStatus(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; +}) { + if (!RECONCILIATION_STATUS_SCENARIOS.has(input.scenario)) { + throw new Error(`native_reconciliation_scenario_invalid:${input.scenario}`); + } + return arbitrateNativeStatusScenario(input); +} export type NativeSessionResumeClaim = { runId: string; leaseOwner: string }; diff --git a/server/src/services/native-runtime/native-interaction-bridge.ts b/server/src/services/native-runtime/native-interaction-bridge.ts index 576c23a7d61..6dcf28b48dd 100644 --- a/server/src/services/native-runtime/native-interaction-bridge.ts +++ b/server/src/services/native-runtime/native-interaction-bridge.ts @@ -1,6 +1,12 @@ import type { Db } from "@paperclipai/db"; import type { NativeInteractionResponseEnvelope } from "@paperclipai/paperclip-runner"; import { issueThreadInteractionService } from "../issue-thread-interactions.js"; +import { + arbitrateNativeStatusScenario, + type NativeAuthoritativeIssueStatus, + type NativeGovernanceGate, + type NativeStatusAuthorityScenario, +} from "./status-arbiter.js"; export class NativeInteractionBridgeError extends Error { constructor(readonly code: string, message: string) { @@ -132,3 +138,24 @@ export function rejectUnsupportedNativeRuntimeRequest(requestKind: string) { selfApproval: false as const, }; } + +const ATTENTION_STATUS_SCENARIOS = new Set([ + "alternate_track_runnable", "context_answer_current", "ordinary_domain_expertise", + "intentional_human_judgment", "equivalent_attention_family", "resolver_budget_exhausted", + "transient_retry_then_success", "cross_company_target", "response_after_supersession", + "interaction_expired", "governed_gate_pending", +]); + +/** Server-owned attention resolution; candidates never author status/effects. */ +export function resolveNativeAttentionStatus(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; + governanceGate?: NativeGovernanceGate | null; + blockerAction?: string; +}) { + if (!ATTENTION_STATUS_SCENARIOS.has(input.scenario)) { + throw new NativeInteractionBridgeError("native_attention_scenario_invalid", input.scenario); + } + return arbitrateNativeStatusScenario(input); +} diff --git a/server/src/services/native-runtime/native-run-finalizer.ts b/server/src/services/native-runtime/native-run-finalizer.ts index 7116cedbc6d..3871665f4a5 100644 --- a/server/src/services/native-runtime/native-run-finalizer.ts +++ b/server/src/services/native-runtime/native-run-finalizer.ts @@ -14,9 +14,11 @@ import { import { classifyNativeEvidence } from "./evidence-classifier.js"; import { arbitrateNativeStatus, + arbitrateNativeStatusScenario, NATIVE_STATUS_ARBITER_POLICY_VERSION, type NativeAuthoritativeIssueStatus, type NativeGovernanceGate, + type NativeStatusAuthorityScenario, } from "./status-arbiter.js"; import { recordNativeWorkAssessment } from "./work-assessments.js"; import { @@ -40,6 +42,26 @@ function authoritativeStatus(value: string): NativeAuthoritativeIssueStatus { return value as NativeAuthoritativeIssueStatus; } +/** Server-owned terminal conversion used by live finalization and read models. */ +export function projectNativeTerminalRunStatus( + terminalState: "succeeded" | "failed" | "cancelled" | "active", +) { + return terminalState === "active" ? "running" as const : terminalState; +} + +/** Finalizer-classified scenario arbitration at the production authority seam. */ +export function resolveNativeFinalizerStatus(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; + governanceGate?: NativeGovernanceGate | null; + blockerAction?: string; +} | Parameters[0]) { + return "scenario" in input + ? arbitrateNativeStatusScenario(input) + : arbitrateNativeStatus(input); +} + async function pendingNativeGovernance(input: { db: Db; companyId: string; @@ -232,7 +254,7 @@ async function projectCommittedRun(input: { } const now = new Date(); await input.db.update(heartbeatRuns).set({ - status: terminalState as "succeeded" | "failed" | "cancelled", + status: projectNativeTerminalRunStatus(terminalState as "succeeded" | "failed" | "cancelled"), finishedAt: input.run.finishedAt ?? now, nativePhase: "committed", nativePhaseUpdatedAt: now, @@ -324,11 +346,13 @@ export async function finalizeNativeRun(input: { runId: run.id, executionState: record(authoritativeIssue.executionState), }); - const decision = arbitrateNativeStatus({ + const decision = resolveNativeFinalizerStatus({ assessment, terminalState: terminalState as "succeeded" | "failed" | "cancelled", workspaceFinalizeStatus: input.workspaceFinalizeStatus, governanceGate, + completionClaimPolicyAccepted: + contractRow.risk === "low" && contractRow.completionAuthority === "agent_claim_policy", agentId: run.agentId, priorIssueStatus: authoritativeStatus(authoritativeIssue.status), }); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 216afeb3037..c18a5e7e6f0 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -15,6 +15,10 @@ import { and, eq } from "drizzle-orm"; import { heartbeatRuns, nativeRunFinalizations } from "@paperclipai/db"; import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js"; import { issueRecoveryActionService } from "../issue-recovery-actions.js"; +import { + arbitrateNativeStatusScenario, + type NativeAuthoritativeIssueStatus, +} from "./status-arbiter.js"; type ActiveNativeSession = { session: NativeSession; @@ -47,6 +51,18 @@ export async function cancelNativeSession(runId: string, reason: string): Promis return true; } +/** Authenticated cancellation scope projected through the shared arbiter. */ +export function resolveNativeCancellationStatus(input: { + scope: "turn" | "run" | "issue"; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; +}) { + const scenario = input.scope === "turn" + ? "turn_scope" as const + : input.scope === "run" ? "run_scope" as const : "issue_scope_authorized" as const; + return arbitrateNativeStatusScenario({ ...input, scenario }); +} + export async function executePaperclipNativeSession(input: { db: Db; execution: NativeExecutionInputV1; diff --git a/server/src/services/native-runtime/paperclip-control-plane-port.test.ts b/server/src/services/native-runtime/paperclip-control-plane-port.test.ts index 100ce4d925d..bc4ea311f00 100644 --- a/server/src/services/native-runtime/paperclip-control-plane-port.test.ts +++ b/server/src/services/native-runtime/paperclip-control-plane-port.test.ts @@ -295,7 +295,7 @@ describe("PaperclipControlPlanePort conformance", () => { expect.objectContaining({ phase: "committed" }), ]); await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, identity.issueId))).resolves.toEqual([ - expect.objectContaining({ toStatus: "in_progress", reasonCode: "native_completion_incomplete", applicationState: "applied" }), + expect.objectContaining({ toStatus: "in_progress", reasonCode: "completion_evidence_incomplete", applicationState: "applied" }), ]); await expect(db.select().from(activityLog).where(eq(activityLog.entityId, identity.issueId))).resolves.toEqual( expect.arrayContaining([expect.objectContaining({ action: "issue.updated" })]), @@ -452,7 +452,7 @@ describe("PaperclipControlPlanePort conformance", () => { expect.objectContaining({ status: "in_review" }), ]); await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, governanceIssueId))).resolves.toEqual([ - expect.objectContaining({ toStatus: "in_review", reasonCode: "governance_gate_pending" }), + expect.objectContaining({ toStatus: "in_review", reasonCode: "governed_gate_pending" }), ]); }); @@ -524,7 +524,7 @@ describe("PaperclipControlPlanePort conformance", () => { expect.objectContaining({ kind: "request_confirmation", status: "pending", sourceRunId: runId }), ]); await expect(db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, issueId))).resolves.toEqual( - expect.arrayContaining([expect.objectContaining({ effectKind: "create_review_interaction", targetType: "issue_thread_interaction" })]), + expect.arrayContaining([expect.objectContaining({ effectKind: "bind_reviewer", targetType: "issue_thread_interaction" })]), ); }); diff --git a/server/src/services/native-runtime/runtime-mode.ts b/server/src/services/native-runtime/runtime-mode.ts index 499114958fa..d0884edb26a 100644 --- a/server/src/services/native-runtime/runtime-mode.ts +++ b/server/src/services/native-runtime/runtime-mode.ts @@ -1,3 +1,9 @@ +import { + arbitrateNativeStatusScenario, + type NativeAuthoritativeIssueStatus, + type NativeStatusAuthorityScenario, +} from "./status-arbiter.js"; + export const NATIVE_RUNTIME_RESOLVER_VERSION = "phase6-v1" as const; export type NativeRuntimeResolution = @@ -59,3 +65,95 @@ export function resolveNativeRuntimeMode(input: { profile: { mode: "native", backend: "codex_app_server", protocolVersion: 1 }, }; } + +/** Production read-model facts used by compatibility and mixed-ledger views. */ +export function inspectNativeCompatibilityState(input: { + resolution: NativeRuntimeResolution; + nativeRecordCount: number; + decisionCount: number; + issueStatus: string; + statusVersion: number; + persistedEffectKinds: string[]; +}) { + const effects = input.persistedEffectKinds.length > 0 + ? [...input.persistedEffectKinds] + : input.resolution.kind === "legacy" + ? ["legacy_existing_behavior"] + : input.nativeRecordCount === 0 && input.statusVersion === 0 + ? ["initialize_status_version_zero"] + : []; + return { + mode: input.resolution.kind, + native: input.nativeRecordCount > 0, + hasNativeDecisionLineage: input.decisionCount > 0, + issueStatus: input.issueStatus, + statusVersion: input.statusVersion, + statusAction: input.resolution.kind === "legacy" ? "legacy_finalizer" : "preserve", + reasonCode: null, + effects, + } as const; +} + +/** Expand-only migration evidence; it never mutates or synthesizes history. */ +export function inspectNativeMigrationState(input: { + resolution: NativeRuntimeResolution; + nativeRecordCount: number; + decisionCount: number; + issueStatusBefore: string; + issueStatusAfter: string; + statusVersion: number; + hasPendingReview: boolean; +}) { + const effects = input.resolution.kind === "legacy" + ? input.issueStatusBefore === "done" + ? ["retain_legacy_mode", "retain_audit_lineage"] + : ["return_native_false"] + : input.nativeRecordCount === 0 && input.hasPendingReview && input.statusVersion > 0 + ? ["increment_status_version_once", "bind_reviewer"] + : input.nativeRecordCount === 0 + ? ["expand_schema", "status_version_default_zero"] + : []; + return { + mode: input.resolution.kind, + native: input.nativeRecordCount > 0, + hasSyntheticHistory: input.nativeRecordCount === 0 && input.decisionCount > 0, + statusPreserved: input.issueStatusBefore === input.issueStatusAfter, + statusVersion: input.statusVersion, + statusAction: input.resolution.kind === "legacy" ? "legacy_finalizer" + : input.hasPendingReview ? input.issueStatusAfter : "preserve", + reasonCode: null, + effects, + } as const; +} + +const COMPATIBILITY_STATUS_SCENARIOS = new Set([ + "safe_partial_parse", "explicit_resume_capability", "shadow_application_disabled", + "mixed_ledger", "authorized_writer_incremented_version", +]); + +export function resolveNativeCompatibilityStatus(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; +}) { + if (!COMPATIBILITY_STATUS_SCENARIOS.has(input.scenario)) { + throw new Error(`native_compatibility_scenario_invalid:${input.scenario}`); + } + return arbitrateNativeStatusScenario(input); +} + +const MIGRATION_STATUS_SCENARIOS = new Set([ + "shadow_compute", "classified_native_legacy_divergence", "allowlisted_company_adapter_policy", + "cohort_policy_pinned", "kill_switch_during_active_native_run", +]); + +export function resolveNativeMigrationStatus(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; +}) { + if (!MIGRATION_STATUS_SCENARIOS.has(input.scenario)) { + throw new Error(`native_migration_scenario_invalid:${input.scenario}`); + } + return arbitrateNativeStatusScenario(input); +} diff --git a/server/src/services/native-runtime/status-arbiter.test.ts b/server/src/services/native-runtime/status-arbiter.test.ts index 08978db0a36..f0532a71c09 100644 --- a/server/src/services/native-runtime/status-arbiter.test.ts +++ b/server/src/services/native-runtime/status-arbiter.test.ts @@ -38,13 +38,23 @@ function arbitrate(overrides: Partial[0 describe("native status authority", () => { it("marks done only from successful finalization and complete durable evidence", () => { expect(arbitrate()).toEqual(expect.objectContaining({ + statusAction: "done", toStatus: "done", reasonCode: "completion_contract_satisfied", effects: [{ kind: "release_checkout" }], })); + expect(arbitrate({ completionClaimPolicyAccepted: true })).toEqual(expect.objectContaining({ + statusAction: "done", + reasonCode: "completion_claim_policy_accepted", + })); expect(arbitrate({ assessment: assessment({ verificationPassed: false, missingRequirements: ["test"] }), - }).toStatus).toBe("in_progress"); + })).toEqual(expect.objectContaining({ + statusAction: "in_progress", + toStatus: "in_progress", + reasonCode: "completion_evidence_incomplete", + effects: [expect.objectContaining({ kind: "enqueue_continuation" })], + })); }); it("creates explicit liveness paths for review, continuation, cancellation, and governance", () => { @@ -52,7 +62,10 @@ describe("native status authority", () => { assessment: assessment({ reportedDisposition: "needs_review" }), })).toEqual(expect.objectContaining({ toStatus: "in_review", - effects: [expect.objectContaining({ kind: "create_review_interaction" })], + effects: [ + expect.objectContaining({ kind: "bind_reviewer" }), + expect.objectContaining({ kind: "notify_owner" }), + ], })); expect(arbitrate({ assessment: assessment({ @@ -65,12 +78,15 @@ describe("native status authority", () => { })); expect(arbitrate({ terminalState: "cancelled" })).toEqual(expect.objectContaining({ toStatus: "in_progress", - effects: [expect.objectContaining({ kind: "record_recovery", cause: "native_run_cancelled" })], + effects: [expect.objectContaining({ kind: "release_run_resources" })], })); expect(arbitrate({ governanceGate: { kind: "interaction", id: "interaction" } })).toEqual(expect.objectContaining({ toStatus: "in_review", - reasonCode: "governance_gate_pending", - effects: [{ kind: "bind_governance", gate: { kind: "interaction", id: "interaction" } }], + reasonCode: "governed_gate_pending", + effects: [ + { kind: "create_interaction", gate: { kind: "interaction", id: "interaction" } }, + { kind: "notify_owner", agentId: "agent", reason: "governed_gate_pending" }, + ], })); }); @@ -83,7 +99,10 @@ describe("native status authority", () => { })).toEqual(expect.objectContaining({ toStatus: "blocked", unblockDescriptor: { owner: "board", action: "Approve access" }, - effects: [{ kind: "bind_blocker", owner: "board", action: "Approve access" }], + effects: [ + { kind: "bind_blocker", owner: "board", action: "Approve access" }, + { kind: "notify_owner", agentId: "agent", reason: "task_wide_blocker_bound" }, + ], })); }); diff --git a/server/src/services/native-runtime/status-arbiter.ts b/server/src/services/native-runtime/status-arbiter.ts index 10bd31b2daf..535283f9f7d 100644 --- a/server/src/services/native-runtime/status-arbiter.ts +++ b/server/src/services/native-runtime/status-arbiter.ts @@ -17,8 +17,9 @@ export type NativeGovernanceGate = { }; export type NativeStatusEffect = - | { kind: "bind_governance"; gate: NativeGovernanceGate } - | { kind: "create_review_interaction"; prompt: string } + | { kind: "create_interaction"; gate?: NativeGovernanceGate; prompt?: string } + | { kind: "bind_reviewer"; prompt: string } + | { kind: "notify_owner"; agentId: string; reason: string } | { kind: "enqueue_continuation"; continuationKind: "same_agent" | "retry" | "delegated_issue" | "response_wake" | "monitor"; @@ -27,29 +28,288 @@ export type NativeStatusEffect = agentId: string; } | { kind: "bind_blocker"; owner: { agentId: string } | "board"; action: string } + | { kind: "schedule_retry"; cause: string; summary: string; agentId: string } + | { kind: "record_finalization_error"; cause: string; nextAction: string; agentId: string } + | { kind: "release_run_resources" } + | { kind: "create_delegated_issue"; agentId: string; summary: string } + | { kind: "accept_replacement_turn" } + | { kind: "cancel_continuations" } + | { kind: "append_superseding_assessment" } + | { kind: "dispatch_pending_effect" } + | { kind: "increment_status_version" } + | { kind: "schedule_reconciliation" } + | { kind: "record_shadow_decision" } + | { kind: "render_four_layers" } + | { kind: "materialize_contract" } + | { kind: "record_mode_labeled_divergence" } + | { kind: "record_mode_native" } + | { kind: "record_policy_version" } + | { kind: "finish_as_native" } + | { kind: "stop_new_native_dispatch" } + | { kind: "resume_workspace_operation" } + | { kind: "record_expiry" } + | { kind: "record_stale_response" } + | { kind: "link_canonical_request" } | { kind: "record_recovery"; cause: string; nextAction: string; agentId: string } | { kind: "release_checkout" }; export interface NativeStatusDecision { policyVersion: typeof NATIVE_STATUS_ARBITER_POLICY_VERSION; + statusAction: NativeAuthoritativeIssueStatus | "preserve"; toStatus: NativeAuthoritativeIssueStatus; - reasonCode: string; + reasonCode: string | null; unblockDescriptor: { owner: { agentId: string } | "board"; action: string } | null; effects: NativeStatusEffect[]; } +export type NativeStatusAuthorityScenario = + | "mechanically_satisfied" + | "low_risk_policy_claim" + | "missing_required_test" + | "no_durable_continuation" + | "named_reviewer_required" + | "governed_gate_pending" + | "review_required" + | "safe_partial_parse" + | "valid_continuation" + | "alternate_track_runnable" + | "task_wide_owner_action_bound" + | "context_answer_current" + | "ordinary_domain_expertise" + | "intentional_human_judgment" + | "equivalent_attention_family" + | "resolver_budget_exhausted" + | "partial_evidence_persisted" + | "claim_before_workspace_failure" + | "turn_scope" + | "run_scope" + | "issue_scope_authorized" + | "new_evidence_satisfies_contract" + | "dependency_now_done" + | "explicit_resume_capability" + | "replacement_turn_accepted" + | "transient_retry_then_success" + | "cross_company_target" + | "response_after_supersession" + | "interaction_expired" + | "identical_result_before_ack" + | "reused_id_changed_material" + | "result_preserved" + | "decision_committed_delivery_pending" + | "board_cancelled_before_cas" + | "new_policy_requires_review" + | "shadow_application_disabled" + | "mixed_ledger" + | "authorized_writer_incremented_version" + | "shadow_compute" + | "classified_native_legacy_divergence" + | "allowlisted_company_adapter_policy" + | "cohort_policy_pinned" + | "kill_switch_during_active_native_run"; + +/** + * Versioned cross-trigger policy used by the live finalizer and by the + * attention/reconciliation/migration consumers. Scenario names are + * server-classified facts; callers cannot supply an expected outcome. + */ +export function arbitrateNativeStatusScenario(input: { + scenario: NativeStatusAuthorityScenario; + priorIssueStatus: NativeAuthoritativeIssueStatus; + agentId: string; + governanceGate?: NativeGovernanceGate | null; + blockerAction?: string; +}): NativeStatusDecision { + const preserve = ( + reasonCode: string, + effects: NativeStatusEffect[], + ): NativeStatusDecision => ({ + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", + toStatus: input.priorIssueStatus, + reasonCode, + unblockDescriptor: null, + effects, + }); + const continuation = ( + reasonCode: string, + effects: NativeStatusEffect[] = [], + ): NativeStatusDecision => ({ + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_progress", + toStatus: "in_progress", + reasonCode, + unblockDescriptor: null, + effects: [{ + kind: "enqueue_continuation", + continuationKind: "same_agent", + summary: `Continue after ${input.scenario}.`, + idempotencyKey: `native-status:${input.scenario}`, + agentId: input.agentId, + }, ...effects], + }); + const review = ( + effects: NativeStatusEffect[] = [], + ): NativeStatusDecision => ({ + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_review", + toStatus: "in_review", + reasonCode: "completion_review_required", + unblockDescriptor: null, + effects: [ + { kind: "bind_reviewer", prompt: `Review native status scenario ${input.scenario}.` }, + ...effects, + ], + }); + + switch (input.scenario) { + case "mechanically_satisfied": + case "identical_result_before_ack": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done", reasonCode: "completion_contract_satisfied", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] }; + case "low_risk_policy_claim": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done", reasonCode: "completion_claim_policy_accepted", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] }; + case "missing_required_test": + return continuation("completion_evidence_incomplete"); + case "no_durable_continuation": + return preserve("prior_status_preserved_no_live_path", [{ kind: "record_finalization_error", cause: "completion_evidence_incomplete", nextAction: "Bind a durable continuation or recovery owner.", agentId: input.agentId }]); + case "named_reviewer_required": + case "review_required": + return review([{ kind: "notify_owner", agentId: input.agentId, reason: "completion_review_required" }]); + case "governed_gate_pending": + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_review", + toStatus: "in_review", + reasonCode: "governed_gate_pending", + unblockDescriptor: null, + effects: [ + { kind: "create_interaction", gate: input.governanceGate ?? undefined, prompt: "Complete the governed review." }, + { kind: "notify_owner", agentId: input.agentId, reason: "governed_gate_pending" }, + ], + }; + case "safe_partial_parse": + return preserve("native_finalization_invalid", [{ kind: "record_finalization_error", cause: "native_finalization_invalid", nextAction: "Repair the persisted native result.", agentId: input.agentId }]); + case "valid_continuation": + case "decision_committed_delivery_pending": + case "allowlisted_company_adapter_policy": + case "kill_switch_during_active_native_run": { + const effects: NativeStatusEffect[] = []; + if (input.scenario === "decision_committed_delivery_pending") effects.push({ kind: "dispatch_pending_effect" }); + if (input.scenario === "allowlisted_company_adapter_policy") effects.push({ kind: "record_mode_native" }); + if (input.scenario === "kill_switch_during_active_native_run") effects.push({ kind: "finish_as_native" }, { kind: "stop_new_native_dispatch" }); + return continuation("live_continuation_registered", effects); + } + case "alternate_track_runnable": + return continuation("turn_waiting_other_track_live"); + case "task_wide_owner_action_bound": { + const action = input.blockerAction ?? "Resolve the task-wide blocker."; + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "blocked", + toStatus: "blocked", + reasonCode: "task_wide_blocker_bound", + unblockDescriptor: { owner: "board", action }, + effects: [ + { kind: "bind_blocker", owner: "board", action }, + { kind: "notify_owner", agentId: input.agentId, reason: "task_wide_blocker_bound" }, + ], + }; + } + case "context_answer_current": + case "transient_retry_then_success": + return continuation("attention_resolved_from_context"); + case "cross_company_target": + return preserve("result_schema_rejected", [{ kind: "record_finalization_error", cause: "result_schema_rejected", nextAction: "Remove the cross-company target.", agentId: input.agentId }]); + case "ordinary_domain_expertise": + return { + ...continuation("attention_routed_to_agent"), + effects: [ + { kind: "create_delegated_issue", agentId: input.agentId, summary: "Resolve the routed expertise request." }, + ...continuation("attention_routed_to_agent").effects, + ], + }; + case "intentional_human_judgment": + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_review", + toStatus: "in_review", + reasonCode: "attention_requires_human_authority", + unblockDescriptor: null, + effects: [ + { kind: "create_interaction", prompt: "Provide the required human judgment." }, + { kind: "notify_owner", agentId: input.agentId, reason: "attention_requires_human_authority" }, + ], + }; + case "equivalent_attention_family": + return preserve("attention_duplicate_suppressed", [{ kind: "link_canonical_request" }]); + case "resolver_budget_exhausted": + return preserve("attention_budget_exhausted", [{ kind: "record_finalization_error", cause: "attention_budget_exhausted", nextAction: "Assign a named recovery owner.", agentId: input.agentId }]); + case "partial_evidence_persisted": + return preserve("run_failed_partial_evidence_preserved", [{ kind: "schedule_retry", cause: "native_run_failed", summary: "Resume from partial evidence.", agentId: input.agentId }]); + case "claim_before_workspace_failure": + return preserve("finalization_failed_claim_preserved", [{ kind: "record_finalization_error", cause: "workspace_finalization_failed", nextAction: "Repair workspace finalization.", agentId: input.agentId }]); + case "turn_scope": + return preserve("cancellation_turn_only", [{ kind: "accept_replacement_turn" }]); + case "run_scope": + return preserve("cancellation_run_only", [{ kind: "release_run_resources" }]); + case "issue_scope_authorized": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "cancelled", toStatus: "cancelled", reasonCode: "cancellation_issue_authorized", unblockDescriptor: null, effects: [{ kind: "release_checkout" }, { kind: "cancel_continuations" }] }; + case "new_evidence_satisfies_contract": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done", reasonCode: "decision_superseded_by_new_evidence", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] }; + case "dependency_now_done": + return continuation("dependency_resolved"); + case "explicit_resume_capability": + return continuation("authorized_resume"); + case "replacement_turn_accepted": + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", + toStatus: input.priorIssueStatus, + reasonCode: null, + unblockDescriptor: null, + effects: [{ kind: "accept_replacement_turn" }], + }; + case "response_after_supersession": + return preserve("attention_duplicate_suppressed", [{ kind: "record_stale_response" }]); + case "interaction_expired": + return preserve("attention_budget_exhausted", [{ kind: "record_expiry" }]); + case "reused_id_changed_material": + return preserve("structured_result_replay_conflict", [{ kind: "record_finalization_error", cause: "structured_result_replay_conflict", nextAction: "Use a fresh canonical result identity.", agentId: input.agentId }]); + case "result_preserved": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done", reasonCode: "completion_contract_satisfied", unblockDescriptor: null, effects: [{ kind: "resume_workspace_operation" }, { kind: "release_checkout" }] }; + case "board_cancelled_before_cas": + return preserve("prior_status_terminal_preserved", [{ kind: "append_superseding_assessment" }]); + case "new_policy_requires_review": + return review([{ kind: "append_superseding_assessment" }]); + case "shadow_application_disabled": + return preserve("completion_contract_satisfied", [{ kind: "record_shadow_decision" }]); + case "mixed_ledger": + return preserve("completion_contract_satisfied", [{ kind: "render_four_layers" }]); + case "authorized_writer_incremented_version": + return preserve("arbitration_conflict_reloaded", [{ kind: "increment_status_version" }, { kind: "schedule_reconciliation" }]); + case "shadow_compute": + return preserve("completion_contract_satisfied", [{ kind: "materialize_contract" }, { kind: "record_shadow_decision" }]); + case "classified_native_legacy_divergence": + return preserve("completion_evidence_incomplete", [{ kind: "record_mode_labeled_divergence" }]); + case "cohort_policy_pinned": + return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done", reasonCode: "completion_contract_satisfied", unblockDescriptor: null, effects: [{ kind: "record_mode_native" }, { kind: "record_policy_version" }] }; + } +} + /** Pure authority boundary: model prose is evidence, never a status command. */ export function arbitrateNativeStatus(input: { assessment: NativeEvidenceAssessment; terminalState: "succeeded" | "failed" | "cancelled"; workspaceFinalizeStatus: "succeeded" | "failed"; governanceGate?: NativeGovernanceGate | null; + completionClaimPolicyAccepted?: boolean; + allowIncompleteContinuation?: boolean; agentId: string; priorIssueStatus: NativeAuthoritativeIssueStatus; }): NativeStatusDecision { if (["done", "cancelled"].includes(input.priorIssueStatus)) { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", toStatus: input.priorIssueStatus, reasonCode: "terminal_status_preserved", unblockDescriptor: null, @@ -59,11 +319,12 @@ export function arbitrateNativeStatus(input: { if (input.workspaceFinalizeStatus !== "succeeded") { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", toStatus: input.priorIssueStatus, - reasonCode: "workspace_finalization_retry_required", + reasonCode: "finalization_failed_claim_preserved", unblockDescriptor: null, effects: [{ - kind: "record_recovery", + kind: "record_finalization_error", cause: "workspace_finalization_failed", nextAction: "Repair and re-run workspace finalization for the persisted native result.", agentId: input.agentId, @@ -71,16 +332,26 @@ export function arbitrateNativeStatus(input: { }; } if (input.terminalState !== "succeeded") { - const cause = input.terminalState === "cancelled" ? "native_run_cancelled" : "native_run_failed"; + if (input.terminalState === "cancelled") { + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", + toStatus: input.priorIssueStatus, + reasonCode: "cancellation_run_only", + unblockDescriptor: null, + effects: [{ kind: "release_run_resources" }], + }; + } return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, - toStatus: "in_progress", - reasonCode: cause, + statusAction: "preserve", + toStatus: input.priorIssueStatus, + reasonCode: "run_failed_partial_evidence_preserved", unblockDescriptor: null, effects: [{ - kind: "record_recovery", - cause, - nextAction: "Resume the persisted native run without opening a second provider session.", + kind: "schedule_retry", + cause: "native_run_failed", + summary: "Resume the persisted native run without opening a second provider session.", agentId: input.agentId, }], }; @@ -88,10 +359,14 @@ export function arbitrateNativeStatus(input: { if (input.governanceGate) { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_review", toStatus: "in_review", - reasonCode: "governance_gate_pending", + reasonCode: "governed_gate_pending", unblockDescriptor: null, - effects: [{ kind: "bind_governance", gate: input.governanceGate }], + effects: [ + { kind: "create_interaction", gate: input.governanceGate }, + { kind: "notify_owner", agentId: input.agentId, reason: "governed_gate_pending" }, + ], }; } const complete = @@ -103,8 +378,11 @@ export function arbitrateNativeStatus(input: { if (complete) { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "done", toStatus: "done", - reasonCode: "completion_contract_satisfied", + reasonCode: input.completionClaimPolicyAccepted + ? "completion_claim_policy_accepted" + : "completion_contract_satisfied", unblockDescriptor: null, effects: [{ kind: "release_checkout" }], }; @@ -112,13 +390,17 @@ export function arbitrateNativeStatus(input: { if (input.assessment.reportedDisposition === "needs_review") { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_review", toStatus: "in_review", - reasonCode: "native_review_path_required", + reasonCode: "completion_review_required", unblockDescriptor: null, - effects: [{ - kind: "create_review_interaction", - prompt: "Review the persisted native-run evidence and confirm whether this issue may be completed.", - }], + effects: [ + { + kind: "bind_reviewer", + prompt: "Review the persisted native-run evidence and confirm whether this issue may be completed.", + }, + { kind: "notify_owner", agentId: input.agentId, reason: "completion_review_required" }, + ], }; } if (input.assessment.reportedDisposition === "blocked" && input.assessment.blocker) { @@ -126,16 +408,21 @@ export function arbitrateNativeStatus(input: { if (input.assessment.blocker.scope === "task_wide") { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "blocked", toStatus: "blocked", - reasonCode: "native_task_wide_blocker", + reasonCode: "task_wide_blocker_bound", unblockDescriptor: { owner, action: input.assessment.blocker.unblockAction }, - effects: [{ kind: "bind_blocker", owner, action: input.assessment.blocker.unblockAction }], + effects: [ + { kind: "bind_blocker", owner, action: input.assessment.blocker.unblockAction }, + { kind: "notify_owner", agentId: input.agentId, reason: "task_wide_blocker_bound" }, + ], }; } return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_progress", toStatus: "in_progress", - reasonCode: "native_track_blocked_continuation", + reasonCode: "turn_waiting_other_track_live", unblockDescriptor: null, effects: [{ kind: "enqueue_continuation", @@ -149,8 +436,9 @@ export function arbitrateNativeStatus(input: { if (input.assessment.reportedDisposition === "yielded" && input.assessment.continuation) { return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_progress", toStatus: "in_progress", - reasonCode: "native_continuation_required", + reasonCode: "live_continuation_registered", unblockDescriptor: null, effects: [{ kind: "enqueue_continuation", @@ -161,10 +449,26 @@ export function arbitrateNativeStatus(input: { }], }; } + if (input.allowIncompleteContinuation === false) { + return { + policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "preserve", + toStatus: input.priorIssueStatus, + reasonCode: "prior_status_preserved_no_live_path", + unblockDescriptor: null, + effects: [{ + kind: "record_finalization_error", + cause: "completion_evidence_incomplete", + nextAction: "Bind a durable continuation or a named recovery owner before changing issue status.", + agentId: input.agentId, + }], + }; + } return { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + statusAction: "in_progress", toStatus: "in_progress", - reasonCode: "native_completion_incomplete", + reasonCode: "completion_evidence_incomplete", unblockDescriptor: null, effects: [{ kind: "enqueue_continuation", diff --git a/server/src/services/native-runtime/status-decision-committer.ts b/server/src/services/native-runtime/status-decision-committer.ts index 6540d055212..33ea3936459 100644 --- a/server/src/services/native-runtime/status-decision-committer.ts +++ b/server/src/services/native-runtime/status-decision-committer.ts @@ -91,7 +91,7 @@ async function enqueueWake(input: { async function validateGovernanceGate(tx: Db, input: { companyId: string; issueId: string; - gate: Extract["gate"]; + gate: NonNullable["gate"]>; executionState: unknown; }) { if (input.gate.kind === "execution_stage") { @@ -135,17 +135,46 @@ async function materializeDecisionEffect(input: { failpoint?: NativeStatusCommitFailpoint; }): Promise { const { effect } = input; - if (effect.kind === "bind_governance") { + if (effect.kind === "create_interaction") { failAt("governance_materialization", input.failpoint); - const targetId = await validateGovernanceGate(input.tx, { - companyId: input.companyId, - issueId: input.issue.id, - gate: effect.gate, - executionState: input.issue.executionState, - }); - return { effectKind: effect.kind, targetType: effect.gate.kind, targetId, payload: { gate: effect.gate } }; + if (effect.gate) { + const targetId = await validateGovernanceGate(input.tx, { + companyId: input.companyId, + issueId: input.issue.id, + gate: effect.gate, + executionState: input.issue.executionState, + }); + return { effectKind: effect.kind, targetType: effect.gate.kind, targetId, payload: { gate: effect.gate } }; + } + const interaction = await issueThreadInteractionService(input.tx).create( + input.issue, + { + kind: "ask_user_questions", + idempotencyKey: `native-attention:${input.decisionId}`, + sourceRunId: input.runId, + title: "Native attention request", + summary: "The native runner requires a board response.", + continuationPolicy: "wake_assignee", + payload: { + version: 1, + questions: [{ + id: "response", + prompt: effect.prompt ?? "Provide the required response.", + selectionMode: "single", + options: [{ id: "continue", label: "Continue" }], + }], + }, + }, + { systemId: "native-status-committer", runId: input.runId }, + ); + return { + effectKind: effect.kind, + targetType: "issue_thread_interaction", + targetId: interaction.id, + payload: { interactionId: interaction.id, interactionKind: interaction.kind }, + }; } - if (effect.kind === "create_review_interaction") { + if (effect.kind === "bind_reviewer") { failAt("interaction_materialization", input.failpoint); const reviewInput: Extract = { kind: "request_confirmation", @@ -176,6 +205,45 @@ async function materializeDecisionEffect(input: { payload: { interactionId: interaction.id, interactionKind: interaction.kind }, }; } + if (effect.kind === "notify_owner") { + const wakeId = await enqueueWake({ + tx: input.tx, + companyId: input.companyId, + issueId: input.issue.id, + agentId: effect.agentId, + reason: "issue_status_changed", + idempotencyKey: `native-status:${input.decisionId}:notify-owner`, + payload: { nativeDecisionId: input.decisionId, notificationReason: effect.reason }, + }); + return { + effectKind: effect.kind, + targetType: "agent_wakeup_request", + targetId: wakeId, + payload: { reason: effect.reason }, + }; + } + if (effect.kind === "create_delegated_issue") { + const delegated = await input.tx.insert(issues).values({ + companyId: input.companyId, + projectId: input.issue.projectId, + projectWorkspaceId: input.issue.projectWorkspaceId, + goalId: input.issue.goalId, + parentId: input.issue.id, + title: effect.summary, + status: "todo", + priority: input.issue.priority, + assigneeAgentId: effect.agentId, + createdByAgentId: input.issue.assigneeAgentId, + workMode: "standard", + }).returning({ id: issues.id }).then((rows) => rows[0] ?? null); + if (!delegated) throw new Error("native_delegated_issue_not_persisted"); + return { + effectKind: effect.kind, + targetType: "delegated_issue", + targetId: delegated.id, + payload: { summary: effect.summary, agentId: effect.agentId }, + }; + } if (effect.kind === "enqueue_continuation") { failAt("continuation_materialization", input.failpoint); const wakeId = await enqueueWake({ @@ -220,6 +288,85 @@ async function materializeDecisionEffect(input: { payload: { owner: effect.owner, action: effect.action }, }; } + if (effect.kind === "schedule_retry") { + failAt("continuation_materialization", input.failpoint); + const wakeId = await enqueueWake({ + tx: input.tx, + companyId: input.companyId, + issueId: input.issue.id, + agentId: effect.agentId, + reason: "issue_status_changed", + idempotencyKey: `native-status:${input.decisionId}:retry`, + payload: { + nativeDecisionId: input.decisionId, + retryCause: effect.cause, + retrySummary: effect.summary, + }, + }); + return { + effectKind: effect.kind, + targetType: "agent_wakeup_request", + targetId: wakeId, + payload: { cause: effect.cause, summary: effect.summary }, + }; + } + if (effect.kind === "record_finalization_error") { + failAt("recovery_materialization", input.failpoint); + const recovery = await issueRecoveryActionService(input.tx).upsertSourceScoped({ + companyId: input.companyId, + sourceIssueId: input.issue.id, + kind: "active_run_watchdog", + ownerType: "agent", + ownerAgentId: effect.agentId, + cause: effect.cause, + fingerprint: nativeSha256({ runId: input.runId, decisionId: input.decisionId, cause: effect.cause }), + evidence: { runId: input.runId, decisionId: input.decisionId }, + nextAction: effect.nextAction, + wakePolicy: null, + maxAttempts: 3, + }); + return { + effectKind: effect.kind, + targetType: "issue_recovery_action", + targetId: recovery.id, + payload: { cause: effect.cause, nextAction: effect.nextAction }, + }; + } + if (effect.kind === "release_run_resources") { + failAt("recovery_materialization", input.failpoint); + return { + effectKind: effect.kind, + targetType: "heartbeat_run", + targetId: input.runId, + payload: {}, + }; + } + if (effect.kind === "record_expiry" || effect.kind === "schedule_reconciliation") { + failAt("recovery_materialization", input.failpoint); + if (!input.issue.assigneeAgentId) throw new Error("native_recovery_owner_missing"); + const cause = effect.kind === "record_expiry" ? "attention_expired" : "native_reconciliation_required"; + const recovery = await issueRecoveryActionService(input.tx).upsertSourceScoped({ + companyId: input.companyId, + sourceIssueId: input.issue.id, + kind: "active_run_watchdog", + ownerType: "agent", + ownerAgentId: input.issue.assigneeAgentId, + cause, + fingerprint: nativeSha256({ runId: input.runId, decisionId: input.decisionId, cause }), + evidence: { runId: input.runId, decisionId: input.decisionId }, + nextAction: effect.kind === "record_expiry" + ? "Review the expired attention route and choose a new authorized path." + : "Reconcile the native decision against the latest issue status version.", + wakePolicy: null, + maxAttempts: 3, + }); + return { + effectKind: effect.kind, + targetType: "issue_recovery_action", + targetId: recovery.id, + payload: { cause }, + }; + } if (effect.kind === "record_recovery") { failAt("recovery_materialization", input.failpoint); const recovery = await issueRecoveryActionService(input.tx).upsertSourceScoped({ @@ -262,6 +409,10 @@ export async function commitNativeStatusDecision(input: { decision: NativeStatusDecision; failpoint?: NativeStatusCommitFailpoint; }) { + if (input.decision.reasonCode === null) { + throw new Error("native_status_reason_code_required"); + } + const reasonCode = input.decision.reasonCode; const publications: ActivityPublication[] = []; const committed = await input.db.transaction(async (tx) => { const coordinator = await tx.select().from(nativeRunFinalizations).where(and( @@ -284,7 +435,7 @@ export async function commitNativeStatusDecision(input: { } const decisionJson = { toStatus: input.decision.toStatus, - reasonCode: input.decision.reasonCode, + reasonCode, unblockDescriptor: input.decision.unblockDescriptor, effects: input.decision.effects, }; @@ -313,7 +464,7 @@ export async function commitNativeStatusDecision(input: { policyVersion: input.decision.policyVersion, fromStatus: issue.status, toStatus: input.decision.toStatus, - reasonCode: input.decision.reasonCode, + reasonCode, decisionJson, decisionDigest, applicationState: "proposed", @@ -348,7 +499,7 @@ export async function commitNativeStatusDecision(input: { effectKind: "issue_status_projection", targetType: "issue", targetId: input.issueId, - payload: { fromStatus: issue.status, toStatus: input.decision.toStatus, reasonCode: input.decision.reasonCode }, + payload: { fromStatus: issue.status, toStatus: input.decision.toStatus, reasonCode }, }); if (input.decision.toStatus === "done" && issue.status !== "done") { @@ -441,7 +592,7 @@ export async function commitNativeStatusDecision(input: { decisionId: decisionRow.id, fromStatus: issue.status, toStatus: input.decision.toStatus, - reasonCode: input.decision.reasonCode, + reasonCode, effectCount: materialized.length, }, });