@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
@@ -38,12 +38,17 @@
38
38
  * Filesystem only: no MCP transport, no network, no git, no subprocesses, and no
39
39
  * process-level state. The caller supplies the trusted docs directory and the
40
40
  * already-fetched plan metadata.
41
+ *
42
+ * BAPI-1426: post-PR ownership comes from a server-accepted ledger the adapter
43
+ * fetched and passed in as `acceptedLedger`; this module never fetches it. Stored
44
+ * post-PR records stay readable when their step was authorized at the time they
45
+ * were written, even if today's accepted state has moved on.
41
46
  */
42
47
  import { createHash } from "crypto";
43
48
  import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
44
49
  import path from "path";
45
- import { PLAN_PHASES, PLAN_STEP_DISPOSITIONS, PlanLedgerError, currentLedgerState, resolveOwnedParts, resolveOwnedSteps, sanitizePlanStepRecords, unresolvedLedgerRecords, validatePhaseResult, validatePlanMetadata, } from "./plan-execution-ledger.js";
46
- import { assertPlanIsRoutable } from "./plan-phase-routing.js";
50
+ import { PLAN_PHASES, PLAN_STEP_DISPOSITIONS, PlanLedgerError, currentLedgerState, isAcceptedPrDependentEscalation, resolveOwnedSteps, sanitizePlanStepRecords, unresolvedLedgerRecords, validatePhaseResult, validatePlanMetadata, } from "./plan-execution-ledger.js";
51
+ import { assertPlanIsRoutable, resolveOwnedRanges } from "./plan-phase-routing.js";
47
52
  // ---------------------------------------------------------------------------
48
53
  // Layout
49
54
  // ---------------------------------------------------------------------------
@@ -94,7 +99,7 @@ export const CHECKPOINT_STALE_REASONS = [
94
99
  // ---------------------------------------------------------------------------
95
100
  // Path safety
96
101
  // ---------------------------------------------------------------------------
97
- function assertValidTicketKey(ticketKey) {
102
+ export function assertValidTicketKey(ticketKey) {
98
103
  if (typeof ticketKey !== "string" || !TICKET_KEY_PATTERN.test(ticketKey.trim())) {
99
104
  throw new PlanLedgerError("ticket key must match PROJ-123; it becomes a filename segment and is never sanitized into a different one");
100
105
  }
@@ -106,7 +111,7 @@ function assertValidPhase(phase) {
106
111
  }
107
112
  return phase;
108
113
  }
109
- function assertDocsDir(docsDir) {
114
+ export function assertDocsDir(docsDir) {
110
115
  if (typeof docsDir !== "string" || docsDir.trim() === "") {
111
116
  throw new PlanLedgerError("docs directory is required and must be a non-empty path");
112
117
  }
@@ -121,7 +126,7 @@ function assertDocsDir(docsDir) {
121
126
  * Checked before any `mkdir`, read, temporary write, or rename — a containment
122
127
  * check that runs after the directory exists has already had its side effect.
123
128
  */
124
- function resolveWithinRoot(root, ...segments) {
129
+ export function resolveWithinRoot(root, ...segments) {
125
130
  const resolved = path.resolve(root, ...segments);
126
131
  const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
127
132
  if (resolved !== root && !resolved.startsWith(rootWithSep)) {
@@ -169,7 +174,7 @@ function isMissingFileError(err) {
169
174
  err !== null &&
170
175
  err.code === "ENOENT");
171
176
  }
172
- async function readJsonArtifact(absolutePath, relativePath) {
177
+ export async function readJsonArtifact(absolutePath, relativePath) {
173
178
  let raw;
174
179
  try {
175
180
  raw = await readFile(absolutePath, "utf8");
@@ -221,9 +226,16 @@ function validateArtifactEnvelope(parsed, relativePath, ticketKey, phase, finger
221
226
  * vocabulary are still enforced — a record naming a step this phase does not own,
222
227
  * or a disposition outside the set, means the artifact does not describe this
223
228
  * plan and is refused rather than partially trusted.
229
+ *
230
+ * Post-PR records are the exception to "owned under the current plan": post-PR
231
+ * ownership moves as steps resolve, so a stored post-PR record is checked against
232
+ * `postPrAuthorized` — the steps authorized at the time it was written — rather
233
+ * than today's ownership set. See {@link postPrHistoricalAuthorization}.
224
234
  */
225
- function assertStoredRecordsRoutable(records, metadata, phase, relativePath) {
226
- const owned = new Set(resolveOwnedSteps(metadata, phase));
235
+ function assertStoredRecordsRoutable(records, metadata, phase, relativePath, postPrAuthorized = new Set()) {
236
+ const owned = phase === "post_pr_gap_close"
237
+ ? new Set([...postPrAuthorized].filter((step) => stepIsInPlan(metadata, step)))
238
+ : new Set(resolveOwnedSteps(metadata, phase));
227
239
  for (const record of records) {
228
240
  if (typeof record !== "object" || record === null) {
229
241
  throw new PlanLedgerError(`${relativePath} holds a record that is not an object`);
@@ -237,6 +249,46 @@ function assertStoredRecordsRoutable(records, metadata, phase, relativePath) {
237
249
  }
238
250
  return records.map((record) => ({ ...record, phase }));
239
251
  }
252
+ function stepIsInPlan(metadata, stepId) {
253
+ return metadata.parts.some((part) => stepId >= part.start_step && stepId <= part.end_step);
254
+ }
255
+ /**
256
+ * Steps a stored post-PR record may name: those authorized for post-PR work at any
257
+ * point in history.
258
+ *
259
+ * With an accepted ledger, that is every step that ever had an accepted PR-dependent
260
+ * escalation — not just the ones still open today, since a resolved step's post-PR
261
+ * record is exactly what resolved it. Without one, the local pre-PR records stand in
262
+ * for reading history only: a local escalated record carrying `prDependency` makes
263
+ * an already-written post-PR record readable. Ownership for a NEW post-PR write
264
+ * still comes only from the accepted ledger.
265
+ */
266
+ function postPrHistoricalAuthorization(accepted, localPrePrRecords) {
267
+ const authorized = new Set();
268
+ if (accepted && Array.isArray(accepted.acceptedRecords)) {
269
+ for (const record of accepted.acceptedRecords) {
270
+ if (record && isAcceptedPrDependentEscalation(record))
271
+ authorized.add(record.step_id);
272
+ }
273
+ return authorized;
274
+ }
275
+ for (const record of localPrePrRecords) {
276
+ if (record.disposition === "escalated" && record.prDependency !== undefined) {
277
+ authorized.add(record.stepId);
278
+ }
279
+ }
280
+ return authorized;
281
+ }
282
+ /** Read and route-check the stored pre-PR records, or none when absent. */
283
+ async function readStoredPrePrRecords(docsDir, ticketKey, metadata, fingerprint) {
284
+ const relativePath = phaseResultRelativePath(ticketKey, "pre_pr_verification");
285
+ const absolutePath = resolveWithinRoot(docsDir, relativePath);
286
+ const parsed = await readJsonArtifact(absolutePath, relativePath);
287
+ if (parsed === undefined)
288
+ return [];
289
+ return assertStoredRecordsRoutable(validateArtifactEnvelope(parsed, relativePath, ticketKey, "pre_pr_verification", fingerprint)
290
+ .records, metadata, "pre_pr_verification", relativePath);
291
+ }
240
292
  /**
241
293
  * Append `incoming` to `existing`, refusing to walk a settled step backwards.
242
294
  *
@@ -282,6 +334,7 @@ export async function loadInlinePhaseContext(options) {
282
334
  assertPlanIsRoutable(metadata);
283
335
  const fingerprint = planFingerprint(metadata);
284
336
  let ledger = [];
337
+ let prePrRecords = [];
285
338
  const artifactPaths = [];
286
339
  // Latest record index per produce step, within the produce artifact's own list.
287
340
  const produceLatestIndex = new Map();
@@ -297,7 +350,11 @@ export async function loadInlinePhaseContext(options) {
297
350
  const artifact = validateArtifactEnvelope(parsed, relativePath, ticketKey, priorPhase, fingerprint);
298
351
  // Re-check against the CURRENT metadata: an artifact that was valid when
299
352
  // written must still describe steps this phase owns under the plan in play now.
300
- const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath);
353
+ const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath, priorPhase === "post_pr_gap_close"
354
+ ? postPrHistoricalAuthorization(options.acceptedLedger, prePrRecords)
355
+ : undefined);
356
+ if (priorPhase === "pre_pr_verification")
357
+ prePrRecords = routable;
301
358
  if (priorPhase === "produce") {
302
359
  produceRecordCount = routable.length;
303
360
  routable.forEach((record, index) => produceLatestIndex.set(record.stepId, index));
@@ -367,14 +424,8 @@ export async function loadInlinePhaseContext(options) {
367
424
  return {
368
425
  phase,
369
426
  routingSource: INLINE_ROUTING_SOURCE,
370
- ownedSteps: resolveOwnedSteps(metadata, phase),
371
- ownedRanges: resolveOwnedParts(metadata, phase).map((part) => ({
372
- partId: part.part_id,
373
- provenanceClass: part.provenance_class,
374
- startStep: part.start_step,
375
- endStep: part.end_step,
376
- declaredAdvisory: part.declared_advisory,
377
- })),
427
+ ownedSteps: resolveOwnedSteps(metadata, phase, options.acceptedLedger),
428
+ ownedRanges: resolveOwnedRanges(metadata, phase, options.acceptedLedger),
378
429
  ledger,
379
430
  currentRecordsByStep,
380
431
  terminalStepIds,
@@ -507,7 +558,7 @@ async function readProduceCoverage(docsDir, ticketKey) {
507
558
  * half-written ledger. The temp file is removed on failure so a rejected write
508
559
  * leaves nothing behind to be mistaken for state.
509
560
  */
510
- async function writeAtomically(absolutePath, relativePath, payload) {
561
+ export async function writeAtomically(absolutePath, relativePath, payload) {
511
562
  const tempPath = `${absolutePath}.tmp`;
512
563
  await mkdir(path.dirname(absolutePath), { recursive: true });
513
564
  try {
@@ -539,7 +590,7 @@ export async function writeInlinePhaseResult(options) {
539
590
  throw new PlanLedgerError("phase result must be an object");
540
591
  }
541
592
  const phase = assertValidPhase(envelope.phase);
542
- const validated = validatePhaseResult(envelope, metadata, phase);
593
+ const validated = validatePhaseResult(envelope, metadata, phase, options.acceptedLedger);
543
594
  const sanitized = sanitizePlanStepRecords(validated);
544
595
  const relativePath = phaseResultRelativePath(ticketKey, phase);
545
596
  const absolutePath = resolveWithinRoot(docsDir, relativePath);
@@ -549,7 +600,11 @@ export async function writeInlinePhaseResult(options) {
549
600
  const existing = await readJsonArtifact(absolutePath, relativePath);
550
601
  let priorRecords = [];
551
602
  if (existing !== undefined) {
552
- priorRecords = assertStoredRecordsRoutable(validateArtifactEnvelope(existing, relativePath, ticketKey, phase, fingerprint).records, metadata, phase, relativePath);
603
+ priorRecords = assertStoredRecordsRoutable(validateArtifactEnvelope(existing, relativePath, ticketKey, phase, fingerprint).records, metadata, phase, relativePath, phase === "post_pr_gap_close"
604
+ ? postPrHistoricalAuthorization(options.acceptedLedger, options.acceptedLedger
605
+ ? []
606
+ : await readStoredPrePrRecords(docsDir, ticketKey, metadata, fingerprint))
607
+ : undefined);
553
608
  }
554
609
  const merged = appendInlineRecords(priorRecords, sanitized);
555
610
  const artifact = {
@@ -19,7 +19,10 @@
19
19
  import { createHash } from "node:crypto";
20
20
  import { resolveRecipe } from "./pipeline-utils.js";
21
21
  import { parseUnsupportedInLocalModeEnvelope } from "./tool-error-envelope.js";
22
+ import { TICKET_KEY_PATTERN } from "./ticket-key-utils.js";
22
23
  import { PlanLedgerError } from "./plan-execution-ledger.js";
24
+ import { enqueueAndSync, enqueueExistingLocalRecords, ensureDeliveryBinding, fetchAcceptedLedgerContext, serverShaOrUndefined, syncPendingSubmissions, } from "./ticket-delivery-enrollment.js";
25
+ import { getReadiness, } from "./ticket-delivery.js";
23
26
  import { assertPlanIsRoutable, buildPhaseRoutingContext, extractPlanMetadata, injectPhaseRoutingContext, parseCheckpointResult, phaseForInstructionFile, settlePhaseResult, } from "./plan-phase-routing.js";
24
27
  /**
25
28
  * Derive a STABLE, content-addressed hash of an idea — the basis for the
@@ -363,7 +366,15 @@ export async function resumePipeline(deps, input) {
363
366
  const phase = phaseForInstructionFile(step.instruction_file);
364
367
  const planMetadata = recordedPlanMetadata(accumulatedResults);
365
368
  if (phase && planMetadata) {
366
- const outcome = settlePhaseResult(input.agent_result, planMetadata, phase, recordedLedger(accumulatedResults));
369
+ // BAPI-1426: the binding normally exists already (made before the first
370
+ // routed phase paused). Post-PR settlement uses the freshly accepted server
371
+ // ledger, so a local-only dependency mark cannot claim ownership.
372
+ const binding = await ensureOrchestratedBinding(deps, recipe, accumulatedResults);
373
+ const accepted = phase === "post_pr_gap_close"
374
+ ? await refreshAcceptedLedger(deps, recipe, binding)
375
+ : undefined;
376
+ const existingLedger = recordedLedger(accumulatedResults);
377
+ const outcome = settlePhaseResult(input.agent_result, planMetadata, phase, existingLedger, accepted);
367
378
  entry.plan_step_records = outcome.ledger;
368
379
  if (outcome.diagnostic) {
369
380
  entry.plan_routing_diagnostic = outcome.diagnostic;
@@ -397,6 +408,13 @@ export async function resumePipeline(deps, input) {
397
408
  results: accumulatedResults,
398
409
  };
399
410
  }
411
+ // BAPI-1426: queue the records this phase settled (a reported envelope or
412
+ // a generated escalation) and sync them. The ledger only ever appends, so
413
+ // the new records are the tail past the prior ledger. A sync failure keeps
414
+ // the batch pending; it never fails the run or claims delivery.
415
+ const deliverySync = await syncSettledPhase(deps, recipe, binding, phase, outcome.ledger.slice(existingLedger.length));
416
+ if (deliverySync)
417
+ entry.delivery_sync = deliverySync;
400
418
  }
401
419
  accumulatedResults.push(entry);
402
420
  nextIndex = stepIndex + 1;
@@ -570,6 +588,214 @@ function attachPlanMetadata(entry) {
570
588
  console.error(`[pipeline] plan routing unavailable: ${message}`);
571
589
  }
572
590
  }
591
+ // ---------------------------------------------------------------------------
592
+ // BAPI-1426 — managed delivery, shared with the inline tools
593
+ // ---------------------------------------------------------------------------
594
+ /** Full adapter deps for this run, or undefined when delivery is not wired. */
595
+ function deliveryAdapter(deps) {
596
+ const delivery = deps.ticketDelivery;
597
+ if (!delivery)
598
+ return undefined;
599
+ return {
600
+ client: delivery.client ?? {
601
+ baseUrl: deps.baseUrl,
602
+ headers: async () => ({ "X-API-Key": deps.apiKey }),
603
+ },
604
+ repoName: deps.repoName,
605
+ docsDir: deps.docsDir,
606
+ resolveSourceBranch: delivery.resolveSourceBranch,
607
+ resolvedBaseBranch: delivery.resolvedBaseBranch?.trim() || "main",
608
+ ...(delivery.executorJobId !== undefined ? { executorJobId: delivery.executorJobId } : {}),
609
+ ...(delivery.store ? { store: delivery.store } : {}),
610
+ };
611
+ }
612
+ /** The ticket a recipe works on, read from its resolved `ticket_number` params. */
613
+ function recipeTicketKey(recipe) {
614
+ for (const step of recipe.steps) {
615
+ const value = step.params?.ticket_number;
616
+ if (typeof value !== "string")
617
+ continue;
618
+ const key = value.trim().toUpperCase();
619
+ if (TICKET_KEY_PATTERN.test(key))
620
+ return key;
621
+ }
622
+ return undefined;
623
+ }
624
+ /** True when the recipe has at least one routed plan phase. */
625
+ function recipeHasRoutedPhases(recipe) {
626
+ return recipe.steps.some((step) => phaseForInstructionFile(step.instruction_file) !== undefined);
627
+ }
628
+ /** The binding recorded on an earlier entry, if any. */
629
+ function recordedDeliveryBinding(results) {
630
+ for (let i = results.length - 1; i >= 0; i -= 1) {
631
+ const binding = results[i].delivery_binding;
632
+ if (binding)
633
+ return binding;
634
+ }
635
+ return undefined;
636
+ }
637
+ /**
638
+ * Bind this run to a server attempt, once, and record the binding on the latest
639
+ * entry so the next persistence write carries it. Never throws. A run that was
640
+ * refused enrollment (`disabled`) is not asked again; an `unavailable` one is.
641
+ */
642
+ async function ensureOrchestratedBinding(deps, recipe, results) {
643
+ const adapter = deliveryAdapter(deps);
644
+ const ticketKey = recipeTicketKey(recipe);
645
+ if (!adapter || !ticketKey || results.length === 0)
646
+ return undefined;
647
+ const recorded = recordedDeliveryBinding(results);
648
+ if (recorded?.binding)
649
+ return recorded.binding;
650
+ if (recorded?.enrollment === "disabled")
651
+ return undefined;
652
+ const ensured = await ensureDeliveryBinding(adapter, ticketKey);
653
+ if (ensured.binding && ensured.newlyRegistered) {
654
+ // Work recorded before this attempt existed is sent once for reconciliation.
655
+ try {
656
+ await enqueueExistingLocalRecords(adapter, ticketKey, recordedLedger(results));
657
+ }
658
+ catch (err) {
659
+ console.error(`[pipeline] could not queue existing local records: ${String(err)}`);
660
+ }
661
+ }
662
+ const last = results.length - 1;
663
+ results[last] = {
664
+ ...results[last],
665
+ delivery_binding: {
666
+ enrollment: ensured.enrollment,
667
+ attempt_id: ensured.binding?.attempt_id ?? null,
668
+ ...(ensured.binding ? { binding: ensured.binding } : {}),
669
+ ...(ensured.reason ? { reason: ensured.reason } : {}),
670
+ },
671
+ };
672
+ return ensured.binding;
673
+ }
674
+ /**
675
+ * The server-accepted ledger, refreshed right before post-PR routing or
676
+ * settlement. Pending batches are retried first so ownership reflects the latest
677
+ * accepted evidence. Undefined (owns nothing) when unbound or unreadable: an
678
+ * unsynchronized local `prDependency` mark never creates post-PR ownership.
679
+ */
680
+ async function refreshAcceptedLedger(deps, recipe, binding) {
681
+ const adapter = deliveryAdapter(deps);
682
+ const ticketKey = recipeTicketKey(recipe);
683
+ if (!adapter || !ticketKey || !binding)
684
+ return undefined;
685
+ await syncPendingSubmissions(adapter, ticketKey, binding);
686
+ const fetched = await fetchAcceptedLedgerContext(adapter, binding);
687
+ if (!fetched.ok) {
688
+ console.error(`[pipeline] accepted ledger unavailable: ${fetched.error}`);
689
+ return undefined;
690
+ }
691
+ return fetched.context;
692
+ }
693
+ /**
694
+ * Queue and sync the records one routed phase just settled — a reported envelope
695
+ * or a generated missing-envelope escalation alike. The journal write is local and
696
+ * comes first; a server or network failure leaves the batch pending.
697
+ */
698
+ async function syncSettledPhase(deps, recipe, binding, phase, newRecords) {
699
+ const adapter = deliveryAdapter(deps);
700
+ const ticketKey = recipeTicketKey(recipe);
701
+ if (!adapter || !ticketKey || newRecords.length === 0)
702
+ return undefined;
703
+ const read = deps.ticketDelivery?.readGitValue;
704
+ const head = read ? serverShaOrUndefined(await read(["rev-parse", "HEAD"]).catch(() => undefined)) : undefined;
705
+ const tree = read
706
+ ? serverShaOrUndefined(await read(["rev-parse", "HEAD^{tree}"]).catch(() => undefined))
707
+ : undefined;
708
+ const payload = {
709
+ phase_result: { version: 1, phase, records: newRecords },
710
+ ...(head ? { tested_head_sha: head } : {}),
711
+ ...(tree ? { tested_tree_sha: tree } : {}),
712
+ };
713
+ try {
714
+ const { submission, sync } = await enqueueAndSync(adapter, ticketKey, binding, { phase, payload });
715
+ const report = sync?.reports.find((r) => r.submission_id === submission.submission_id);
716
+ const stored = report !== undefined && report.operation_status !== null && !report.retryable;
717
+ const error = report?.error ??
718
+ (!binding
719
+ ? "no managed binding; the batch is kept locally"
720
+ : !report
721
+ ? "an earlier pending batch was not accepted yet; this batch stays pending"
722
+ : undefined);
723
+ return {
724
+ submission_id: submission.submission_id,
725
+ operation_status: report?.operation_status ?? null,
726
+ refusals: report?.refusals ?? [],
727
+ pending: !stored,
728
+ ...(error ? { error } : {}),
729
+ readiness_outcome: sync?.readiness?.outcome ?? null,
730
+ };
731
+ }
732
+ catch (err) {
733
+ return {
734
+ submission_id: null,
735
+ operation_status: null,
736
+ refusals: [],
737
+ pending: false,
738
+ error: `the local delivery journal could not be updated: ${String(err instanceof Error ? err.message : err)}`,
739
+ };
740
+ }
741
+ }
742
+ /** The server's delivery decision for the completed envelope. Never throws. */
743
+ async function deliveryOutcomeForCompletion(deps, recipe, results) {
744
+ const recorded = recordedDeliveryBinding(results);
745
+ const base = {
746
+ managed: false,
747
+ enrollment: recorded?.enrollment ?? "not_configured",
748
+ attempt_id: recorded?.attempt_id ?? null,
749
+ outcome: null,
750
+ blockers: [],
751
+ identities: {},
752
+ preserved_branch: null,
753
+ preserved_head_sha: null,
754
+ publication: null,
755
+ checkpoint_only: false,
756
+ delivery_complete: false,
757
+ };
758
+ const adapter = deliveryAdapter(deps);
759
+ const ticketKey = recipeTicketKey(recipe);
760
+ if (!adapter || !ticketKey)
761
+ return { ...base, enrollment: "not_configured" };
762
+ const binding = recorded?.binding;
763
+ if (!binding) {
764
+ return {
765
+ ...base,
766
+ ...(recorded?.reason ? { unavailable_reason: recorded.reason } : {}),
767
+ };
768
+ }
769
+ const pass = await syncPendingSubmissions(adapter, ticketKey, binding);
770
+ const response = await getReadiness(adapter.client, binding.attempt_id, {
771
+ repo_name: deps.repoName,
772
+ boundary: "publish",
773
+ });
774
+ if (!response.ok) {
775
+ return {
776
+ ...base,
777
+ managed: true,
778
+ pending_submissions: pass.pending,
779
+ unavailable_reason: `${response.error}: ${response.message}`,
780
+ };
781
+ }
782
+ const readiness = response.body.readiness;
783
+ const publication = readiness?.publication ?? null;
784
+ return {
785
+ ...base,
786
+ managed: true,
787
+ outcome: readiness?.outcome ?? null,
788
+ boundary: readiness?.boundary,
789
+ blockers: Array.isArray(readiness?.blockers) ? readiness.blockers : [],
790
+ identities: readiness?.identities ?? {},
791
+ preserved_branch: readiness?.preserved_branch ?? binding.source_branch,
792
+ preserved_head_sha: readiness?.preserved_head_sha ?? null,
793
+ publication,
794
+ checkpoint_only: publication?.action === "checkpoint_only",
795
+ delivery_complete: readiness?.delivery_complete === true,
796
+ pending_submissions: pass.pending,
797
+ };
798
+ }
573
799
  async function continuePipelineExecution(deps, persistence, row, recipe, autoApprove, forceExecuteAtIndex, approvalAgentResult) {
574
800
  let pipelineRunId = row.pipeline_run_id;
575
801
  let stepIndex = row.current_step_index;
@@ -618,6 +844,19 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
618
844
  };
619
845
  }
620
846
  if (step.type === "agent_task") {
847
+ const phase = phaseForInstructionFile(step.instruction_file);
848
+ const planMetadata = recordedPlanMetadata(results);
849
+ // BAPI-1426: enroll before the first routed phase (the production task)
850
+ // starts, and persist the binding with the pause below so a resumed run or
851
+ // a fresh process reuses the same attempt. Before post-PR routing, refresh
852
+ // the server-accepted ledger: it alone decides post-PR ownership.
853
+ let accepted;
854
+ if (phase && planMetadata) {
855
+ const binding = await ensureOrchestratedBinding(deps, recipe, results);
856
+ if (phase === "post_pr_gap_close") {
857
+ accepted = await refreshAcceptedLedger(deps, recipe, binding);
858
+ }
859
+ }
621
860
  try {
622
861
  await persistence.patchRun(pipelineRunId, {
623
862
  current_step_index: stepIndex,
@@ -639,10 +878,8 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
639
878
  // hand-off did not survive — the recipe pauses, and the session that resumes
640
879
  // may be a different one.
641
880
  let instruction = step.instruction ?? "";
642
- const phase = phaseForInstructionFile(step.instruction_file);
643
- const planMetadata = recordedPlanMetadata(results);
644
881
  if (phase && planMetadata) {
645
- instruction = injectPhaseRoutingContext(instruction, buildPhaseRoutingContext(planMetadata, phase, recordedLedger(results), recordedCheckpoint(results)));
882
+ instruction = injectPhaseRoutingContext(instruction, buildPhaseRoutingContext(planMetadata, phase, recordedLedger(results), recordedCheckpoint(results), accepted));
646
883
  }
647
884
  return {
648
885
  status: "needs_agent_task",
@@ -752,12 +989,18 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
752
989
  step_index: step.step,
753
990
  });
754
991
  }
992
+ // BAPI-1426: recipe completion is not ticket delivery. A routed recipe reports
993
+ // the server's delivery decision separately; a checkpoint-only outcome is a
994
+ // supported exit and leaves the recipe status `completed`.
755
995
  return {
756
996
  status: "completed",
757
997
  pipeline_run_id: pipelineRunId,
758
998
  pipeline: recipe.pipeline,
759
999
  total_steps: totalSteps,
760
1000
  results,
1001
+ ...(recipeHasRoutedPhases(recipe)
1002
+ ? { delivery: await deliveryOutcomeForCompletion(deps, recipe, results) }
1003
+ : {}),
761
1004
  };
762
1005
  }
763
1006
  function handlePersistenceErrorForEnvelope(err, extras) {