@bridge_gpt/mcp-server 0.2.68 → 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 (166) hide show
  1. package/README.md +226 -320
  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 +650 -98
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-review-workflow-drift.js +3 -3
  9. package/build/claude-settings.js +84 -2
  10. package/build/cli-help.js +7 -1
  11. package/build/codex-bridge-profile.js +1 -1
  12. package/build/codex-home.js +9 -1
  13. package/build/codex-launcher-pin.js +598 -0
  14. package/build/commands.generated.js +8 -8
  15. package/build/conductor/bridge-api-client.js +598 -19
  16. package/build/conductor/bring-up-facts.js +9 -9
  17. package/build/conductor/doctor.js +466 -18
  18. package/build/conductor/epic-child-continuation.js +54 -3
  19. package/build/conductor/epic-implementer-cli.js +132 -34
  20. package/build/conductor/epic-runtime.js +2 -2
  21. package/build/conductor/epic-state.js +1 -1
  22. package/build/conductor/hook-runtime-qualification.js +1 -1
  23. package/build/conductor/install-doctor.js +4 -2
  24. package/build/conductor/launch-need-probes.js +337 -0
  25. package/build/conductor/local-merge.js +241 -56
  26. package/build/conductor/merge-credential.js +172 -0
  27. package/build/conductor/merge-execution-eligibility.js +1 -1
  28. package/build/conductor/merged-child-check.js +95 -0
  29. package/build/conductor/plan-trim.js +30 -0
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +11 -6
  32. package/build/conductor/readiness-prerequisites.js +41 -0
  33. package/build/conductor/readiness-sections.js +345 -55
  34. package/build/conductor/readiness.js +99 -87
  35. package/build/conductor/recovery-cli.js +606 -21
  36. package/build/conductor/recovery-operations.js +306 -7
  37. package/build/conductor/repair-contract.js +47 -2
  38. package/build/conductor/repository-workflow-remedy.js +8 -6
  39. package/build/conductor/stored-merge-credential.js +76 -0
  40. package/build/conductor/stored-merge-token-probe.js +28 -0
  41. package/build/conductor/temporary-plan.js +50 -0
  42. package/build/conductor/wait-run-cli.js +759 -0
  43. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  44. package/build/conductor-bin.js +2 -2
  45. package/build/conductor-claude-hook-bin.js +2 -2
  46. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  47. package/build/conductor-removed-stub-bin.js +2 -5
  48. package/build/connect-github-api.js +33 -14
  49. package/build/credential-store.js +356 -13
  50. package/build/docs.generated.js +27 -14
  51. package/build/doctor.js +203 -38
  52. package/build/drive-epic-help.js +212 -0
  53. package/build/drive-epic.js +1840 -373
  54. package/build/epic-ci-workflow-drift.js +77 -0
  55. package/build/epic-ci-workflow-install.js +183 -0
  56. package/build/epic-ci-workflow.js +157 -0
  57. package/build/epic-contract-context.js +326 -0
  58. package/build/epic-implementer-bin.js +131 -120
  59. package/build/epic-implementer-claude-hook-bin.js +2 -2
  60. package/build/escalation-reachability.js +134 -0
  61. package/build/executor/cleanup-report.js +99 -0
  62. package/build/executor/cli.js +12 -1
  63. package/build/executor/completion-contract.js +68 -11
  64. package/build/executor/completion-supervision.js +99 -0
  65. package/build/executor/delivery-outcome.js +179 -0
  66. package/build/executor/deps.js +3 -0
  67. package/build/executor/env.js +71 -0
  68. package/build/executor/failure-reason-codes.js +126 -0
  69. package/build/executor/heartbeat.js +38 -0
  70. package/build/executor/http-client.js +115 -4
  71. package/build/executor/integration-job.js +298 -5
  72. package/build/executor/job-errors.js +36 -6
  73. package/build/executor/job-runner.js +720 -36
  74. package/build/executor/likely-tests.js +36 -2
  75. package/build/executor/merge-job.js +145 -2
  76. package/build/executor/observation.js +73 -4
  77. package/build/executor/permissions.js +193 -97
  78. package/build/executor/preflight.js +14 -3
  79. package/build/executor/process-containment.js +28 -2
  80. package/build/executor/prompt-spec.js +50 -5
  81. package/build/executor/rebase-finalization.js +38 -3
  82. package/build/executor/reconcile-overlap-job.js +35 -7
  83. package/build/executor/required-work-ledger.js +236 -81
  84. package/build/executor/resume-pre-spawn.js +38 -1
  85. package/build/executor/runner.js +91 -3
  86. package/build/executor/test-clock.js +8 -0
  87. package/build/executor/types.js +37 -2
  88. package/build/executor/worker-cli-launcher.js +259 -0
  89. package/build/executor/worker-command.js +41 -4
  90. package/build/executor/worker-database-target.js +91 -0
  91. package/build/executor/worker-database.js +908 -0
  92. package/build/executor/worker-finalization.js +106 -0
  93. package/build/executor/worker-guard-hook.js +203 -0
  94. package/build/executor/worker-resources.js +47 -0
  95. package/build/executor/worker-verification-guidance.js +15 -2
  96. package/build/executor/worktree.js +303 -15
  97. package/build/executor-launch.js +11 -1
  98. package/build/generated-assets-doctor.js +9 -1
  99. package/build/generated-assets-health.js +13 -4
  100. package/build/generated-assets-legacy.js +39 -0
  101. package/build/generated-assets-paths.js +12 -1
  102. package/build/generated-assets-repair.js +4 -1
  103. package/build/generated-assets-test-support.js +12 -0
  104. package/build/github-remote.js +33 -0
  105. package/build/github-sign-in.js +62 -0
  106. package/build/index.js +1214 -465
  107. package/build/init.js +39 -1
  108. package/build/install-bridge-conductor-gate.js +78 -0
  109. package/build/install-bridge-conductor-setup.js +413 -0
  110. package/build/install-bridge-conductor.js +1226 -267
  111. package/build/install-bridge.js +629 -350
  112. package/build/launcher-config-inspection.js +184 -19
  113. package/build/mcp-host-targets.js +4 -0
  114. package/build/mcp-identity.js +7 -0
  115. package/build/mcp-install-state.js +8 -2
  116. package/build/merge-pull-request.js +6 -0
  117. package/build/merge-workflow-install.js +763 -345
  118. package/build/phase-result-artifacts.js +75 -20
  119. package/build/pipeline-orchestrator.js +247 -4
  120. package/build/pipelines.generated.js +35 -35
  121. package/build/plan-discovery.js +15 -2
  122. package/build/plan-execution-ledger.js +306 -17
  123. package/build/plan-phase-routing.js +48 -13
  124. package/build/plane/cli.js +105 -51
  125. package/build/plane/defaults.js +3 -0
  126. package/build/plane/plane-freshness.js +294 -0
  127. package/build/plane/schema-compatibility.js +137 -0
  128. package/build/plane/shutdown-deps.js +38 -0
  129. package/build/plane/shutdown.js +83 -8
  130. package/build/plane/terminal-run-cleanup.js +303 -0
  131. package/build/printed-remedy-commands.js +26 -4
  132. package/build/readiness-check.js +6 -2
  133. package/build/readiness-prerequisites.generated.js +532 -0
  134. package/build/readme.generated.js +1 -1
  135. package/build/session-upgrade-notice.js +86 -0
  136. package/build/setup-epic.js +400 -30
  137. package/build/shared-resources.generated.js +5 -0
  138. package/build/start-tickets.js +43 -15
  139. package/build/test-command-detection.js +247 -0
  140. package/build/test-command-validation.js +73 -0
  141. package/build/testing-config-client.js +150 -0
  142. package/build/ticket-delivery-artifacts.js +258 -0
  143. package/build/ticket-delivery-enrollment.js +306 -0
  144. package/build/ticket-delivery.js +205 -0
  145. package/build/ticket-review-summary.js +14 -0
  146. package/build/tool-call-advisory.js +162 -0
  147. package/build/tool-surface-gating.js +28 -89
  148. package/build/update-check.js +97 -16
  149. package/build/update-status.js +61 -71
  150. package/build/upgrade-advice.js +15 -11
  151. package/build/upgrade-cli.js +360 -20
  152. package/build/v2-readiness-requirements.js +552 -0
  153. package/build/version.generated.js +3 -3
  154. package/build/worker-guard-hook-bin.js +2 -2
  155. package/build/worktree-core.js +22 -7
  156. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  157. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  158. package/docs/EPIC-IMPLEMENTER.md +104 -63
  159. package/docs/epic-implementer/getting-started.md +131 -0
  160. package/docs/epic-implementer/self-hosting.md +133 -0
  161. package/package.json +3 -3
  162. package/pipelines/idea-to-ticket.json +1 -1
  163. package/pipelines/implement-ticket.json +8 -8
  164. package/pipelines/review-ticket.json +2 -2
  165. package/design-assets/favicon/favicon-32x32.png +0 -0
  166. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -15,19 +15,47 @@
15
15
  * the per-task lifecycle, keyed by the FULL `tool_use_id`, says whether the work
16
16
  * the worker started actually finished.
17
17
  *
18
+ * A task ID may identify sequential runs, each with its own tool_use_id.
19
+ * Lifecycle-terminal failure permits a new run but need not resolve execution.
20
+ *
18
21
  * Two places deliberately differ from the probe reducer (`reducer-v2.mjs`), on
19
22
  * the user-approved ticket's terms:
20
- * - A REQUIRED task that ended `failed`, `stopped`, or `withdrawn` (the worker
21
- * ran its own `TaskStop`) stays UNRESOLVED. The probe counted a worker
22
- * withdrawal as complete; a worker giving up on required work is not success.
23
+ * - A REQUIRED task that ended `failed` or `stopped` stays UNRESOLVED. A worker
24
+ * withdrawal (the worker ran its own `TaskStop`) and a background command
25
+ * that failed inside the startup window (`failed_to_start`) resolve
26
+ * EXECUTION only: the work is no longer running, but nothing here says it
27
+ * succeeded or that anything was verified. Declared verification still
28
+ * depends only on verification facts.
29
+ * - BAPI-1455: a required COMMAND (`bash_task`, `bash_bg`, `subagent_bash`)
30
+ * that ended `failed` or `stopped` is resolved when the worker received
31
+ * that command's OWN terminal result — today only its foreground
32
+ * tool_result, bound by the exact tool_use_id. The recorded state stays the
33
+ * real one and the envelope carries `result_delivered: true` beside it.
34
+ * Delegates, monitors, denials, launch acknowledgements, and background
35
+ * output reads never count: no background result shape has been measured
36
+ * that binds to its task by structure rather than text, so background
37
+ * commands stay unresolved exactly as before.
23
38
  * - A denial links to an obligation only on the exact OPERATION and TARGET (for
24
39
  * example the prepared branch on `origin`), never on the effect class alone.
25
40
  *
26
- * Purity: no I/O, no real timers (the supervisor takes injected timer deps), no
41
+ * Purity: no I/O, no real timers (the supervisor takes injected timer deps; the
42
+ * ledger itself only reads an injected clock and starts no timers), no
27
43
  * retained prompt, command, argument, URL, or path. Tool inputs and denial
28
44
  * targets are kept only as SHA-256 digests.
29
45
  */
30
- import { MAX_DENIALS, MAX_EVENT_BYTES, MAX_TASKS, TASK_RESOLVING_STATES, boundEnvelope, boundedIdentifier, deriveCompletionCause, digestStructured, runtimeMatchesSupported, successRefusalReason, COMPLETION_CONTRACT_VERSION, COMPLETION_CONTRACT_VERSION_V2, } from "./completion-contract.js";
46
+ import { DELIVERY_RESOLVABLE_KINDS, DELIVERY_RESOLVABLE_STATES, MAX_DENIALS, MAX_EVENT_BYTES, MAX_TASKS, boundEnvelope, boundedIdentifier, deriveCompletionCause, digestStructured, isTaskResolved, runtimeMatchesSupported, successRefusalReason, COMPLETION_CONTRACT_VERSION, COMPLETION_CONTRACT_VERSION_V2, } from "./completion-contract.js";
47
+ /**
48
+ * A background command that fails this soon after its launch acknowledgement is
49
+ * classified `failed_to_start` and resolves execution of a required task.
50
+ *
51
+ * Accepted risk: a background verification command that genuinely fails inside
52
+ * the window also resolves execution. The envelope keeps the visible
53
+ * `failed_to_start` state and does not establish verification. Required CI and
54
+ * merge admission stay fail-closed, and the existing CI-failure and
55
+ * observation-timeout notices remain the alert paths. A check outside required
56
+ * CI is an accepted gap whose exposure can last one CI cycle.
57
+ */
58
+ export const FAILED_TO_START_WINDOW_MS = 30_000;
31
59
  // ---------------------------------------------------------------------------
32
60
  // Bounds and small pure helpers
33
61
  // ---------------------------------------------------------------------------
@@ -309,12 +337,15 @@ const TERMINAL_STATES = new Set([
309
337
  "failed",
310
338
  "stopped",
311
339
  "withdrawn",
340
+ "failed_to_start",
312
341
  "conflict",
313
342
  ]);
343
+ const FAILED_TO_START_KINDS = new Set(["bash_bg", "bash_task", "subagent_bash"]);
314
344
  const KNOWN_TASK_STATUSES = new Set(["pending", "running", "completed", "failed", "stopped", "killed"]);
315
345
  export function createRequiredWorkLedger(init) {
316
346
  const binding = { ...init.binding };
317
347
  const lifecycleSupported = init.lifecycleSupported === true;
348
+ const clock = init.now ?? Date.now;
318
349
  const optionalWatchers = (init.optionalWatchers ?? []).slice(0, 16).map((w) => ({ ...w }));
319
350
  // An obligation whose target cannot be normalized links nothing: a denial of
320
351
  // it then reads `unlinked`/`ambiguous`, and unmet finalization still refuses
@@ -326,7 +357,8 @@ export function createRequiredWorkLedger(init) {
326
357
  const obligations = new Map();
327
358
  const evicted = new Map();
328
359
  const toolUses = new Map();
329
- const workerStops = new Set();
360
+ const workerStops = new Set(); // bound obligation keys
361
+ const deferredWorkerStops = new Set(); // consumed at first association
330
362
  const denials = [];
331
363
  let latestSnapshot = null;
332
364
  let materialSeq = 0;
@@ -352,7 +384,7 @@ export function createRequiredWorkLedger(init) {
352
384
  continue;
353
385
  if (latestSnapshot?.includes(o.taskId ?? "\u0000"))
354
386
  continue;
355
- evicted.set(o.key, { state: o.state, taskId: o.taskId, kind: o.kind, inputDigest: o.inputDigest, order: o.order });
387
+ evicted.set(o.key, { key: o.key, state: o.state, taskId: o.taskId, kind: o.kind, inputDigest: o.inputDigest, order: o.order });
356
388
  obligations.delete(o.key);
357
389
  return true;
358
390
  }
@@ -372,18 +404,48 @@ export function createRequiredWorkLedger(init) {
372
404
  key,
373
405
  state: "pending",
374
406
  order: ++orderSeq,
407
+ launchedAt: null,
408
+ resultDelivered: false,
409
+ failedByResult: false,
375
410
  ...spec,
376
411
  required: spec.required ?? !isOptional(spec.kind, spec.inputDigest),
377
412
  };
378
413
  obligations.set(key, o);
414
+ if (o.taskId !== null)
415
+ bindDeferredStop(o);
379
416
  return o;
380
417
  };
381
- const findByTaskId = (taskId) => {
382
- for (const o of obligations.values())
383
- if (o.taskId === taskId && !o.placeholder)
384
- return o;
385
- return obligations.get(`t:${taskId}`);
418
+ const bindDeferredStop = (o) => {
419
+ if (o.taskId !== null && deferredWorkerStops.delete(o.taskId))
420
+ workerStops.add(o.key);
421
+ };
422
+ /** Record the clock at the FIRST launch event only; a later event never moves it. */
423
+ const recordLaunch = (o) => {
424
+ if (o.launchedAt !== null || o.state !== "pending")
425
+ return;
426
+ o.launchedAt = clock();
427
+ };
428
+ /** `failed_to_start` only for a first failure of background work inside the startup window. */
429
+ const classifyFailure = (o) => {
430
+ if (o.state !== "pending" || !o.background || !FAILED_TO_START_KINDS.has(o.kind) || o.launchedAt === null)
431
+ return "failed";
432
+ const elapsed = clock() - o.launchedAt;
433
+ return Number.isFinite(elapsed) && elapsed >= 0 && elapsed <= FAILED_TO_START_WINDOW_MS ? "failed_to_start" : "failed";
434
+ };
435
+ /** Prefer a live run, then the newest lifecycle-terminal run, including evictions. */
436
+ const findCurrentRunByTaskId = (taskId) => {
437
+ let current;
438
+ for (const o of [...obligations.values(), ...evicted.values()]) {
439
+ if (o.taskId !== taskId)
440
+ continue;
441
+ if (!current ||
442
+ (!TERMINAL_STATES.has(o.state) && TERMINAL_STATES.has(current.state)) ||
443
+ (TERMINAL_STATES.has(o.state) === TERMINAL_STATES.has(current.state) && o.order > current.order))
444
+ current = o;
445
+ }
446
+ return current;
386
447
  };
448
+ const hasOtherLiveClaim = (taskId, key) => [...obligations.values(), ...evicted.values()].some((o) => o.taskId === taskId && o.key !== key && !o.key.startsWith("t:") && !TERMINAL_STATES.has(o.state));
387
449
  /** Apply a terminal state. Identical duplicates are idempotent; a contradiction is `conflict`. */
388
450
  const applyTerminal = (o, next) => {
389
451
  if (!TERMINAL_STATES.has(o.state)) {
@@ -394,7 +456,11 @@ export function createRequiredWorkLedger(init) {
394
456
  return o.state === "conflict" ? "conflict" : "applied";
395
457
  // A stop (CLI teardown or worker TaskStop) after the task already ended is
396
458
  // measured noise (reducer-v2 R3), not a contradiction of the recorded end.
397
- if ((next === "stopped" || next === "withdrawn") && (o.state === "succeeded" || o.state === "failed"))
459
+ if ((next === "stopped" || next === "withdrawn") && (o.state === "succeeded" || o.state === "failed" || o.state === "failed_to_start"))
460
+ return "applied";
461
+ // A repeated failure notice keeps the first classification: a duplicate
462
+ // after the window must not contradict `failed_to_start`.
463
+ if (next === "failed" && o.state === "failed_to_start")
398
464
  return "applied";
399
465
  // A killed foreground call reports `stopped` and then its own error
400
466
  // tool_result (measured C5k, "Exit code 137"): one ending, not two.
@@ -410,21 +476,40 @@ export function createRequiredWorkLedger(init) {
410
476
  insufficient.add("event_conflict");
411
477
  return "conflict";
412
478
  };
479
+ /**
480
+ * BAPI-1455 — apply a task's own foreground result and record that it was
481
+ * delivered. The recorded state stays the real outcome; a later lifecycle
482
+ * `stopped` may still refine a result-derived `failed` (a killed call).
483
+ */
484
+ const applyForegroundResult = (o, next) => {
485
+ const wasPending = !TERMINAL_STATES.has(o.state);
486
+ const r = applyTerminal(o, next);
487
+ o.resultDelivered = true;
488
+ if (wasPending && o.state === "failed")
489
+ o.failedByResult = true;
490
+ return r;
491
+ };
413
492
  /** Fold a placeholder (snapshot-only or orphan terminal keyed by task id) into the real obligation. */
414
493
  const mergePlaceholder = (real, taskId) => {
415
494
  const ph = obligations.get(`t:${taskId}`);
416
495
  if (!ph || ph === real)
417
496
  return "applied";
418
497
  obligations.delete(ph.key);
498
+ if (workerStops.delete(ph.key))
499
+ workerStops.add(real.key);
500
+ if (ph.resultDelivered)
501
+ real.resultDelivered = true;
502
+ if (real.launchedAt === null && ph.launchedAt !== null && real.state === "pending")
503
+ real.launchedAt = ph.launchedAt;
419
504
  if (TERMINAL_STATES.has(ph.state))
420
505
  return applyTerminal(real, ph.state);
421
506
  return "applied";
422
507
  };
423
- const terminalOnEvicted = (key, next) => {
508
+ const terminalOnEvicted = (key, taskId, next) => {
424
509
  const ev = evicted.get(key);
425
510
  if (!ev)
426
511
  return "ignored";
427
- if (ev.state === next || next === "stopped" || next === "withdrawn")
512
+ if (ev.taskId === taskId && (ev.state === next || next === "stopped" || next === "withdrawn"))
428
513
  return "applied";
429
514
  insufficient.add("event_conflict");
430
515
  return "conflict";
@@ -492,6 +577,7 @@ export function createRequiredWorkLedger(init) {
492
577
  }
493
578
  const input = b["input"];
494
579
  const inputDigest = obligationInputDigest(input);
580
+ const bg = isRecord(input) && input["run_in_background"] === true;
495
581
  // Bounded index; the input itself is dropped here, only digests remain.
496
582
  if (!toolUses.has(id)) {
497
583
  if (toolUses.size >= MAX_TOOL_USE_INDEX) {
@@ -499,9 +585,14 @@ export function createRequiredWorkLedger(init) {
499
585
  if (oldest !== undefined)
500
586
  toolUses.delete(oldest);
501
587
  }
502
- toolUses.set(id, { name, inputDigest, denial: resolveDenialOperation(name, input) });
588
+ toolUses.set(id, {
589
+ name,
590
+ inputDigest,
591
+ denial: resolveDenialOperation(name, input),
592
+ background: bg,
593
+ foregroundResult: null,
594
+ });
503
595
  }
504
- const bg = isRecord(input) && input["run_in_background"] === true;
505
596
  if (name === "Agent" || name === "Task") {
506
597
  track(id, { toolUseId: id, taskId: null, kind: bg ? "delegate_bg" : "delegate", inputDigest, background: bg, placeholder: false });
507
598
  }
@@ -513,10 +604,16 @@ export function createRequiredWorkLedger(init) {
513
604
  }
514
605
  else if (name === "TaskStop") {
515
606
  // R13: the worker's OWN stop request. It turns a later stop into
516
- // `withdrawn` for the record — still unresolved when the task was required.
607
+ // `withdrawn` for the record. A withdrawal resolves execution of a
608
+ // required task (the work is no longer running); it is not success.
517
609
  const tid = isRecord(input) ? boundedIdentifier(input["task_id"]) : null;
518
- if (tid && workerStops.size < MAX_WORKER_STOPS)
519
- workerStops.add(tid);
610
+ if (tid && workerStops.size + deferredWorkerStops.size < MAX_WORKER_STOPS) {
611
+ const current = findCurrentRunByTaskId(tid);
612
+ if (current)
613
+ workerStops.add(current.key);
614
+ else
615
+ deferredWorkerStops.add(tid);
616
+ }
520
617
  }
521
618
  // Synchronous foreground Bash and every other tool are exploratory: not
522
619
  // obligations. Their denials are still recorded from the tool_result.
@@ -546,9 +643,25 @@ export function createRequiredWorkLedger(init) {
546
643
  continue;
547
644
  }
548
645
  const o = obligations.get(id);
549
- if (!o || o.background)
550
- continue; // background: a launch acknowledgement, not a result.
551
- const r = applyTerminal(o, isError ? "failed" : "succeeded"); // foreground delegate result.
646
+ if (!o) {
647
+ // BAPI-1455: a foreground Bash result can precede the lifecycle start
648
+ // that turns it into an obligation. Keep only the closed outcome.
649
+ const entry = toolUses.get(id);
650
+ if (entry && entry.name === "Bash" && !entry.background && entry.foregroundResult === null) {
651
+ entry.foregroundResult = isError ? "failed" : "succeeded";
652
+ }
653
+ continue;
654
+ }
655
+ if (o.background) {
656
+ // A launch acknowledgement, not a result: non-terminal. It is the
657
+ // first named launch event for the startup window. No result text is read.
658
+ if (!isError)
659
+ recordLaunch(o);
660
+ continue;
661
+ }
662
+ // A foreground result is the task's own terminal result, delivered to the
663
+ // worker under its exact tool_use_id (BAPI-1455): whatever its exit code.
664
+ const r = applyForegroundResult(o, isError ? "failed" : "succeeded");
552
665
  if (r === "conflict")
553
666
  outcome = "conflict";
554
667
  }
@@ -572,6 +685,7 @@ export function createRequiredWorkLedger(init) {
572
685
  track(`t:${taskId}`, { toolUseId: taskId, taskId, kind, inputDigest: null, background: bg, placeholder: true, required: true });
573
686
  return "applied";
574
687
  }
688
+ const repeatedStart = obligations.get(toolUseId)?.taskId === taskId;
575
689
  const entry = toolUses.get(toolUseId);
576
690
  const o = track(toolUseId, {
577
691
  toolUseId,
@@ -587,8 +701,28 @@ export function createRequiredWorkLedger(init) {
587
701
  insufficient.add("event_conflict");
588
702
  return "conflict";
589
703
  }
704
+ if (!repeatedStart && hasOtherLiveClaim(taskId, o.key)) {
705
+ o.taskId = taskId;
706
+ o.state = "conflict";
707
+ insufficient.add("event_conflict");
708
+ return "conflict";
709
+ }
590
710
  o.taskId = taskId;
591
- return mergePlaceholder(o, taskId);
711
+ bindDeferredStop(o);
712
+ // An orphan terminal recorded before this start had to guess a generic
713
+ // background `task`; the trusted start now names the real kind.
714
+ if (o.kind === "task") {
715
+ o.kind = kind;
716
+ o.background = bg;
717
+ }
718
+ recordLaunch(o);
719
+ const merged = mergePlaceholder(o, taskId);
720
+ if (merged === "conflict")
721
+ return merged;
722
+ // BAPI-1455: the command's own foreground result arrived before this start.
723
+ if (!bg && entry?.foregroundResult)
724
+ return applyForegroundResult(o, entry.foregroundResult);
725
+ return merged;
592
726
  };
593
727
  const handleTaskTerminal = (e, status) => {
594
728
  const taskId = boundedIdentifier(e["task_id"]);
@@ -603,8 +737,6 @@ export function createRequiredWorkLedger(init) {
603
737
  let next = status === "completed" ? "succeeded" : status === "failed" ? "failed" : status === "stopped" || status === "killed" ? "stopped" : null;
604
738
  if (next === null)
605
739
  return "applied"; // pending/running: non-terminal progress.
606
- if (next === "stopped" && workerStops.has(taskId))
607
- next = "withdrawn";
608
740
  const rawToolUse = e["tool_use_id"];
609
741
  const toolUseId = rawToolUse === undefined || rawToolUse === null ? null : boundedIdentifier(rawToolUse);
610
742
  if (rawToolUse !== undefined && rawToolUse !== null && !toolUseId) {
@@ -613,42 +745,66 @@ export function createRequiredWorkLedger(init) {
613
745
  }
614
746
  let o;
615
747
  if (toolUseId) {
616
- const byTask = findByTaskId(taskId);
617
- if (byTask && !byTask.placeholder && byTask.key !== toolUseId) {
748
+ // Exact keyed notices are authoritative for their own run, even after resume.
749
+ if (evicted.has(toolUseId))
750
+ return terminalOnEvicted(toolUseId, taskId, next);
751
+ o = obligations.get(toolUseId);
752
+ if (o?.taskId !== null && o?.taskId !== undefined && o.taskId !== taskId) {
618
753
  insufficient.add("event_conflict");
619
754
  return "conflict";
620
755
  }
621
- if (evicted.has(toolUseId))
622
- return terminalOnEvicted(toolUseId, next);
623
- // R4: a terminal without a start is retained, not dropped.
624
- o = track(toolUseId, {
625
- toolUseId,
626
- taskId,
627
- kind: "task",
628
- inputDigest: toolUses.get(toolUseId)?.inputDigest ?? null,
629
- background: true,
630
- placeholder: false,
631
- });
632
- if (!o)
633
- return "applied";
634
- if (o.taskId === null)
756
+ if (!o || o.taskId === null) {
757
+ const byTask = findCurrentRunByTaskId(taskId);
758
+ // An unexplained terminal cannot introduce a resumed start. Association
759
+ // of an existing anonymous tool use still requires no competing live claim.
760
+ if ((o && hasOtherLiveClaim(taskId, toolUseId)) ||
761
+ (!o && byTask && !byTask.key.startsWith("t:") && byTask.key !== toolUseId)) {
762
+ insufficient.add("event_conflict");
763
+ return "conflict";
764
+ }
765
+ o = o ?? track(toolUseId, {
766
+ toolUseId, taskId, kind: "task",
767
+ inputDigest: toolUses.get(toolUseId)?.inputDigest ?? null,
768
+ background: true, placeholder: false,
769
+ });
770
+ if (!o)
771
+ return "applied";
635
772
  o.taskId = taskId;
636
- const merged = mergePlaceholder(o, taskId);
637
- if (merged === "conflict")
638
- return merged;
773
+ bindDeferredStop(o);
774
+ const merged = mergePlaceholder(o, taskId);
775
+ if (merged === "conflict")
776
+ return merged;
777
+ }
639
778
  }
640
779
  else {
641
- o = findByTaskId(taskId);
780
+ // A late unkeyed original terminal is inherently indistinguishable from
781
+ // a current-run update; no new correlation protocol is inferred here.
782
+ const current = findCurrentRunByTaskId(taskId);
783
+ if (current && evicted.has(current.key))
784
+ return terminalOnEvicted(current.key, taskId, next);
785
+ o = current ? obligations.get(current.key) : undefined;
642
786
  if (!o) {
643
- for (const [k, ev] of evicted)
644
- if (ev.taskId === taskId)
645
- return terminalOnEvicted(k, next);
646
787
  o = track(`t:${taskId}`, { toolUseId: taskId, taskId, kind: "task", inputDigest: null, background: true, placeholder: true, required: true });
647
788
  if (!o)
648
789
  return "applied";
649
790
  }
650
791
  }
651
- return applyTerminal(o, next);
792
+ if (next === "stopped" && workerStops.has(o.key))
793
+ next = "withdrawn";
794
+ // BAPI-1455: a killed foreground call can deliver its error result before
795
+ // the lifecycle `stopped`. The lifecycle names the real end, so a
796
+ // result-derived `failed` is refined to `stopped` (one ending, not two).
797
+ if (next === "stopped" && o.state === "failed" && o.failedByResult) {
798
+ o.state = "stopped";
799
+ o.failedByResult = false;
800
+ return "applied";
801
+ }
802
+ const applied = applyTerminal(o, next === "failed" ? classifyFailure(o) : next);
803
+ // A lifecycle failure confirms a result-derived one: later teardown noise
804
+ // must not refine it.
805
+ if (o.state === "failed")
806
+ o.failedByResult = false;
807
+ return applied;
652
808
  };
653
809
  const handleSnapshot = (e) => {
654
810
  const tasks = e["tasks"];
@@ -673,13 +829,7 @@ export function createRequiredWorkLedger(init) {
673
829
  // A task the snapshot lists but no start/tool use explains yet is an
674
830
  // UNKNOWN obligation, required until a lifecycle event resolves it.
675
831
  for (const tid of ids) {
676
- if (findByTaskId(tid))
677
- continue;
678
- let wasEvicted = false;
679
- for (const ev of evicted.values())
680
- if (ev.taskId === tid)
681
- wasEvicted = true;
682
- if (wasEvicted)
832
+ if (findCurrentRunByTaskId(tid))
683
833
  continue;
684
834
  track(`t:${tid}`, { toolUseId: tid, taskId: tid, kind: "task", inputDigest: null, background: true, placeholder: true, required: true });
685
835
  }
@@ -779,19 +929,14 @@ export function createRequiredWorkLedger(init) {
779
929
  const required = [];
780
930
  const optional = [];
781
931
  for (const tid of latestSnapshot ?? []) {
782
- const o = findByTaskId(tid);
932
+ const o = findCurrentRunByTaskId(tid);
783
933
  if (!o) {
784
- let evictedResolved = false;
785
- for (const ev of evicted.values())
786
- if (ev.taskId === tid)
787
- evictedResolved = true;
788
- if (!evictedResolved)
789
- required.push(tid);
934
+ required.push(tid);
790
935
  continue;
791
936
  }
792
937
  if (!isLive(o))
793
938
  continue;
794
- (o.required ? required : optional).push(tid);
939
+ (obligations.get(o.key)?.required !== false ? required : optional).push(tid);
795
940
  }
796
941
  return { required, optional };
797
942
  };
@@ -814,7 +959,7 @@ export function createRequiredWorkLedger(init) {
814
959
  return all.map((o) => {
815
960
  // Only a terminal non-success is replaceable: live work is not superseded,
816
961
  // and a `conflict` is contradictory evidence, not a failure to retry.
817
- const replaceable = o.state === "failed" || o.state === "stopped" || o.state === "withdrawn";
962
+ const replaceable = o.state === "failed" || o.state === "failed_to_start" || o.state === "stopped" || o.state === "withdrawn";
818
963
  return { o, state: replaceable && succeededLater(o) ? "replaced" : o.state };
819
964
  });
820
965
  };
@@ -869,7 +1014,7 @@ export function createRequiredWorkLedger(init) {
869
1014
  // A snapshot id whose obligation is already listed (by its tool_use_id or as
870
1015
  // a placeholder) is the same work: list it once, not under both handles.
871
1016
  for (const tid of snap.required)
872
- if (!liveRequired.includes(tid) && !findByTaskId(tid))
1017
+ if (!liveRequired.includes(tid) && !findCurrentRunByTaskId(tid))
873
1018
  liveRequired.push(tid);
874
1019
  for (const tid of snap.optional)
875
1020
  if (!liveOptional.includes(tid))
@@ -886,11 +1031,11 @@ export function createRequiredWorkLedger(init) {
886
1031
  onlyBashBg = false;
887
1032
  }
888
1033
  for (const tid of snap.required) {
889
- const o = findByTaskId(tid);
1034
+ const o = findCurrentRunByTaskId(tid);
890
1035
  if (!o || o.kind !== "bash_bg")
891
1036
  onlyBashBg = false;
892
- else if (!bashBgIds.includes(o.taskId ?? o.toolUseId))
893
- bashBgIds.push(o.taskId ?? o.toolUseId);
1037
+ else if (!bashBgIds.includes(o.taskId ?? o.key))
1038
+ bashBgIds.push(o.taskId ?? o.key);
894
1039
  }
895
1040
  return {
896
1041
  readyToClose,
@@ -901,6 +1046,12 @@ export function createRequiredWorkLedger(init) {
901
1046
  turnOpen,
902
1047
  finalResultNoOpenTurn: resultsSeen > 0 && latestIsResult && !turnOpen,
903
1048
  liveRequiredBashBg: onlyBashBg ? bashBgIds : [],
1049
+ liveRequiredAfterFinal: resultsSeen > 0 && latestIsResult && !turnOpen
1050
+ ? [...new Set([
1051
+ ...[...obligations.values()].filter((o) => isLive(o) && o.required).map((o) => o.taskId ?? o.toolUseId),
1052
+ ...snap.required,
1053
+ ])]
1054
+ : [],
904
1055
  };
905
1056
  };
906
1057
  // One predicate for "a required task is still pending", shared by the envelope
@@ -935,7 +1086,10 @@ export function createRequiredWorkLedger(init) {
935
1086
  if (snap.required.length > 0 || unexplainedPlaceholder)
936
1087
  reasons.add("stale_task_snapshot");
937
1088
  const sufficient = reasons.size === 0;
938
- const unresolvedRequired = effective.some(({ o, state }) => o.required && !TASK_RESOLVING_STATES.has(state));
1089
+ // BAPI-1455: a command whose own terminal result was delivered is resolved,
1090
+ // whatever its exit code; its recorded state is kept as it is.
1091
+ const delivered = (o, state) => o.resultDelivered && DELIVERY_RESOLVABLE_KINDS.has(o.kind) && DELIVERY_RESOLVABLE_STATES.has(state);
1092
+ const unresolvedRequired = effective.some(({ o, state }) => o.required && !isTaskResolved({ kind: o.kind, state, result_delivered: delivered(o, state) }));
939
1093
  const anyConflict = effective.some(({ state }) => state === "conflict");
940
1094
  let execution = unresolvedRequired || anyConflict || snap.required.length > 0 || unexplainedPlaceholder ? "incomplete" : "complete";
941
1095
  if (!sufficient && execution === "complete")
@@ -948,6 +1102,7 @@ export function createRequiredWorkLedger(init) {
948
1102
  state: state === "pending" ? "unknown" : state,
949
1103
  required: o.required,
950
1104
  input_digest: o.inputDigest,
1105
+ ...(delivered(o, state) ? { result_delivered: true } : {}),
951
1106
  }));
952
1107
  const satisfied = new Set((facts.satisfiedObligations ?? [])
953
1108
  .map(normalizeObligation)
@@ -979,7 +1134,7 @@ export function createRequiredWorkLedger(init) {
979
1134
  };
980
1135
  });
981
1136
  const candidate = {
982
- evidence_version: facts.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
1137
+ evidence_version: facts.verificationFacts?.deferred_obligation_identities !== undefined ? 3 : facts.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
983
1138
  runtime: { ...facts.runtime },
984
1139
  binding: { ...binding, final_head_sha: facts.finalHeadSha },
985
1140
  session_id: sessionId,
@@ -1031,9 +1186,9 @@ export function createRequiredWorkLedger(init) {
1031
1186
  export const CLOSURE_DEBOUNCE_MS = 1500;
1032
1187
  /**
1033
1188
  * The absolute post-result window (BAPI-1395). Once a final result is in, no
1034
- * turn is open, and the only live required work is the worker's own background
1035
- * Bash, stdin is closed at most this long after the window opens — whatever the
1036
- * worker does meanwhile. Normal early closure is unchanged.
1189
+ * turn is open, and any required work remains, stdin closes within this window.
1190
+ * A new turn cancels it; a later final result opens a fresh window. Background
1191
+ * activity never extends it, and the absolute deadline always takes precedence.
1037
1192
  */
1038
1193
  export const COMPLETION_WINDOW_MS = 30 * 60 * 1000;
1039
1194
  /**
@@ -1046,13 +1201,13 @@ export const COMPLETION_WINDOW_MS = 30 * 60 * 1000;
1046
1201
  * caller drains to EOF and the ledger recomputes, so a late turn still
1047
1202
  * invalidates the apparent closure (`late_activity_after_close`).
1048
1203
  *
1049
- * The nudge: at most one, only when a turn ended while ONLY server-declared
1204
+ * The optional-watcher nudge: at most one, only when a turn ended while ONLY server-declared
1050
1205
  * optional watchers are live. It is never sent while required work is live and
1051
1206
  * never asks for a broad stop — cancelling required work is not the executor's
1052
1207
  * call.
1053
1208
  *
1054
1209
  * The completion window (BAPI-1395): when the final result is in and only
1055
- * required `bash_bg` work is live, one fixed end `min(opened + window, deadline)`
1210
+ * any required work is live, one fixed end `min(opened + window, deadline)`
1056
1211
  * is set. A new worker turn cancels it; background activity does not. The one
1057
1212
  * nudge (shared with the optional-watcher nudge, so a job gets at most one)
1058
1213
  * asks the worker to stop only its own tasks that are merely waiting. Closing
@@ -1127,7 +1282,7 @@ export function createClosureSupervisor(opts) {
1127
1282
  onUndelivered();
1128
1283
  });
1129
1284
  };
1130
- const openWindow = (bashBgIds) => {
1285
+ const openWindow = (requiredIds) => {
1131
1286
  windowOpened = true;
1132
1287
  const now = deps.now();
1133
1288
  const windowEnd = Math.min(now + windowMs, opts.deadlineAtMs);
@@ -1141,7 +1296,7 @@ export function createClosureSupervisor(opts) {
1141
1296
  }, Math.max(0, windowEnd - now));
1142
1297
  }
1143
1298
  if (!nudged && opts.buildWindowNudge) {
1144
- sendNudge(opts.buildWindowNudge([...bashBgIds]), () => {
1299
+ sendNudge(opts.buildWindowNudge([...requiredIds]), () => {
1145
1300
  nudgeAwaitingReply = false;
1146
1301
  });
1147
1302
  }
@@ -1165,8 +1320,8 @@ export function createClosureSupervisor(opts) {
1165
1320
  nudgeAwaitingReply = false;
1166
1321
  return;
1167
1322
  }
1168
- if (!windowOpened && s.finalResultNoOpenTurn && s.liveRequiredBashBg.length > 0) {
1169
- openWindow(s.liveRequiredBashBg);
1323
+ if (!windowOpened && s.finalResultNoOpenTurn && s.liveRequiredAfterFinal.length > 0) {
1324
+ openWindow(s.liveRequiredAfterFinal);
1170
1325
  return;
1171
1326
  }
1172
1327
  if (!s.readyToClose)
@@ -16,6 +16,8 @@
16
16
  import { createWorktreeForTicket } from "../worktree-core.js";
17
17
  import { ContinuationLineageMismatch, ContinuationWorktreeMissing, ExecutorNamedError, WorktreeLostBeforePush, } from "./job-errors.js";
18
18
  import { assertWorktreeOnBranch, listGitWorktrees, findWorktreeByBranch, remoteBranchExists as remoteBranchExistsDefault, } from "./worktree-inspection.js";
19
+ import { ciFixContextError } from "./prompt-spec.js";
20
+ import { fastForwardRetainedWorktreeToBoundHead } from "./worktree.js";
19
21
  /** The canonical WIP checkpoint commit message (executor-run, deterministic). */
20
22
  export const RESUME_WIP_COMMIT_MESSAGE = "wip: auto-checkpoint before resume";
21
23
  function asString(value) {
@@ -189,11 +191,34 @@ export async function verifyContinuationHeadLineage(worktreePath, boundHeadSha,
189
191
  throw new ExecutorNamedError(ContinuationLineageMismatch, `continuation worktree HEAD does not descend from the authorized head ${boundHeadSha.slice(0, 12)}`);
190
192
  }
191
193
  }
194
+ /**
195
+ * BAPI-1514: the bound head a REFRESHED ci_fix continuation's evidence
196
+ * describes, or null for every other job. Non-null only when the job is a
197
+ * `ci_fix` continuation whose `ci_resolved_inputs.evidence_head_sha` is a full
198
+ * 40-hex SHA equal to both `ci_resolved_inputs.head_sha` and the binding's
199
+ * bound head. A legacy payload (no `evidence_head_sha`) or any mismatch is null,
200
+ * which keeps the retained-work protocol byte for byte (pure).
201
+ */
202
+ export function readRefreshedCiFixEvidenceHead(job, binding) {
203
+ if (job.job_type !== "ci_fix" || binding.boundHeadSha === null)
204
+ return null;
205
+ const payload = job.payload && typeof job.payload === "object" ? job.payload : null;
206
+ const inputs = payload?.ci_resolved_inputs;
207
+ if (!inputs || typeof inputs !== "object" || Array.isArray(inputs))
208
+ return null;
209
+ const raw = inputs;
210
+ const evidence = typeof raw.evidence_head_sha === "string" ? raw.evidence_head_sha.trim().toLowerCase() : "";
211
+ const head = typeof raw.head_sha === "string" ? raw.head_sha.trim().toLowerCase() : "";
212
+ if (!FULL_SHA.test(evidence) || evidence !== head || evidence !== binding.boundHeadSha)
213
+ return null;
214
+ return evidence;
215
+ }
192
216
  /**
193
217
  * Locate and verify a continuation's RETAINED worktree, then checkpoint it.
194
218
  *
195
219
  * Order: resolve the existing worktree (never recreate) -> verify its branch ->
196
- * verify its head lineage -> WIP-checkpoint dirty state. A missing worktree is a
220
+ * (BAPI-1514, refreshed ci_fix only) fast-forward a clean worktree to the bound
221
+ * head -> verify its head lineage -> WIP-checkpoint dirty state. A missing worktree is a
197
222
  * refusal ({@link ContinuationWorktreeMissing}), not a recreation from origin:
198
223
  * recreation cannot restore unpushed commits, and continuing without them would
199
224
  * silently discard exactly the work the continuation exists to carry. The
@@ -211,6 +236,18 @@ export async function prepareRetainedContinuationWorktree(job, binding, seams) {
211
236
  throw new ExecutorNamedError(ContinuationWorktreeMissing, `continuation for '${expectedBranch}' found no retained worktree; it is never recreated from origin because unpushed work cannot be recovered that way`);
212
237
  }
213
238
  await assertWorktreeOnBranch(seams.runCommand, worktreePath, expectedBranch);
239
+ // BAPI-1514: a refreshed ci_fix continuation describes its bound head. Move a
240
+ // clean retained worktree forward to it before the lineage proof; every
241
+ // outcome that did not reach the bound head falls through to that proof,
242
+ // which refuses unchanged. Only a moved remote tip refuses here, as the
243
+ // existing stale-head refusal, because the evidence no longer describes it.
244
+ const refreshedHead = readRefreshedCiFixEvidenceHead(job, binding);
245
+ if (refreshedHead !== null) {
246
+ const fastForward = seams.fastForwardRetainedWorktree ?? fastForwardRetainedWorktreeToBoundHead;
247
+ const advanced = await fastForward(worktreePath, expectedBranch, refreshedHead, seams.runCommand);
248
+ if (advanced.kind === "remote_moved")
249
+ throw ciFixContextError(job, "stale_head");
250
+ }
214
251
  await verifyContinuationHeadLineage(worktreePath, binding.boundHeadSha, seams);
215
252
  const checkpoint = await autoCheckpointDirtyState(worktreePath, seams, CONTINUATION_WIP_COMMIT_MESSAGE);
216
253
  return { worktreePath, branch: expectedBranch, checkpointed: checkpoint.committed };