@bridge_gpt/mcp-server 0.2.69 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +140 -292
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +23 -4
  7. package/build/claude-review-workflow-drift.js +3 -3
  8. package/build/codex-bridge-profile.js +1 -1
  9. package/build/commands.generated.js +6 -6
  10. package/build/conductor/bridge-api-client.js +598 -19
  11. package/build/conductor/bring-up-facts.js +9 -9
  12. package/build/conductor/doctor.js +466 -18
  13. package/build/conductor/epic-child-continuation.js +54 -3
  14. package/build/conductor/epic-implementer-cli.js +132 -34
  15. package/build/conductor/epic-runtime.js +2 -2
  16. package/build/conductor/epic-state.js +1 -1
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/install-doctor.js +4 -2
  19. package/build/conductor/launch-need-probes.js +337 -0
  20. package/build/conductor/local-merge.js +241 -56
  21. package/build/conductor/merge-credential.js +172 -0
  22. package/build/conductor/merge-execution-eligibility.js +1 -1
  23. package/build/conductor/merged-child-check.js +95 -0
  24. package/build/conductor/plan-trim.js +30 -0
  25. package/build/conductor/pr-ci-producer.js +1 -1
  26. package/build/conductor/readiness-cli.js +11 -6
  27. package/build/conductor/readiness-prerequisites.js +41 -0
  28. package/build/conductor/readiness-sections.js +345 -55
  29. package/build/conductor/readiness.js +99 -87
  30. package/build/conductor/recovery-cli.js +606 -21
  31. package/build/conductor/recovery-operations.js +306 -7
  32. package/build/conductor/repair-contract.js +47 -2
  33. package/build/conductor/repository-workflow-remedy.js +8 -6
  34. package/build/conductor/stored-merge-credential.js +76 -0
  35. package/build/conductor/stored-merge-token-probe.js +28 -0
  36. package/build/conductor/temporary-plan.js +50 -0
  37. package/build/conductor/wait-run-cli.js +759 -0
  38. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  39. package/build/conductor-bin.js +2 -2
  40. package/build/conductor-claude-hook-bin.js +2 -2
  41. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  42. package/build/conductor-removed-stub-bin.js +2 -5
  43. package/build/credential-store.js +356 -13
  44. package/build/docs.generated.js +26 -13
  45. package/build/doctor.js +94 -32
  46. package/build/drive-epic-help.js +212 -0
  47. package/build/drive-epic.js +1840 -373
  48. package/build/epic-ci-workflow-drift.js +77 -0
  49. package/build/epic-ci-workflow-install.js +183 -0
  50. package/build/epic-ci-workflow.js +157 -0
  51. package/build/epic-contract-context.js +326 -0
  52. package/build/epic-implementer-bin.js +57 -41
  53. package/build/epic-implementer-claude-hook-bin.js +2 -2
  54. package/build/escalation-reachability.js +134 -0
  55. package/build/executor/cleanup-report.js +99 -0
  56. package/build/executor/cli.js +12 -1
  57. package/build/executor/completion-contract.js +66 -11
  58. package/build/executor/completion-supervision.js +99 -0
  59. package/build/executor/delivery-outcome.js +179 -0
  60. package/build/executor/deps.js +3 -0
  61. package/build/executor/env.js +71 -0
  62. package/build/executor/failure-reason-codes.js +126 -0
  63. package/build/executor/heartbeat.js +38 -0
  64. package/build/executor/http-client.js +72 -0
  65. package/build/executor/integration-job.js +298 -5
  66. package/build/executor/job-errors.js +36 -6
  67. package/build/executor/job-runner.js +720 -36
  68. package/build/executor/likely-tests.js +36 -2
  69. package/build/executor/merge-job.js +145 -2
  70. package/build/executor/observation.js +73 -4
  71. package/build/executor/permissions.js +193 -97
  72. package/build/executor/preflight.js +14 -3
  73. package/build/executor/process-containment.js +28 -2
  74. package/build/executor/prompt-spec.js +50 -5
  75. package/build/executor/rebase-finalization.js +38 -3
  76. package/build/executor/reconcile-overlap-job.js +35 -7
  77. package/build/executor/required-work-ledger.js +236 -81
  78. package/build/executor/resume-pre-spawn.js +38 -1
  79. package/build/executor/runner.js +91 -3
  80. package/build/executor/test-clock.js +8 -0
  81. package/build/executor/types.js +37 -2
  82. package/build/executor/worker-cli-launcher.js +259 -0
  83. package/build/executor/worker-command.js +41 -4
  84. package/build/executor/worker-database-target.js +91 -0
  85. package/build/executor/worker-database.js +908 -0
  86. package/build/executor/worker-finalization.js +106 -0
  87. package/build/executor/worker-guard-hook.js +203 -0
  88. package/build/executor/worker-resources.js +47 -0
  89. package/build/executor/worker-verification-guidance.js +15 -2
  90. package/build/executor/worktree.js +303 -15
  91. package/build/executor-launch.js +11 -1
  92. package/build/generated-assets-doctor.js +9 -1
  93. package/build/generated-assets-health.js +13 -4
  94. package/build/generated-assets-legacy.js +39 -0
  95. package/build/generated-assets-paths.js +12 -1
  96. package/build/generated-assets-repair.js +4 -1
  97. package/build/generated-assets-test-support.js +12 -0
  98. package/build/github-remote.js +33 -0
  99. package/build/github-sign-in.js +62 -0
  100. package/build/index.js +1067 -536
  101. package/build/init.js +39 -1
  102. package/build/install-bridge-conductor-gate.js +78 -0
  103. package/build/install-bridge-conductor-setup.js +413 -0
  104. package/build/install-bridge-conductor.js +1226 -267
  105. package/build/install-bridge.js +376 -60
  106. package/build/mcp-install-state.js +8 -2
  107. package/build/merge-pull-request.js +6 -0
  108. package/build/merge-workflow-install.js +763 -345
  109. package/build/phase-result-artifacts.js +75 -20
  110. package/build/pipeline-orchestrator.js +247 -4
  111. package/build/pipelines.generated.js +35 -35
  112. package/build/plan-discovery.js +15 -2
  113. package/build/plan-execution-ledger.js +306 -17
  114. package/build/plan-phase-routing.js +48 -13
  115. package/build/plane/cli.js +105 -51
  116. package/build/plane/defaults.js +3 -0
  117. package/build/plane/plane-freshness.js +294 -0
  118. package/build/plane/schema-compatibility.js +137 -0
  119. package/build/plane/shutdown-deps.js +38 -0
  120. package/build/plane/shutdown.js +83 -8
  121. package/build/plane/terminal-run-cleanup.js +303 -0
  122. package/build/printed-remedy-commands.js +26 -4
  123. package/build/readiness-check.js +6 -2
  124. package/build/readiness-prerequisites.generated.js +532 -0
  125. package/build/readme.generated.js +1 -1
  126. package/build/setup-epic.js +400 -30
  127. package/build/shared-resources.generated.js +5 -0
  128. package/build/start-tickets.js +28 -15
  129. package/build/test-command-detection.js +247 -0
  130. package/build/test-command-validation.js +73 -0
  131. package/build/testing-config-client.js +150 -0
  132. package/build/ticket-delivery-artifacts.js +258 -0
  133. package/build/ticket-delivery-enrollment.js +306 -0
  134. package/build/ticket-delivery.js +205 -0
  135. package/build/ticket-review-summary.js +14 -0
  136. package/build/upgrade-cli.js +13 -0
  137. package/build/v2-readiness-requirements.js +552 -0
  138. package/build/version.generated.js +3 -3
  139. package/build/worker-guard-hook-bin.js +2 -2
  140. package/build/worktree-core.js +22 -7
  141. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  142. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  143. package/docs/EPIC-IMPLEMENTER.md +104 -63
  144. package/docs/epic-implementer/getting-started.md +131 -0
  145. package/docs/epic-implementer/self-hosting.md +133 -0
  146. package/package.json +3 -3
  147. package/pipelines/idea-to-ticket.json +1 -1
  148. package/pipelines/implement-ticket.json +8 -8
  149. package/pipelines/review-ticket.json +2 -2
  150. package/design-assets/favicon/favicon-32x32.png +0 -0
  151. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { findWorktreeByBranch, parseGitWorktreePorcelain } from "./worktree-inspection.js";
6
6
  import { resolveExecutorBranch, configureExecutorBranch } from "./worktree.js";
7
- import { renderWorkerVerificationGuidance, renderWorkerPrBaseGuidance } from "./worker-verification-guidance.js";
7
+ import { renderWorkerVerificationGuidance, renderWorkerPrBaseGuidance, renderWorkerTimeBudgetGuidance } from "./worker-verification-guidance.js";
8
8
  /**
9
9
  * Claimed-job execution engine (BAPI-534 core, BAPI-535 job behaviors; TDD §7/§8/§9).
10
10
  *
@@ -21,11 +21,13 @@ import { renderWorkerVerificationGuidance, renderWorkerPrBaseGuidance } from "./
21
21
  * `spec_review`, plus per-job worker-log tee, watch registry, and optional
22
22
  * read-only viewer tabs wired around every spawned worker.
23
23
  */
24
- import { rm, readdir, lstat, unlink, realpath } from "node:fs/promises";
24
+ import { rm, readdir, lstat, unlink, realpath, rename as fsRename, stat as fsStat } from "node:fs/promises";
25
+ import { provisionWorktreeSettingsExclude } from "./permissions.js";
25
26
  import os from "node:os";
26
- import { runHeartbeatLoop } from "./heartbeat.js";
27
- import { isExecutorNamedError, toExecutorFailure, RebaseNotVerified, secretFreeErrorMessage, MissingVerdictArtifact, PackagedRecipeUndeliverable, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, WorkerEndedWithPendingBackgroundWork, WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, CiFixContext, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
28
- import { buildConductorMergeAccessForExecutorJob, buildDefaultMergeLocalDeps, runExecutorMergeJob, MERGE_RETRYABLE, MERGE_FAILED, } from "./merge-job.js";
27
+ import { runHeartbeatLoop, sendImmediateHeartbeat } from "./heartbeat.js";
28
+ import { cleanupReportPermitsRecordRemoval, retryWakeCleanupReport, submitCleanupReport, } from "./cleanup-report.js";
29
+ import { isExecutorNamedError, toExecutorFailure, RebaseNotVerified, secretFreeErrorMessage, secretFreeWorktreeMessage, WorktreeSetupRefused, MissingVerdictArtifact, PackagedRecipeUndeliverable, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, WorkerEndedWithPendingBackgroundWork, WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, CiFixContext, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
30
+ import { buildConductorMergeAccessForExecutorJob, buildMergeLocalDepsForJob, runExecutorMergeJob, MERGE_RETRYABLE, MERGE_FAILED, MERGE_ACCESS_UNAVAILABLE, MERGE_FLOW_ERROR, MERGE_FLOW_TIMEOUT, MERGE_NO_OUTCOME, } from "./merge-job.js";
29
31
  import { createProductionCiRerunDeps, runCiRerunJob, } from "./ci-rerun-job.js";
30
32
  import { createProductionGateQualificationDeps, runGateQualificationJob, GATE_QUALIFICATION_FAILED, GATE_QUALIFICATION_INTERRUPTED, } from "./gate-qualification-job.js";
31
33
  import { createObservationState } from "./observation.js";
@@ -34,8 +36,8 @@ import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter
34
36
  import { redactAdapterDiagnostic } from "../agent-launchers/executor-adapter.js";
35
37
  import { resolveExecutorAgentId } from "./agent-identity.js";
36
38
  import { closeStdinBounded, createProcessTerminationController, runProcessWithTimeout, writeStdinBounded, } from "./process.js";
37
- import { buildNotLaunchedEnvelope, claimRequiresCompletionEvidence, COMPLETION_CONTRACT_MARKER_VALUES, isCoveredJobType, MAX_EVENT_BYTES, runtimeMatchesSupported, successRefusalReason, verificationVerdictFromFacts, } from "./completion-contract.js";
38
- import { COMPLETION_CAUSE_ERROR_KINDS, EOF_DRAIN_BOUND_MS, STDIN_CLOSE_GRACE_MS, finalizationFacts, finalizationObligations, planCompletionAllowance, readCompletionBinding, readDeclaredChecks, readOptionalWatchers, readRequiredVerification, } from "./completion-supervision.js";
39
+ import { buildNotLaunchedEnvelope, claimRequiresCleanupReport, claimRequiresCompletionEvidence, COMPLETION_CONTRACT_MARKER_VALUES, isCoveredJobType, MAX_EVENT_BYTES, runtimeMatchesSupported, successRefusalReason, verificationVerdictFromFacts, } from "./completion-contract.js";
40
+ import { COMPLETION_CAUSE_ERROR_KINDS, EOF_DRAIN_BOUND_MS, STDIN_CLOSE_GRACE_MS, finalizationFacts, finalizationObligations, planCompletionAllowance, readCompletionBinding, readDeclaredChecks, readOptionalWatchers, readRequiredVerification, renderApprovedVerificationGuidance, } from "./completion-supervision.js";
39
41
  import { compareFinalArtifact, createRequiredVerificationRunner, createSupervisedCommandLauncher, observeFinalArtifact, OWNERSHIP_POLL_MS, preCheckArtifactState, toCompletionCheckFact, } from "./required-verification.js";
40
42
  import { createClosureSupervisor, createRequiredWorkLedger } from "./required-work-ledger.js";
41
43
  import { buildCleanupVerdict, CLEANUP_RESIDUALS_NOT_COVERED, createDefaultOwnershipRecordFs, createDescendantTracker, createMacProcessInspector, createOwnershipToken, hostIdentityDigest, ownershipTokenDigest, removeOwnershipRecord, runOwnedProcessSweep, withOwnershipTag, writeOwnershipRecord, } from "./process-containment.js";
@@ -51,6 +53,8 @@ import { buildGenericSuccessResult, CI_FIX_CLAIMS_STATUS_KEY, readCiFixResultArt
51
53
  import { sendTerminalMutationWithRetry } from "./terminal-mutation.js";
52
54
  import { removeStaleVerdictArtifacts } from "./stale-artifacts.js";
53
55
  import { stripWorkerEnvFiles } from "./env-file-guard.js";
56
+ import { formatWorkerDatabaseDropFailedLine, formatWorkerDatabaseUnavailableLine, } from "./worker-database.js";
57
+ import { formatWorkerCliLauncherUnavailableLine } from "./worker-cli-launcher.js";
54
58
  import { isVerdictJobType, readVerdictArtifact } from "./verdict-artifact.js";
55
59
  import { createWorkerLogClaimId, createWorkerLogTee, closeWorkerLogTee, distinctWorkerLogSessionIds, formatWorkerLogSessionInvariantWarning, teeAsyncIterable, } from "./worker-log.js";
56
60
  import { acquireExecutorWorktreeLock, } from "./worktree-lock.js";
@@ -61,12 +65,14 @@ import { cleanupInterruptedWorktree } from "./interrupted-worktree.js";
61
65
  import { lazyProviderCheckPoller, runReconcileOverlapJob, } from "./reconcile-overlap-job.js";
62
66
  import { resolveExecutorJobBaseBranch } from "./base-branch.js";
63
67
  import { resolveExecutorJobIndexScope } from "./index-scope.js";
64
- import { readFocusedPullRequest, validateWorkerFinalization } from "./worker-finalization.js";
68
+ import { deliveryHoldOf, readFocusedPullRequest, resolveOriginBranchSha, validateWorkerFinalization, } from "./worker-finalization.js";
69
+ import { DELIVERY_OUTCOME_READ_TIMEOUT_MS, fetchServerDeliveryOutcome, } from "./delivery-outcome.js";
65
70
  import { FOCUSED_REBASE_JOB_TYPE, focusedRebaseContextError, hasFocusedRebaseContext, validateFocusedRebaseContext, waitForFocusedRebase, } from "./rebase-finalization.js";
66
71
  import { ensureExecutorWorktree, finalizeRetainedExecutorWorktree, } from "./worktree.js";
67
- import { CI_FIX_JOB_TYPE, resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
72
+ import { CI_FIX_JOB_TYPE, renderCiFixOperatorNote, resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
68
73
  import { CORRECTION_INTENT_CAPABILITY, isRemainingWork, validateCorrectionIntent, } from "./correction-dispatch.js";
69
74
  import { collectGitTelemetry, collectRemoteTrackingSha, createMcpSurfaceObserver, evaluatePreSpawnGitVerification, formatWorkerRateLimitAdvisory, normalizeMcpServerNames, } from "./observation.js";
75
+ import { failureReasonCode, isFailureReasonCode } from "./failure-reason-codes.js";
70
76
  import { readCustomizedCommands, TERMINAL_CAPACITY_PROTOCOL_VERSIONS, TERMINAL_CAPACITY_RUNTIME_CONTRACTS, TERMINAL_CAPACITY_RUNTIMES, } from "./types.js";
71
77
  import { resolvePackagedRecipes } from "./packaged-recipe.js";
72
78
  import { probeInstalledCorrectionHook } from "./preflight.js";
@@ -90,6 +96,71 @@ const ENV_FILE_TRACKED_LOOKUP_TIMEOUT_MS = 5_000;
90
96
  const ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE = "[executor] tracked env-file lookup unavailable; exempting nothing";
91
97
  /** Default runtime for the no-op smoke process (ms). */
92
98
  const DEFAULT_SMOKE_DURATION_MS = 100;
99
+ /**
100
+ * BAPI-1426: implementation-style job types that never read a delivery outcome.
101
+ * `reconcile_overlap`'s push is the wrapper's own, not a ticket delivery attempt.
102
+ * BAPI-1516: `ci_fix` is excluded too. A repair is judged by its own completion
103
+ * contract (a provider-observed push plus dispatched-check claims), not by ticket
104
+ * delivery readiness. An older server still guards `ci_fix`; the executor then
105
+ * reports success and that server's refusal parks the ticket as before, which the
106
+ * reconciler's green-head lane recovers once the PR head is green.
107
+ */
108
+ const DELIVERY_OUTCOME_EXCLUDED_JOB_TYPES = new Set([
109
+ "reconcile_overlap",
110
+ "ci_fix",
111
+ ]);
112
+ /** BAPI-1426: the outcome used when the read could not run or failed. */
113
+ const DELIVERY_OUTCOME_UNAVAILABLE = { bound: "unknown", unavailable: true };
114
+ /**
115
+ * BAPI-1426: read the server-confirmed delivery outcome for a finalizing
116
+ * implementation-style job. Returns undefined (no read at all) for a job with no
117
+ * ticket or branch, and for the excluded job types above. Bounded by
118
+ * `remainingMs`; never throws.
119
+ */
120
+ async function readDeliveryOutcomeForFinalization(params) {
121
+ const { job, branch, deps, seams } = params;
122
+ const env = params.resolvedBaseUrl !== undefined && params.resolvedBaseUrl.trim() !== ""
123
+ ? { ...deps.env, BAPI_BASE_URL: params.resolvedBaseUrl }
124
+ : deps.env;
125
+ if (!isImplementationStyleJobType(job.job_type) || DELIVERY_OUTCOME_EXCLUDED_JOB_TYPES.has(job.job_type)) {
126
+ return undefined;
127
+ }
128
+ const trimmedBranch = typeof branch === "string" ? branch.trim() : "";
129
+ if (!trimmedBranch || !job.ticket_key)
130
+ return undefined;
131
+ const budgetMs = Math.min(DELIVERY_OUTCOME_READ_TIMEOUT_MS, params.remainingMs());
132
+ if (budgetMs <= 0)
133
+ return DELIVERY_OUTCOME_UNAVAILABLE;
134
+ try {
135
+ if (seams.fetchDeliveryOutcome)
136
+ return await seams.fetchDeliveryOutcome(job, trimmedBranch);
137
+ return await fetchServerDeliveryOutcome(job, trimmedBranch, {
138
+ fetch: deps.fetch,
139
+ accessDeps: {
140
+ env,
141
+ cwd: deps.cwd,
142
+ homedir: deps.homedir,
143
+ platform: deps.platform,
144
+ readFile: deps.readFile,
145
+ stat: deps.stat,
146
+ },
147
+ timeoutMs: budgetMs,
148
+ setTimer: deps.setTimer,
149
+ clearTimer: deps.clearTimer,
150
+ });
151
+ }
152
+ catch {
153
+ return DELIVERY_OUTCOME_UNAVAILABLE;
154
+ }
155
+ }
156
+ /**
157
+ * BAPI-1426: a finalization failure may carry its own telemetry (the delivery
158
+ * hold marker). It is merged over the observation snapshot instead of being
159
+ * replaced by it; a failure without telemetry keeps the snapshot unchanged.
160
+ */
161
+ function withFailureTelemetry(snapshot, failureTelemetry) {
162
+ return failureTelemetry ? { ...snapshot, ...failureTelemetry } : snapshot;
163
+ }
93
164
  /** ProcessClassification maps 1:1 onto the wire ExecutorClassification. */
94
165
  function toWireClassification(c) {
95
166
  return c;
@@ -507,6 +578,7 @@ export async function defaultRunMergeForClaimed(job, deps, options, controls) {
507
578
  error_kind: MERGE_RETRYABLE,
508
579
  error_message: accessResult.error,
509
580
  classification: "crashed",
581
+ telemetry: { failure_reason_code: MERGE_ACCESS_UNAVAILABLE },
510
582
  },
511
583
  };
512
584
  }
@@ -514,12 +586,19 @@ export async function defaultRunMergeForClaimed(job, deps, options, controls) {
514
586
  access: accessResult.access,
515
587
  // Thread the overall-timeout abort signal into local merge deps WITHOUT
516
588
  // changing the local `gh` credential model (the signal is not a credential).
517
- localMergeDeps: buildDefaultMergeLocalDeps(deps, controls?.signal),
589
+ // BAPI-1453: the same signal bounds the post-`gh_pr_view_failed` permission
590
+ // probe, so it can never outlive the merge flow's own timeout.
591
+ // BAPI-1461: resolved when the job runs, with the stored-entry fallback for
592
+ // an executor started in the person's own session. A durable-host service
593
+ // executor declared itself with --scrub-inherited-github-credentials and
594
+ // never reads a stored GitHub credential.
595
+ localMergeDeps: await buildMergeLocalDepsForJob(job, deps, { allowStoredEntry: options.scrubInheritedGithubCredentials !== true }, controls?.signal),
518
596
  // BAPI-951: the executor's own configured base — resolveExecutorJobBaseBranch
519
597
  // (called inside runExecutorMergeJob) resolves the CHILD PR's base from the
520
598
  // job's persisted run base, falling back to this value for a legacy job.
521
599
  repositoryBaseBranch: options.baseBranch,
522
600
  epicIntegrationAdvisoryLog: deps.errorLog,
601
+ diagnosticLog: deps.errorLog,
523
602
  cwd: deps.cwd,
524
603
  });
525
604
  }
@@ -1205,7 +1284,7 @@ async function runMergeJob(job, httpClient, options, deps, ownership, observatio
1205
1284
  error_kind: MERGE_RETRYABLE,
1206
1285
  error_message: `merge flow timed out after ${timeoutSeconds} seconds`,
1207
1286
  classification: "timeout",
1208
- telemetry: observation.snapshot(),
1287
+ telemetry: { failure_reason_code: MERGE_FLOW_TIMEOUT, ...observation.snapshot() },
1209
1288
  }),
1210
1289
  deps,
1211
1290
  options,
@@ -1223,7 +1302,7 @@ async function runMergeJob(job, httpClient, options, deps, ownership, observatio
1223
1302
  error_kind: MERGE_FAILED,
1224
1303
  error_message: secretFreeErrorMessage(flowError),
1225
1304
  classification: "crashed",
1226
- telemetry: observation.snapshot(),
1305
+ telemetry: { failure_reason_code: MERGE_FLOW_ERROR, ...observation.snapshot() },
1227
1306
  }),
1228
1307
  deps,
1229
1308
  options,
@@ -1242,7 +1321,7 @@ async function runMergeJob(job, httpClient, options, deps, ownership, observatio
1242
1321
  error_kind: MERGE_FAILED,
1243
1322
  error_message: "merge flow completed without an outcome",
1244
1323
  classification: "crashed",
1245
- telemetry: observation.snapshot(),
1324
+ telemetry: { failure_reason_code: MERGE_NO_OUTCOME, ...observation.snapshot() },
1246
1325
  }),
1247
1326
  deps,
1248
1327
  options,
@@ -1271,7 +1350,13 @@ async function runMergeJob(job, httpClient, options, deps, ownership, observatio
1271
1350
  }
1272
1351
  const terminal = await sendTerminalMutationWithAuthReport({
1273
1352
  kind: "fail",
1274
- send: () => httpClient.fail(job, { ...outcome.failure, telemetry: observation.snapshot() }),
1353
+ // BAPI-1453: MERGE the builder's telemetry with the snapshot rather than
1354
+ // replacing it, so `failure_reason_code` survives. Observation state never
1355
+ // carries a code, so the snapshot cannot overwrite it.
1356
+ send: () => httpClient.fail(job, {
1357
+ ...outcome.failure,
1358
+ telemetry: { ...outcome.failure.telemetry, ...observation.snapshot() },
1359
+ }),
1275
1360
  deps,
1276
1361
  options,
1277
1362
  ownership,
@@ -1519,13 +1604,68 @@ function resolveStaleBranchClassification(payload) {
1519
1604
  return "unknown";
1520
1605
  return raw.trim() === PR_NOT_ATTACHED_REASON_CODE ? "succeeded_pr_not_attached" : "unknown";
1521
1606
  }
1522
- /** The `/fail` payload for a failed worktree preparation. */
1607
+ /**
1608
+ * BAPI-1530: structured worktree outcomes the executor reports as the named,
1609
+ * deterministic `WorktreeSetupRefused`. A fetch, ref-resolution, or unclassified
1610
+ * failure (`unavailable` or no kind at all) keeps `WorktreeError` so the server's
1611
+ * legacy crash and transient handling is unchanged.
1612
+ */
1613
+ const SETUP_REFUSAL_KINDS = new Set([
1614
+ "stale_branch",
1615
+ "lineage_refused",
1616
+ "setup_refused",
1617
+ ]);
1618
+ /**
1619
+ * Display-only reason codes. They are set from structured outcomes only and read
1620
+ * by no disposition decision; a deliberate refusal with no accurate display
1621
+ * category (lock, authority, occupied destination) carries none.
1622
+ */
1623
+ function worktreeDisplayReasonCode(kind) {
1624
+ switch (kind) {
1625
+ case "stale_branch":
1626
+ return failureReasonCode("worktree_stale_branch");
1627
+ case "lineage_refused":
1628
+ return failureReasonCode("worktree_lineage_refused");
1629
+ case "unavailable":
1630
+ return failureReasonCode("worktree_fetch_failed");
1631
+ default:
1632
+ return undefined;
1633
+ }
1634
+ }
1635
+ /**
1636
+ * The `/fail` payload for a failed worktree preparation. The message is redacted
1637
+ * and bounded here, before it reaches the server or a log line, so raw Worktrunk
1638
+ * stderr never crosses the boundary. A named failure that already carries its own
1639
+ * secret-free message is returned as it is.
1640
+ */
1523
1641
  function jobWorktreeFailure(wt) {
1524
- return (wt.failure ?? {
1525
- error_kind: "WorktreeError",
1526
- error_message: wt.error,
1642
+ if (wt.failure)
1643
+ return wt.failure;
1644
+ const refused = wt.failureKind !== undefined && SETUP_REFUSAL_KINDS.has(wt.failureKind);
1645
+ const displayCode = worktreeDisplayReasonCode(wt.failureKind);
1646
+ return {
1647
+ error_kind: refused ? WorktreeSetupRefused : "WorktreeError",
1648
+ error_message: secretFreeWorktreeMessage(wt.error),
1527
1649
  classification: "crashed",
1528
- });
1650
+ ...(displayCode ? { telemetry: { failure_reason_code: displayCode } } : {}),
1651
+ };
1652
+ }
1653
+ /** The payload key the reconciler stamps with a fenced child's resume head (BAPI-1530). */
1654
+ const FENCED_RESUME_HEAD_KEY = "fenced_implement_resume_head_sha";
1655
+ /**
1656
+ * BAPI-1530: the SERVER-AUTHORED resume head from the job payload, or `null`.
1657
+ * Absent, malformed, or non-SHA values degrade to the ordinary guarded
1658
+ * fresh-dispatch path: absence proves nothing about the server's version and
1659
+ * triggers no renaming.
1660
+ */
1661
+ function readFencedResumeHead(payload) {
1662
+ if (!payload || typeof payload !== "object")
1663
+ return null;
1664
+ const raw = payload[FENCED_RESUME_HEAD_KEY];
1665
+ if (typeof raw !== "string")
1666
+ return null;
1667
+ const head = raw.trim().toLowerCase();
1668
+ return /^[0-9a-f]{40}$/.test(head) ? head : null;
1529
1669
  }
1530
1670
  /**
1531
1671
  * Prepare a job's worktree. A job the server dispatched as a parked-child
@@ -1775,6 +1915,9 @@ async function prepareSpawn(job, httpClient, options, deps, seams, preparation)
1775
1915
  // `implement` retry acts on it; `ensureExecutorWorktree` re-checks that.
1776
1916
  // Spread only when present so an ordinary job keeps its exact legacy policy.
1777
1917
  ...(job.prior_attempt_head_sha ? { priorAttemptHeadSha: job.prior_attempt_head_sha } : {}),
1918
+ // BAPI-1530: forwarded separately from the claim's prior-attempt head so
1919
+ // same-row retry preservation keeps its own semantics.
1920
+ ...(readFencedResumeHead(job.payload) ? { fencedImplementResumeHeadSha: readFencedResumeHead(job.payload) } : {}),
1778
1921
  });
1779
1922
  if (!wt.ok) {
1780
1923
  await httpClient.fail(job, jobWorktreeFailure(wt));
@@ -1971,9 +2114,11 @@ async function renderVerifiedCiFixPrompt(params) {
1971
2114
  workBranch: branch,
1972
2115
  baseBranch,
1973
2116
  runCommand: deps.runCommand,
1974
- comparisonSha: ciFix.headSha,
2117
+ comparisonSha: ciFix.failingHeadSha ?? ciFix.headSha,
2118
+ timeoutSeconds: params.timeoutSeconds,
2119
+ deferComparisonGuidance: true,
1975
2120
  });
1976
- return { ok: true, prompt };
2121
+ return { ok: true, prompt: prompt + ciFixProvenanceGuidance(ciFix) + renderCiFixOperatorNote(job.payload) };
1977
2122
  }
1978
2123
  catch (err) {
1979
2124
  return {
@@ -1982,8 +2127,57 @@ async function renderVerifiedCiFixPrompt(params) {
1982
2127
  };
1983
2128
  }
1984
2129
  }
2130
+ /**
2131
+ * BAPI-1514: the provenance line for a focused ci_fix prompt. It separates the
2132
+ * ORIGINAL failing head (`failingHeadSha`) from the head the supplied evidence
2133
+ * describes (`evidenceHeadSha` on a refreshed payload, else the legacy anchor),
2134
+ * and never calls the evidence stale when it describes the start head itself.
2135
+ */
2136
+ function ciFixProvenanceGuidance(ciFix) {
2137
+ const original = ciFix.failingHeadSha;
2138
+ if (ciFix.evidenceHeadSha) {
2139
+ const evidence = ciFix.evidenceHeadSha;
2140
+ if (!original || original === evidence) {
2141
+ return "\n\nSupplied CI logs and snapshots describe the evidence head " + evidence +
2142
+ ", the head this repair was dispatched against. Re-verify the affected behavior at the execution head.";
2143
+ }
2144
+ return "\n\nOriginal failing head " + original + "; evidence head " + evidence +
2145
+ "; the supplied CI logs and snapshots describe the evidence head, the head this repair was dispatched against." +
2146
+ " Inspect the diff from the original failing head for context and re-verify the affected behavior at the execution head.";
2147
+ }
2148
+ if (!original)
2149
+ return "";
2150
+ if (original === ciFix.headSha) {
2151
+ return "\n\nSupplied CI logs and snapshots describe the failing head " + original +
2152
+ ", the head this repair was dispatched against. Re-verify the affected behavior at the execution head.";
2153
+ }
2154
+ return "\n\nSupplied CI logs and snapshots describe the failing head " + original +
2155
+ " and may predate this pushed checkout. Inspect the diff from that anchor and re-verify the affected behavior at the execution head.";
2156
+ }
1985
2157
  /** BAPI-1311: cap on any one focused finalization read, before the deadline clamp. */
1986
2158
  const FOCUSED_FINALIZATION_READ_MAX_MS = 120_000;
2159
+ /** BAPI-1473: bound on the pre-launch origin-tip read for a focused rebase. */
2160
+ const FOCUSED_REBASE_START_TIP_READ_MAX_MS = 30_000;
2161
+ /**
2162
+ * BAPI-1473: the work branch's authoritative origin tip just before a focused
2163
+ * rebase worker launches, as a full lower-case SHA, or null when it cannot be
2164
+ * established. Never throws; null keeps the existing post-push wait.
2165
+ */
2166
+ async function readFocusedRebaseStartTip(deps, worktreePath, branch, remainingMs) {
2167
+ const timeoutMs = Math.min(FOCUSED_REBASE_START_TIP_READ_MAX_MS, remainingMs());
2168
+ if (timeoutMs <= 0)
2169
+ return null;
2170
+ try {
2171
+ const lookup = await resolveOriginBranchSha(deps.runCommand, worktreePath, branch, { timeoutMs });
2172
+ if (lookup.state !== "found")
2173
+ return null;
2174
+ const sha = lookup.sha.trim().toLowerCase();
2175
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
2176
+ }
2177
+ catch {
2178
+ return null;
2179
+ }
2180
+ }
1987
2181
  /**
1988
2182
  * BAPI-1311 — run the focused rebase's post-push wait and, unless it is `ready`,
1989
2183
  * report the job's ONE terminal outcome.
@@ -2057,6 +2251,7 @@ async function superviseFocusedRebaseWait(params) {
2057
2251
  expectedBaseBranch: params.expectedBaseBranch,
2058
2252
  readPullRequest: (timeoutMs) => readFocusedPullRequest(deps.runCommand, params.worktreePath, params.context.prNumber, { timeoutMs }),
2059
2253
  pollChecks,
2254
+ ...(params.readDelivery ? { readDelivery: params.readDelivery } : {}),
2060
2255
  now: deps.now,
2061
2256
  sleep: deps.sleep,
2062
2257
  signal,
@@ -2135,6 +2330,20 @@ async function superviseFocusedRebaseWait(params) {
2135
2330
  * returns BEFORE either the deny layer or MCP provisioning runs — neither ever
2136
2331
  * touches a worktree that will not become a runnable worker.
2137
2332
  */
2333
+ /**
2334
+ * BAPI-1453 — the single executor log line for a pre-spawn refusal: the job id,
2335
+ * the closed reason, and the bounded dirty-path evidence from the observation
2336
+ * snapshot. Paths are already repository-relative, length-bounded,
2337
+ * control-escaped, and redaction-checked by `parseDirtyPathEvidence`, and are
2338
+ * JSON-quoted here so the line stays one line.
2339
+ */
2340
+ export function formatPreSpawnRefusalLine(job, reason, telemetry) {
2341
+ const code = isFailureReasonCode(reason) ? reason : "unknown";
2342
+ const paths = Array.isArray(telemetry.dirty_paths) ? telemetry.dirty_paths : [];
2343
+ const overflow = typeof telemetry.dirty_paths_overflow_count === "number" ? telemetry.dirty_paths_overflow_count : 0;
2344
+ return (`pre-spawn verification refused job_id=${job.id} reason=${code} ` +
2345
+ `dirty_paths=${JSON.stringify(paths)} dirty_paths_overflow_count=${overflow}`);
2346
+ }
2138
2347
  /** Reserve the branch before Worktrunk can mutate it; lock an existing tree before preparation. */
2139
2348
  async function runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver) {
2140
2349
  const originalHttp = httpClient;
@@ -2221,20 +2430,37 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
2221
2430
  }
2222
2431
  };
2223
2432
  try {
2224
- return await runReservedSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver, undefined, undefined, reserve);
2433
+ return await runReservedSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver, undefined, undefined, reserve, timeout.timeoutSeconds);
2225
2434
  }
2226
2435
  finally {
2227
2436
  await retainedLock?.release();
2228
2437
  await reservation?.release();
2229
2438
  }
2230
2439
  }
2440
+ /**
2441
+ * The filesystem boundary for exclude provisioning, built from the runner's
2442
+ * injected deps. A deps builder that omits the optional `rename`/`removeFile`
2443
+ * seams gets the real operation, matching the worktree-lock fallback direction.
2444
+ */
2445
+ function excludeProvisioningDeps(deps) {
2446
+ return {
2447
+ readFile: deps.readFile,
2448
+ writeFile: deps.writeFile,
2449
+ mkdir: deps.mkdir,
2450
+ stat: deps.stat ?? ((filePath) => fsStat(filePath)),
2451
+ rename: deps.rename ?? ((from, to) => fsRename(from, to)),
2452
+ removeFile: deps.removeFile ?? ((filePath) => rm(filePath, { force: true })),
2453
+ };
2454
+ }
2231
2455
  async function runReservedSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control,
2232
2456
  /**
2233
2457
  * BAPI-1152: an optional spawn-job driver (`reconcile_overlap`). It runs inside
2234
2458
  * the SAME lock and the SAME prepared-spawn path as every other spawn job, so it
2235
2459
  * adds no second worktree entry and no second worker launcher.
2236
2460
  */
2237
- driver, retainedLock, preparation, reserve) {
2461
+ driver, retainedLock, preparation, reserve,
2462
+ /** Successful total budget resolved before preparation, never remaining time. */
2463
+ resolvedTimeoutSeconds) {
2238
2464
  // A server-owned read precedes every worktree reset/cut and worker launch.
2239
2465
  // Only this boundary can authorize a no-charge pre-work wait; a later plan
2240
2466
  // retrieval failure is ordinary bounded worker failure.
@@ -2290,9 +2516,7 @@ driver, retainedLock, preparation, reserve) {
2290
2516
  claude: {
2291
2517
  readFile: deps.readFile,
2292
2518
  denyProvisioningDeps: {
2293
- readFile: deps.readFile,
2294
- writeFile: deps.writeFile,
2295
- mkdir: deps.mkdir,
2519
+ ...excludeProvisioningDeps(deps),
2296
2520
  homedir: deps.homedir,
2297
2521
  },
2298
2522
  },
@@ -2377,6 +2601,18 @@ driver, retainedLock, preparation, reserve) {
2377
2601
  return { status: "failed", reason: "focused_rebase_context" };
2378
2602
  }
2379
2603
  }
2604
+ // BAPI-1451: provision the shared git exclude BEFORE any lock acquisition. The
2605
+ // retained-worktree lock (inside `reserve()`) and the fresh-worktree lock both
2606
+ // write `.conductor/` artifacts, and so do the recently-failed file and
2607
+ // pre-spawn verification. The executor clone's common `info/exclude` covers
2608
+ // both retained and newly prepared linked worktrees. A warning is logged and
2609
+ // the job continues unchanged: no failure reason, alert, cleanup, or early
2610
+ // return — dirty-worktree refusal stays with pre-spawn verification.
2611
+ {
2612
+ const provisioned = await (seams.provisionWorktreeSettingsExclude ?? provisionWorktreeSettingsExclude)(deps.cwd, excludeProvisioningDeps(deps));
2613
+ if (provisioned.warning)
2614
+ deps.errorLog(provisioned.warning);
2615
+ }
2380
2616
  // Acquire ownership only after read-only admission and contract validation.
2381
2617
  if (reserve) {
2382
2618
  const held = await reserve();
@@ -2505,6 +2741,7 @@ driver, retainedLock, preparation, reserve) {
2505
2741
  const rendered = await renderVerifiedCiFixPrompt({
2506
2742
  job,
2507
2743
  ciFix: prep.ciFix,
2744
+ timeoutSeconds: resolvedTimeoutSeconds,
2508
2745
  publication: prep.publication, preparation,
2509
2746
  worktreePath,
2510
2747
  branch,
@@ -2521,7 +2758,7 @@ driver, retainedLock, preparation, reserve) {
2521
2758
  readyPrep = { ...prep, prompt: rendered.prompt };
2522
2759
  }
2523
2760
  if (["implement", "resume", "remediate", "rebase", "ci_fix"].includes(job.job_type)) {
2524
- readyPrep = { ...readyPrep, comparisonSha: prep.ciFix?.headSha ??
2761
+ readyPrep = { ...readyPrep, comparisonSha: prep.ciFix?.failingHeadSha ?? prep.ciFix?.headSha ??
2525
2762
  await captureComparisonSha(worktreePath, deps) };
2526
2763
  if (!prep.ciFix && isRemainingWork(job.job_type, job.payload)) {
2527
2764
  if (job.job_type === "resume" || prep.priorWork) {
@@ -2568,6 +2805,9 @@ driver, retainedLock, preparation, reserve) {
2568
2805
  });
2569
2806
  return { status: "failed", reason: "stale_artifact_cleanup" };
2570
2807
  }
2808
+ // `.conductor/` is excluded through the executor-provisioned common
2809
+ // `info/exclude` (see the spawn-entry provisioning above), independently of
2810
+ // any repository `.gitignore`.
2571
2811
  try {
2572
2812
  await (seams.writeRecentlyFailedTestsFile ?? writeRecentlyFailedTestsFile)(worktreePath, job.payload?.recently_failed_tests);
2573
2813
  }
@@ -2615,10 +2855,167 @@ driver, retainedLock, preparation, reserve) {
2615
2855
  // touching it, and the two steps above are what establish that it cannot.
2616
2856
  spawnLifecycle.deregisterLiveWorker?.();
2617
2857
  spawnLifecycle.deregisterLiveWorker = undefined;
2858
+ // BAPI-1479 — the job database and launcher go AFTER the worker is
2859
+ // contained (supervision has settled by now) and BEFORE the worktree lock is
2860
+ // released. A drop failure is one bounded, non-fatal line on the executor's
2861
+ // stderr; it never changes the job's result.
2862
+ await spawnLifecycle.releaseWorkerResources?.().catch(() => undefined);
2618
2863
  await spawnLifecycle.closeWorkerLog?.();
2619
2864
  await worktreeLock.release();
2620
2865
  }
2621
2866
  }
2867
+ /**
2868
+ * BAPI-1479: the job types whose worker gets a disposable database and the
2869
+ * own-build launcher — the PR-producing worker jobs, which are the ones that run
2870
+ * the repository's tests. Verdict-only and driver-owned jobs keep the sentinel.
2871
+ */
2872
+ const WORKER_RESOURCE_JOB_TYPES = new Set([
2873
+ "implement", "resume", "remediate", "ci_fix", "rebase",
2874
+ ]);
2875
+ /**
2876
+ * BAPI-1479 — provision the job's disposable database and own-build launcher
2877
+ * before the worker's spawn shape is built.
2878
+ *
2879
+ * Runs under the SAME heartbeat, server-stop, and timeout supervision as a
2880
+ * driver phase, and an executor shutdown aborts it through the live-worker
2881
+ * registry (which is sticky, so a shutdown that already started still reaches
2882
+ * it). It never stops a job for a missing prerequisite: an unavailable database
2883
+ * or launcher logs ONE bounded non-fatal line and the worker spawns with the
2884
+ * sentinel database and the inherited `PATH`, exactly as before. Only the
2885
+ * existing interruptions end the job here — lease loss, a server stop, or the job
2886
+ * timeout — and they report through the existing outcomes, never a new code.
2887
+ *
2888
+ * Whatever was created is registered on `lifecycle.releaseWorkerResources` the
2889
+ * moment it exists, so `runSpawnJob`'s `finally` drops it on every exit path.
2890
+ */
2891
+ async function prepareWorkerResources(params) {
2892
+ const { job, deps, lifecycle } = params;
2893
+ const jobId = String(job.id);
2894
+ const resources = deps.workerResources;
2895
+ if (!resources)
2896
+ return { kind: "ready" };
2897
+ let target;
2898
+ let disposeLauncher;
2899
+ let launcherDirectory;
2900
+ let released = false;
2901
+ lifecycle.releaseWorkerResources = async () => {
2902
+ if (released)
2903
+ return;
2904
+ released = true;
2905
+ if (target) {
2906
+ const dropped = await resources.dropDatabase(target).catch(() => ({ kind: "failed" }));
2907
+ if (dropped.kind !== "dropped") {
2908
+ try {
2909
+ deps.errorLog(formatWorkerDatabaseDropFailedLine(jobId, target.name));
2910
+ }
2911
+ catch { /* sink */ }
2912
+ }
2913
+ }
2914
+ if (disposeLauncher)
2915
+ await disposeLauncher().catch(() => undefined);
2916
+ };
2917
+ // An executor shutdown aborts provisioning the same way it terminates a worker.
2918
+ const shutdown = new AbortController();
2919
+ const deregister = params.control?.liveWorkers.register({
2920
+ requestTermination: () => shutdown.abort(),
2921
+ });
2922
+ let phase;
2923
+ try {
2924
+ phase = await superviseDriverPhase({
2925
+ job,
2926
+ httpClient: params.httpClient,
2927
+ options: params.options,
2928
+ deps,
2929
+ ownership: params.ownership,
2930
+ observation: params.observation,
2931
+ control: params.control,
2932
+ timeoutSeconds: Math.max(1, Math.ceil(params.remainingMs() / 1000)),
2933
+ collectTelemetry: params.collectTelemetry,
2934
+ run: async (signal) => {
2935
+ // Combined by hand: `AbortSignal.any` is not available on Node 18.
2936
+ const combined = new AbortController();
2937
+ const abortCombined = () => combined.abort();
2938
+ if (signal.aborted || shutdown.signal.aborted)
2939
+ combined.abort();
2940
+ signal.addEventListener("abort", abortCombined, { once: true });
2941
+ shutdown.signal.addEventListener("abort", abortCombined, { once: true });
2942
+ const cancel = combined.signal;
2943
+ const provisioned = await resources
2944
+ .provisionDatabase({ jobId, worktreePath: params.worktreePath, signal: cancel })
2945
+ .catch(() => ({ kind: "unavailable", category: "create_failed" }));
2946
+ if (provisioned.kind === "provisioned") {
2947
+ target = provisioned.target;
2948
+ }
2949
+ else {
2950
+ try {
2951
+ deps.errorLog(formatWorkerDatabaseUnavailableLine(jobId, provisioned.category));
2952
+ // A created database whose rollback failed stays for the host operator.
2953
+ if ("retainedName" in provisioned && provisioned.retainedName) {
2954
+ deps.errorLog(formatWorkerDatabaseDropFailedLine(jobId, provisioned.retainedName));
2955
+ }
2956
+ }
2957
+ catch { /* sink */ }
2958
+ }
2959
+ if (cancel.aborted)
2960
+ return;
2961
+ const launcher = await resources
2962
+ .createLauncher({ jobId })
2963
+ .catch(() => ({ kind: "unavailable", reason: "wrapper_unverified" }));
2964
+ if (launcher.kind === "available") {
2965
+ disposeLauncher = launcher.dispose;
2966
+ if (!released)
2967
+ launcherDirectory = launcher.directory;
2968
+ else
2969
+ await launcher.dispose().catch(() => undefined);
2970
+ }
2971
+ else {
2972
+ try {
2973
+ deps.errorLog(formatWorkerCliLauncherUnavailableLine(jobId, launcher.reason));
2974
+ }
2975
+ catch { /* sink */ }
2976
+ }
2977
+ },
2978
+ });
2979
+ }
2980
+ finally {
2981
+ deregister?.();
2982
+ }
2983
+ if (phase.kind === "abandoned") {
2984
+ return { kind: "interrupted", result: { status: "abandoned", reason: params.ownership.abandonReason } };
2985
+ }
2986
+ if (phase.kind === "server_stop") {
2987
+ await params.httpClient.fail(job, {
2988
+ error_kind: "ServerStopRequested",
2989
+ error_message: "the server requested this run stop before the worker started",
2990
+ classification: "killed",
2991
+ telemetry: params.observation.snapshot(),
2992
+ });
2993
+ params.ownership.abandoned = true;
2994
+ params.ownership.abandonReason = "server_stop";
2995
+ return { kind: "interrupted", result: { status: "abandoned", reason: "server_stop" } };
2996
+ }
2997
+ if (phase.kind === "timeout") {
2998
+ await params.httpClient.fail(job, {
2999
+ error_kind: "Timeout",
3000
+ error_message: `${job.job_type} exceeded the job timeout before the worker started`,
3001
+ classification: "timeout",
3002
+ telemetry: params.observation.snapshot(),
3003
+ });
3004
+ return { kind: "interrupted", result: { status: "failed", reason: "timeout" } };
3005
+ }
3006
+ // An executor shutdown during provisioning: spawn nothing new with a
3007
+ // half-prepared target. The sticky registry terminates the worker on
3008
+ // registration anyway, so continuing with the sentinel keeps the existing
3009
+ // shutdown semantics while the database (if any) is dropped by the `finally`.
3010
+ if (shutdown.signal.aborted)
3011
+ return { kind: "ready" };
3012
+ // `kind: "error"` is an unexpected throw from the seam itself: degrade.
3013
+ return {
3014
+ kind: "ready",
3015
+ ...(target ? { workerDatabase: target } : {}),
3016
+ ...(launcherDirectory ? { workerLauncherDirectory: launcherDirectory } : {}),
3017
+ };
3018
+ }
2622
3019
  function retainedHeadPublisher(params) {
2623
3020
  return async () => {
2624
3021
  const { job, branch, worktreePath, prep, deps, ownership, observation, remainingMs } = params;
@@ -2700,7 +3097,8 @@ async function runPreparedSpawn(params) {
2700
3097
  // an envelope, rather than run under the legacy contract that the server
2701
3098
  // would then refuse to accept.
2702
3099
  const covered = claimIsCovered(job);
2703
- if (covered && (!isCoveredJobType(job.job_type) || driver)) {
3100
+ const policyRebase = job.job_type === "rebase" && job.required_capabilities?.includes("required_verification.ci_deferral.v2");
3101
+ if (covered && ((!isCoveredJobType(job.job_type) && !policyRebase) || driver)) {
2704
3102
  return await refuseCoveredClaimBeforeLaunch({
2705
3103
  job, httpClient, options, deps, ownership, observation, control,
2706
3104
  runtime: unrecognizedRuntime(adapter),
@@ -2797,6 +3195,15 @@ async function runPreparedSpawn(params) {
2797
3195
  if (job.job_type === "implement" && !isRemainingWork(job.job_type, job.payload)) {
2798
3196
  prompt += "\n\n" + renderWorkerVerificationGuidance(prep.comparisonSha);
2799
3197
  }
3198
+ // The locked ci_fix renderer deferred trusted guidance. All existing prompt
3199
+ // additions precede comparison guidance and the final total-budget block.
3200
+ if (prep.ciFix) {
3201
+ prompt += "\n\n" + renderWorkerVerificationGuidance(prep.comparisonSha) +
3202
+ "\n\n" + renderWorkerTimeBudgetGuidance(timeout.timeoutSeconds);
3203
+ }
3204
+ const approvedVerificationGuidance = renderApprovedVerificationGuidance(job.payload);
3205
+ if (approvedVerificationGuidance)
3206
+ prompt += "\n\n" + approvedVerificationGuidance;
2800
3207
  // --- Pre-spawn git verification (FAIL-CLOSED, BAPI-731 WS-E) ---------
2801
3208
  // FIRST of the pre-spawn contracts, and deliberately BEFORE every
2802
3209
  // executor-owned write to the worktree (deny layer, MCP provisioning, worker
@@ -2809,7 +3216,9 @@ async function runPreparedSpawn(params) {
2809
3216
  // failing check. It does NOT replace or weaken the existing hard-reset /
2810
3217
  // untracked-clean / head-verify behavior in `worktree-core.ts`; it is an
2811
3218
  // additional independent check on top of them.
2812
- const preSpawnTelemetry = await collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch);
3219
+ const preSpawnTelemetry = await collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch,
3220
+ // BAPI-1453: a refusal must name the files it found.
3221
+ { includeDirtyPaths: true });
2813
3222
  // BAPI-1152: a driver proves its own pre-spawn state (a reconciliation worker
2814
3223
  // may start inside an in-progress merge, which the generic "clean at the
2815
3224
  // prepared commit" check would refuse by design).
@@ -2827,12 +3236,28 @@ async function runPreparedSpawn(params) {
2827
3236
  observation.setAttemptEndSha(verification.observedSha);
2828
3237
  }
2829
3238
  observation.setGitTelemetry(preSpawnTelemetry);
3239
+ // BAPI-1453: one executor log line naming the job, the reason, and the
3240
+ // bounded dirty-path evidence. The paths come from the SAME observation
3241
+ // snapshot the `/fail` body carries, never from a second copy, and never
3242
+ // reach `error_message` (fixed strings and validated SHAs only).
3243
+ const refusalTelemetry = observation.snapshot();
3244
+ try {
3245
+ deps.errorLog(formatPreSpawnRefusalLine(job, verification.reason, refusalTelemetry));
3246
+ }
3247
+ catch {
3248
+ /* supplemental logging cannot change the refusal */
3249
+ }
2830
3250
  await httpClient.fail(job, {
2831
3251
  error_kind: PreSpawnVerification,
2832
3252
  error_message: verification.message,
2833
3253
  classification: "crashed",
2834
3254
  ...commitResidue(preSpawnTelemetry),
2835
- telemetry: observation.snapshot(),
3255
+ // Builder-owned code first; the spawn-path wrapper then layers the
3256
+ // snapshot over it, so both survive.
3257
+ telemetry: {
3258
+ ...(isFailureReasonCode(verification.reason) ? { failure_reason_code: verification.reason } : {}),
3259
+ ...refusalTelemetry,
3260
+ },
2836
3261
  });
2837
3262
  return { status: "failed", reason: "pre_spawn_verification" };
2838
3263
  }
@@ -3141,6 +3566,29 @@ async function runPreparedSpawn(params) {
3141
3566
  // is denied at the env builder and cannot reach `spawnShape.env`. It stays out
3142
3567
  // of `argv` entirely: it is a routing declaration, not a command-line input,
3143
3568
  // and argv is world-readable via `ps`.
3569
+ // --- Worker resources (BAPI-1479) --------------------------------------
3570
+ // The job's disposable database and own-build launcher, prepared ONCE for this
3571
+ // prepared job, before the one spawn shape both the covered and legacy launches
3572
+ // use. A retry of the job is a fresh claim with a fresh execution, so it gets
3573
+ // fresh resources; nothing is recovered from an earlier execution's state.
3574
+ // Driver-owned jobs (their worker runs a server-rendered reconciliation) and
3575
+ // verdict-only jobs keep the sentinel database.
3576
+ let workerResources = {};
3577
+ if (!driver && WORKER_RESOURCE_JOB_TYPES.has(job.job_type)) {
3578
+ const prepared = await prepareWorkerResources({
3579
+ job, httpClient, options: jobOptions, deps, ownership, observation, control,
3580
+ worktreePath, remainingMs, lifecycle, collectTelemetry: driverTelemetry,
3581
+ });
3582
+ if (prepared.kind === "interrupted") {
3583
+ await closeWorkerLog();
3584
+ await finalizeRegistry();
3585
+ return prepared.result;
3586
+ }
3587
+ workerResources = {
3588
+ ...(prepared.workerDatabase ? { workerDatabase: prepared.workerDatabase } : {}),
3589
+ ...(prepared.workerLauncherDirectory ? { workerLauncherDirectory: prepared.workerLauncherDirectory } : {}),
3590
+ };
3591
+ }
3144
3592
  const invocation = adapter.headlessInvocation;
3145
3593
  if (invocation?.supported !== true) {
3146
3594
  await closeWorkerLog();
@@ -3175,6 +3623,8 @@ async function runPreparedSpawn(params) {
3175
3623
  ...(driver?.omitWorkerBranch ? {} : { workerBranch: branch }),
3176
3624
  ...(correctionWork ? { correctionIntentVersion: 1 } : {}),
3177
3625
  ...(driverStep ? { declaredTouchedFiles: driverStep.declaredTouchedFiles } : {}),
3626
+ // BAPI-1479 — explicit, executor-provisioned values; never read from `parentEnv`.
3627
+ ...workerResources,
3178
3628
  parentEnv: deps.env,
3179
3629
  });
3180
3630
  assertUsableSpawnShape(spawnShape, adapter);
@@ -3309,6 +3759,15 @@ async function runPreparedSpawn(params) {
3309
3759
  // the covered attempt launch through it, so there is still exactly one place a
3310
3760
  // worker can be started, always from the verified spawn shape. Only a covered
3311
3761
  // attempt asks for a held stdin; the legacy options stay byte-identical.
3762
+ // BAPI-1473: a focused rebase records the work branch's ORIGIN tip immediately
3763
+ // before its worker launches. When finalization later proves the origin tip is
3764
+ // still exactly this SHA, the worker pushed nothing (it only renewed evidence),
3765
+ // so there is no post-push evidence to wait for. Read with the same
3766
+ // authoritative `git ls-remote` finalization uses, never a local tracking ref
3767
+ // and never `job.expected_head_sha`. Unknown (null) keeps the existing wait.
3768
+ const focusedRebaseStartTip = prep.focusedRebase
3769
+ ? await readFocusedRebaseStartTip(deps, worktreePath, branch, remainingMs)
3770
+ : null;
3312
3771
  const spawnWorker = (spawnOptions) => deps.spawnProcess(spawnShape.executable, spawnShape.argv, {
3313
3772
  cwd: worktreePath,
3314
3773
  env: spawnOptions.env,
@@ -3342,11 +3801,29 @@ async function runPreparedSpawn(params) {
3342
3801
  if (!spawnShape.stdinMessages?.initial) {
3343
3802
  return await refuse("unsupported_runtime", "the executor adapter produced no held-stdin prompt message for a covered job");
3344
3803
  }
3804
+ // BAPI-1504 — a job stamped with `cleanup_report.v1` is only safe to run on a
3805
+ // client that can deliver the report; refusing here, before any worker exists,
3806
+ // names the missing capability in the envelope instead of launching a job whose
3807
+ // interruption could never be proven clean.
3808
+ if (claimRequiresCleanupReport(job.required_capabilities) && typeof httpClient.reportCleanup !== "function") {
3809
+ await closeWorkerLog();
3810
+ await finalizeRegistry();
3811
+ return await refuseCoveredClaimBeforeLaunch({
3812
+ job, httpClient, options, deps, ownership, observation, control,
3813
+ runtime, dispatchHeadSha, reason: "unsupported_runtime",
3814
+ message: "this executor cannot deliver cleanup reports (cleanup_report.v1); update the executor",
3815
+ missingCapabilities: ["cleanup_report"],
3816
+ });
3817
+ }
3345
3818
  // BAPI-1302 (D4c) — the server-stamped declaration is validated WHOLE,
3346
3819
  // before any allowance is reserved or a worker exists. A truncated,
3347
3820
  // malformed, unsupported or server-refused declaration never launches work
3348
3821
  // against a partial set of checks.
3349
3822
  const verificationRead = readRequiredVerification(job.payload);
3823
+ if (verificationRead.kind === "ok" && verificationRead.declaration.policyVersion === 2 &&
3824
+ !job.required_capabilities?.includes("required_verification.ci_deferral.v2")) {
3825
+ return await refuse("declaration_refused", "CI-deferral capability was not negotiated; refusing to launch");
3826
+ }
3350
3827
  if (verificationRead.kind === "refused") {
3351
3828
  deps.errorLog(`[executor] job ${job.id}: required-verification declaration refused (${verificationRead.reason})`);
3352
3829
  return await refuse("declaration_refused", "the job's required-verification declaration is unusable; refusing to launch");
@@ -3761,11 +4238,24 @@ async function runPreparedSpawn(params) {
3761
4238
  // its push never landed on origin and no PR was opened (e.g. it backgrounded
3762
4239
  // a slow push and returned early). Convert that into a fail-loud executor
3763
4240
  // failure rather than silently reporting success.
4241
+ //
4242
+ // BAPI-1426: a managed ticket's server-confirmed delivery outcome is read
4243
+ // first, so an intentional held checkpoint finalizes as a `/fail` with its
4244
+ // completion cause instead of a missing-PR failure or a success.
4245
+ const deliveryOutcome = await readDeliveryOutcomeForFinalization({
4246
+ job,
4247
+ branch,
4248
+ deps,
4249
+ seams,
4250
+ remainingMs,
4251
+ resolvedBaseUrl: options.resolvedBaseUrl,
4252
+ });
3764
4253
  const finalization = await validateWorkerFinalization({
3765
4254
  job,
3766
4255
  branch,
3767
4256
  worktreePath,
3768
4257
  result: genericResult,
4258
+ ...(deliveryOutcome !== undefined ? { deliveryOutcome } : {}),
3769
4259
  publishRetainedHead: retainedHeadPublisher({ job, branch, worktreePath, prep, deps, ownership, observation, remainingMs, httpClient, options: jobOptions, control }),
3770
4260
  runCommand: deps.runCommand,
3771
4261
  headSha: git.last_commit_sha,
@@ -3791,7 +4281,7 @@ async function runPreparedSpawn(params) {
3791
4281
  // finalization failure; it is never rewritten to look like a crash.
3792
4282
  ...observedExitCode(procResult.exitCode),
3793
4283
  ...commitResidue(git),
3794
- telemetry: observation.snapshot(),
4284
+ telemetry: withFailureTelemetry(observation.snapshot(), finalization.failure.telemetry),
3795
4285
  }),
3796
4286
  deps,
3797
4287
  options,
@@ -3808,11 +4298,38 @@ async function runPreparedSpawn(params) {
3808
4298
  // supervision a worker gets, bounded by the job's ORIGINAL absolute deadline.
3809
4299
  // It launches no worker, enqueues nothing, and resets nothing: a pending check
3810
4300
  // is waited on, never repaired again. Only `ready` reaches `/complete`, once.
3811
- if (prep.focusedRebase) {
4301
+ //
4302
+ // BAPI-1473: two narrower successful ends, both still submitting `/complete`
4303
+ // exactly once below and leaving the server's completion contract and
4304
+ // delivery guard as the authority:
4305
+ // - NO PUSH: the verified origin tip equals the tip recorded before launch,
4306
+ // so the worker only renewed evidence. Skip the post-push wait.
4307
+ // - DEFERRED CI: the wait itself ends `ready` while required checks are still
4308
+ // pending when the server's readiness at the pushed head shows renewed
4309
+ // evidence with only `remote_ci` deferrals (`readDelivery` below).
4310
+ // Neither is merge behavior: the merge guard keeps the child in `merging`
4311
+ // until readiness is `complete`, which needs green checks at the head.
4312
+ const focusedRebasePushedNothing = prep.focusedRebase !== undefined &&
4313
+ focusedRebaseStartTip !== null &&
4314
+ typeof finalization.pushedHeadSha === "string" &&
4315
+ finalization.pushedHeadSha.trim().toLowerCase() === focusedRebaseStartTip;
4316
+ if (focusedRebasePushedNothing) {
4317
+ deps.errorLog(`[executor] job ${job.id}: focused rebase pushed nothing (origin tip unchanged); ` +
4318
+ "skipping the post-push wait and submitting completion once for the server to decide");
4319
+ }
4320
+ if (prep.focusedRebase && !focusedRebasePushedNothing) {
3812
4321
  const focusedTerminal = await superviseFocusedRebaseWait({
3813
4322
  job, httpClient, options, jobOptions, deps, ownership, observation, seams, control,
3814
4323
  context: prep.focusedRebase,
3815
4324
  pushedHeadSha: finalization.pushedHeadSha ?? null,
4325
+ readDelivery: () => readDeliveryOutcomeForFinalization({
4326
+ job,
4327
+ branch,
4328
+ deps,
4329
+ seams,
4330
+ remainingMs,
4331
+ resolvedBaseUrl: options.resolvedBaseUrl,
4332
+ }),
3816
4333
  expectedBaseBranch: effectiveBaseBranch,
3817
4334
  worktreePath,
3818
4335
  jobDeadlineMs,
@@ -4015,6 +4532,7 @@ export function unlaunchedVerification(read) {
4015
4532
  const facts = {
4016
4533
  provenance,
4017
4534
  declaration_digest: digest,
4535
+ ...(read.kind === "ok" && read.declaration.policyVersion === 2 ? { deferred_obligation_identities: read.declaration.deferredIdentities ?? [] } : {}),
4018
4536
  tested_head_sha: null,
4019
4537
  artifact: "not_observed",
4020
4538
  commands: [],
@@ -4073,7 +4591,7 @@ async function refuseCoveredClaimBeforeLaunch(input) {
4073
4591
  finalization: "unknown",
4074
4592
  verification: unlaunched.verification,
4075
4593
  cleanup: buildCleanupVerdict("confirmed", 0, []),
4076
- missingCapabilities: [],
4594
+ missingCapabilities: input.missingCapabilities ?? [],
4077
4595
  ...(unlaunched.facts ? { verificationFacts: unlaunched.facts } : {}),
4078
4596
  });
4079
4597
  const cause = envelope.cause ?? "required_work_incomplete";
@@ -4159,6 +4677,90 @@ async function runCoveredAttempt(params) {
4159
4677
  optionalWatchers: readOptionalWatchers(job.payload),
4160
4678
  obligations,
4161
4679
  });
4680
+ // --- Ownership persisted BEFORE the spawn (BAPI-1504) ---------------------------
4681
+ // A job stamped with `cleanup_report.v1` stores `NULL` ownership only if no worker
4682
+ // was ever spawned, which lets the server read a null as proof of that. So the
4683
+ // server must hold this attempt's ownership (token digest, `worker: null`) before
4684
+ // the worker exists. The local record above was written first.
4685
+ const requiresCleanupReport = claimRequiresCleanupReport(job.required_capabilities);
4686
+ if (requiresCleanupReport) {
4687
+ const abandonBeforeLaunch = async (result) => {
4688
+ await closeWorkerLog();
4689
+ await finalizeRegistry();
4690
+ await removeOwnershipRecord(stateDir, job.id, recordFs).catch(() => undefined);
4691
+ return result;
4692
+ };
4693
+ const notLaunched = async (message) => {
4694
+ await closeWorkerLog();
4695
+ await finalizeRegistry();
4696
+ await removeOwnershipRecord(stateDir, job.id, recordFs).catch(() => undefined);
4697
+ return await refuseCoveredClaimBeforeLaunch({
4698
+ job, httpClient, options, deps, ownership, observation, control,
4699
+ runtime: params.runtime, dispatchHeadSha: params.dispatchHeadSha,
4700
+ reason: "not_launched", message,
4701
+ });
4702
+ };
4703
+ const preSpawnContext = {
4704
+ version: 1,
4705
+ token_digest: tokenDigest,
4706
+ host_digest: hostDigest,
4707
+ worker: null,
4708
+ };
4709
+ // Transient failures retry only inside the existing dead-man window; the window
4710
+ // is measured from the last successful beat and is never extended by a retry.
4711
+ for (;;) {
4712
+ let beat;
4713
+ try {
4714
+ beat = await sendImmediateHeartbeat({
4715
+ job,
4716
+ httpClient,
4717
+ deps,
4718
+ ownership,
4719
+ observation,
4720
+ collectTelemetry: () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch),
4721
+ ownershipContext: preSpawnContext,
4722
+ });
4723
+ }
4724
+ catch {
4725
+ beat = { kind: "transient" };
4726
+ }
4727
+ if (beat.kind === "accepted")
4728
+ break;
4729
+ if (beat.kind === "stale_claim") {
4730
+ return await abandonBeforeLaunch({ status: "abandoned", reason: "stale_claim" });
4731
+ }
4732
+ if (beat.kind === "server_stop") {
4733
+ // Report first, then mark the claim abandoned, exactly as the post-spawn
4734
+ // server-stop path does: an abandoned claim skips the terminal report.
4735
+ const stopped = await notLaunched("the server requested this run stop before the worker launched");
4736
+ ownership.abandoned = true;
4737
+ ownership.abandonReason = "server_stop";
4738
+ return stopped.status === "failed" ? { status: "abandoned", reason: "server_stop" } : stopped;
4739
+ }
4740
+ if (beat.kind === "auth_fatal") {
4741
+ control?.reportAuthFatal?.({
4742
+ jobId: job.id,
4743
+ repoName: job.repo_name,
4744
+ category: "heartbeat",
4745
+ httpStatus: beat.httpStatus,
4746
+ });
4747
+ return await abandonBeforeLaunch({ status: "abandoned", reason: "auth_fatal" });
4748
+ }
4749
+ // transient
4750
+ if (deps.now() - ownership.lastSuccessfulHeartbeatAt >= options.deadmanMs) {
4751
+ return await notLaunched("ownership could not be recorded with the server before launch; not launching the worker");
4752
+ }
4753
+ await deps.sleep(Math.min(jobOptions.heartbeatIntervalMs, 5_000));
4754
+ }
4755
+ // The claim, the run, and the executor can all change while the beat was in
4756
+ // flight; check once more immediately before the spawn.
4757
+ if (ownership.abandoned) {
4758
+ return await abandonBeforeLaunch({ status: "abandoned", reason: ownership.abandonReason });
4759
+ }
4760
+ if (control?.liveWorkers.isShutdownRequested() === true) {
4761
+ return await notLaunched("the executor is shutting down; not launching the worker");
4762
+ }
4763
+ }
4162
4764
  // --- Spawn (held stdin) ---------------------------------------------------
4163
4765
  let proc;
4164
4766
  try {
@@ -4451,7 +5053,7 @@ async function runCoveredAttempt(params) {
4451
5053
  send: () => httpClient.fail(job, {
4452
5054
  ...failure,
4453
5055
  ...commitResidue(git),
4454
- telemetry: observation.snapshot(),
5056
+ telemetry: withFailureTelemetry(observation.snapshot(), failure.telemetry),
4455
5057
  completion_contract: SUPPORTED_COMPLETION_MARKER,
4456
5058
  completion_evidence: envelope,
4457
5059
  ...extra,
@@ -4467,6 +5069,53 @@ async function runCoveredAttempt(params) {
4467
5069
  // The terminal report is acknowledged (or ownership is gone): only now does
4468
5070
  // the job-level heartbeat end, and only now is the restart record retired.
4469
5071
  await endHeartbeat();
5072
+ // BAPI-1504 — a lost claim after a CONFIRMED sweep: deliver the cleanup fact
5073
+ // before the record can be removed. The first attempt is awaited (bounded by
5074
+ // the HTTP client); a retained outcome continues in the background and the
5075
+ // record stays until a terminal server answer.
5076
+ if (requiresCleanupReport &&
5077
+ claimIsCovered(job) &&
5078
+ result.status === "abandoned" &&
5079
+ (result.reason === "stale_claim" || result.reason === "deadman") &&
5080
+ cleanup.verdict === "confirmed" &&
5081
+ cleanup.survivors === 0) {
5082
+ const first = await submitCleanupReport({
5083
+ httpClient, jobId: job.id, repoName: job.repo_name, ownershipToken: token, path: "wake",
5084
+ });
5085
+ if (cleanupReportPermitsRecordRemoval(first)) {
5086
+ await removeOwnershipRecord(stateDir, job.id, recordFs).catch(() => undefined);
5087
+ }
5088
+ else if (first.kind === "retained" && first.retryable) {
5089
+ const retry = retryWakeCleanupReport({
5090
+ httpClient,
5091
+ jobId: job.id,
5092
+ repoName: job.repo_name,
5093
+ ownershipToken: token,
5094
+ firstResult: first,
5095
+ intervalMs: jobOptions.heartbeatIntervalMs,
5096
+ shouldStop: () => control?.liveWorkers.isShutdownRequested() === true,
5097
+ deps: {
5098
+ now: deps.now,
5099
+ sleep: deps.sleep,
5100
+ diagnostic: (d) => deps.errorLog(`[executor] ${d.event} job=${d.jobId}`),
5101
+ },
5102
+ onTerminal: () => removeOwnershipRecord(stateDir, job.id, recordFs).catch(() => undefined),
5103
+ });
5104
+ if (control?.trackBackground)
5105
+ control.trackBackground(retry);
5106
+ else
5107
+ void retry;
5108
+ }
5109
+ // Any other retained outcome keeps the record for the next start.
5110
+ return result;
5111
+ }
5112
+ // BAPI-1504 — an abandoned covered attempt whose sweep did NOT confirm keeps
5113
+ // its record, so the next start can sweep again and report only then.
5114
+ if (requiresCleanupReport &&
5115
+ result.status === "abandoned" &&
5116
+ (result.reason === "stale_claim" || result.reason === "deadman")) {
5117
+ return result;
5118
+ }
4470
5119
  await removeOwnershipRecord(stateDir, job.id, recordFs).catch(() => undefined);
4471
5120
  return result;
4472
5121
  };
@@ -4552,6 +5201,7 @@ async function runCoveredAttempt(params) {
4552
5201
  verificationFacts = {
4553
5202
  provenance: declaration.provenance,
4554
5203
  declaration_digest: declaration.digest,
5204
+ ...(declaration.policyVersion === 2 ? { deferred_obligation_identities: declaration.deferredIdentities ?? [] } : {}),
4555
5205
  tested_head_sha: null,
4556
5206
  artifact: "not_observed",
4557
5207
  commands: [],
@@ -4631,6 +5281,7 @@ async function runCoveredAttempt(params) {
4631
5281
  verificationFacts = {
4632
5282
  provenance: "declared",
4633
5283
  declaration_digest: declaration.digest,
5284
+ ...(declaration.policyVersion === 2 ? { deferred_obligation_identities: declaration.deferredIdentities ?? [] } : {}),
4634
5285
  tested_head_sha: before.head,
4635
5286
  artifact,
4636
5287
  commands: declaredOrderFacts(checks, runFacts).map(toCompletionCheckFact),
@@ -4657,11 +5308,22 @@ async function runCoveredAttempt(params) {
4657
5308
  ? { ...ciResult.fields, ...executorResult, [CI_FIX_CLAIMS_STATUS_KEY]: ciResult.status }
4658
5309
  : { ...executorResult, [CI_FIX_CLAIMS_STATUS_KEY]: ciResult.status };
4659
5310
  }
5311
+ // BAPI-1426: the server-confirmed delivery outcome, read under the same
5312
+ // absolute deadline as the finalization reads below.
5313
+ const deliveryOutcome = await readDeliveryOutcomeForFinalization({
5314
+ job,
5315
+ branch,
5316
+ deps,
5317
+ seams,
5318
+ remainingMs: () => Math.max(0, plan.deadlineAtMs - deps.now()),
5319
+ resolvedBaseUrl: options.resolvedBaseUrl,
5320
+ });
4660
5321
  const finalization = await validateWorkerFinalization({
4661
5322
  job,
4662
5323
  branch,
4663
5324
  worktreePath,
4664
5325
  result: genericResult,
5326
+ ...(deliveryOutcome !== undefined ? { deliveryOutcome } : {}),
4665
5327
  publishRetainedHead: retainedHeadPublisher({ job, branch, worktreePath, prep, deps, ownership, observation, httpClient, options: jobOptions, control,
4666
5328
  remainingMs: () => Math.max(0, plan.deadlineAtMs - deps.now()) }),
4667
5329
  callTimeoutMs: () => Math.min(120_000, Math.max(0, plan.deadlineAtMs - deps.now())),
@@ -4672,8 +5334,17 @@ async function runCoveredAttempt(params) {
4672
5334
  // the existence-only legacy fallback is refused here.
4673
5335
  coveredAttempt: true,
4674
5336
  });
4675
- const facts = finalizationFacts(finalization.ok ? { ok: true } : { ok: false, errorKind: finalization.failure.error_kind });
4676
- const envelope = envelopeWith({
5337
+ // BAPI-1426: a server-confirmed delivery hold proved the push (origin equals
5338
+ // the worker HEAD and the server's preserved head) and deliberately left the
5339
+ // pull request unmet. Its completion cause is set on the envelope so the
5340
+ // envelope, `successRefusalReason` and the `/fail` error kind all agree. A
5341
+ // stronger cause the envelope already derived (cleanup, a locally failed
5342
+ // check, unfinished tasks) is kept.
5343
+ const deliveryHold = finalization.ok ? null : deliveryHoldOf(finalization.failure);
5344
+ const facts = deliveryHold
5345
+ ? { verdict: "unmet", pushSatisfied: true, prSatisfied: false }
5346
+ : finalizationFacts(finalization.ok ? { ok: true } : { ok: false, errorKind: finalization.failure.error_kind });
5347
+ const derivedEnvelope = envelopeWith({
4677
5348
  finalization: facts.verdict,
4678
5349
  verification,
4679
5350
  missing: [],
@@ -4681,6 +5352,14 @@ async function runCoveredAttempt(params) {
4681
5352
  prSatisfied: facts.prSatisfied,
4682
5353
  ...(verificationFacts ? { verificationFacts } : {}),
4683
5354
  });
5355
+ const deliveryHoldCause = deliveryHold
5356
+ ? deliveryHold.outcome === "verification_failed"
5357
+ ? "required_check_failed"
5358
+ : "required_work_incomplete"
5359
+ : null;
5360
+ const envelope = deliveryHoldCause !== null && derivedEnvelope.cause === null
5361
+ ? { ...derivedEnvelope, cause: deliveryHoldCause }
5362
+ : derivedEnvelope;
4684
5363
  // --- Phase 5: terminal report -----------------------------------------------------
4685
5364
  const lateStronger = await strongerOutcome();
4686
5365
  if (lateStronger)
@@ -4723,6 +5402,11 @@ async function runCoveredAttempt(params) {
4723
5402
  ...observedExitCode(procResult.exitCode),
4724
5403
  };
4725
5404
  const cause = envelope.cause;
5405
+ if (cause && !finalization.ok && deliveryHold && cause === deliveryHoldCause) {
5406
+ // BAPI-1426: the intentional delivery hold keeps its own bounded message and
5407
+ // hold marker; its error kind already equals the envelope's cause.
5408
+ return finish(await failWith({ ...finalization.failure, error_kind: COMPLETION_CAUSE_ERROR_KINDS[cause], ...observedExit }, envelope));
5409
+ }
4726
5410
  if (cause) {
4727
5411
  return finish(await failWith({
4728
5412
  error_kind: COMPLETION_CAUSE_ERROR_KINDS[cause],