@bridge_gpt/mcp-server 0.2.53 → 0.2.55

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 (103) hide show
  1. package/README.md +116 -56
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-launchers/claude.js +3 -3
  5. package/build/agent-launchers/prompt.js +8 -11
  6. package/build/agent-notes.js +178 -0
  7. package/build/agent-registry.js +5 -2
  8. package/build/agent-utils.js +58 -0
  9. package/build/agents.generated.js +1 -1
  10. package/build/base-ref.js +33 -9
  11. package/build/bounded-wait.js +174 -0
  12. package/build/codex-skill-adapter.js +55 -0
  13. package/build/commands.generated.js +6 -5
  14. package/build/conductor/bridge-api-client.js +235 -14
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +134 -98
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +120 -4
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/run-branch.js +137 -0
  32. package/build/conductor/store.js +2 -2
  33. package/build/conductor/supervisor-runtime.js +1 -1
  34. package/build/conductor/test-run-branch-vectors.js +165 -0
  35. package/build/conductor-bin.js +2 -139
  36. package/build/conductor-claude-hook-bin.js +2 -2
  37. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  38. package/build/conductor-removed-stub-bin.js +30 -0
  39. package/build/docs.generated.js +1 -1
  40. package/build/doctor.js +145 -18
  41. package/build/drive-epic.js +752 -90
  42. package/build/epic-implementer-bin.js +145 -0
  43. package/build/epic-implementer-bundle-cli.js +264 -0
  44. package/build/epic-implementer-claude-hook-bin.js +3 -0
  45. package/build/epic-integration-pr.js +5 -3
  46. package/build/executor/claim-scope.js +104 -0
  47. package/build/executor/cli.js +14 -25
  48. package/build/executor/env-file-guard.js +82 -3
  49. package/build/executor/env.js +6 -0
  50. package/build/executor/interrupted-worktree.js +60 -0
  51. package/build/executor/job-errors.js +45 -0
  52. package/build/executor/job-runner.js +334 -9
  53. package/build/executor/job-types.js +25 -9
  54. package/build/executor/merge-tree-classifier.js +171 -0
  55. package/build/executor/reconcile-overlap-governance.js +129 -0
  56. package/build/executor/reconcile-overlap-job.js +989 -0
  57. package/build/executor/reconcile-overlap-types.js +14 -0
  58. package/build/executor/spawn-job-driver.js +1 -0
  59. package/build/executor/types.js +2 -0
  60. package/build/executor/worker-finalization.js +25 -2
  61. package/build/executor/worker-guard-hook.js +15 -7
  62. package/build/implement-epic/bridge-client.js +773 -0
  63. package/build/implement-epic/checkpoint-store.js +542 -0
  64. package/build/implement-epic/cli.js +3158 -0
  65. package/build/implement-epic/cut-protocol.js +392 -0
  66. package/build/implement-epic/lock.js +302 -0
  67. package/build/implement-epic/pr-state.js +286 -0
  68. package/build/implement-epic/spawn.js +113 -0
  69. package/build/index.js +657 -481
  70. package/build/init.js +72 -8
  71. package/build/install-bridge-conductor.js +5 -5
  72. package/build/install-bridge.js +403 -70
  73. package/build/local-artifact-storage.js +130 -0
  74. package/build/mcp-host-config.js +22 -60
  75. package/build/mcp-host-entry-adapter.js +18 -0
  76. package/build/mcp-host-targets.js +1 -21
  77. package/build/merge-pull-request.js +1 -1
  78. package/build/pipelines.generated.js +23 -16
  79. package/build/plan-epic-conductor-eligibility.js +1 -1
  80. package/build/plane/cli.js +321 -41
  81. package/build/plane/manifest.js +209 -1
  82. package/build/plane/member-roster.js +70 -0
  83. package/build/plane/preflight.js +128 -12
  84. package/build/plane/shutdown.js +18 -5
  85. package/build/plane/status.js +35 -1
  86. package/build/plane/supervisor.js +546 -164
  87. package/build/plane/types.js +25 -2
  88. package/build/polling-policy.js +72 -0
  89. package/build/readiness-check.js +3 -3
  90. package/build/readme.generated.js +1 -1
  91. package/build/review-generation.js +219 -0
  92. package/build/run-unit-tests-launcher.js +6 -1
  93. package/build/setup-epic.js +567 -38
  94. package/build/start-tickets-conductor.js +8 -7
  95. package/build/ticket-key-utils.js +4 -3
  96. package/build/ticket-review-artifact-gate.js +461 -0
  97. package/build/upgrade-cli.js +5 -26
  98. package/build/version.generated.js +3 -3
  99. package/build/worker-guard-hook-bin.js +1 -1
  100. package/docs/CONDUCTOR.md +8 -6
  101. package/docs/install/mcp-tool-integrations.md +23 -1
  102. package/package.json +5 -3
  103. package/pipelines/review-ticket.json +17 -4
@@ -139,6 +139,51 @@ export const StaleArtifactCleanupFailed = "ContractError.StaleArtifactCleanupFai
139
139
  * than proceeding on an unverified premise.
140
140
  */
141
141
  export const WorkerEnvFilePresent = "ContractError.WorkerEnvFilePresent";
142
+ /**
143
+ * BAPI-1152 — `reconcile_overlap` failure kinds. Each names ONE way the
144
+ * wrapper's merge-into-child could not reach a verified push, so an operator
145
+ * reading the row knows which fact to go look at. None of them is a verdict: a
146
+ * refused governance change and a moved head are NOT failures — they complete
147
+ * with a fact envelope — so they have no kind here.
148
+ *
149
+ * - `ContractError.ReconcileOverlapPayload` — the payload failed validation
150
+ * before any subprocess ran (missing/blank input SHA, unknown schema version,
151
+ * malformed command or region record).
152
+ * - `ContractError.ReconcileOverlapBranchMismatch` — the prepared worktree's
153
+ * branch is not the job's top-level `expected_branch`, so the push destination
154
+ * cannot be trusted.
155
+ * - `ReconcileOverlapMergeFailed` — fetching the inputs, `git merge-tree`, or the
156
+ * materialized `git merge` failed operationally (not a conflict).
157
+ * - `ReconcileOverlapNothingToMerge` — the child head already contains the
158
+ * required epic commit; there is nothing to reconcile.
159
+ * - `ReconcileOverlapConflictsUnresolved` — the worker exited with conflict
160
+ * markers or unmerged index entries still present.
161
+ * - `ReconcileOverlapWorkerRewroteHistory` — the worker moved `HEAD`, committed,
162
+ * or removed `MERGE_HEAD`; commits are the wrapper's alone.
163
+ * - `ReconcileOverlapVerificationRed` — a local verification command stayed red,
164
+ * timed out, or could not run after the reconciliation.
165
+ * - `ReconcileOverlapTopologyRefused` — the candidate is not a merge containing
166
+ * both input heads with the dispatched parents.
167
+ * - `ReconcileOverlapPushUnauthorized` — the executor machine's git credential
168
+ * helper refused the push or the remote read.
169
+ * - `ReconcileOverlapPushRejected` — the remote refused the guarded push for a
170
+ * reason other than a moved head.
171
+ * - `ReconcileOverlapPushUnfinalized` — the push reported success but origin does
172
+ * not carry the pushed head (the reconcile-specific counterpart of
173
+ * `WorkerFinalizationSavedButUnfinalized`, whose "resume from the branch"
174
+ * guidance would mislead here: the worktree has been reset).
175
+ */
176
+ export const ReconcileOverlapPayload = "ContractError.ReconcileOverlapPayload";
177
+ export const ReconcileOverlapBranchMismatch = "ContractError.ReconcileOverlapBranchMismatch";
178
+ export const ReconcileOverlapMergeFailed = "ReconcileOverlapMergeFailed";
179
+ export const ReconcileOverlapNothingToMerge = "ReconcileOverlapNothingToMerge";
180
+ export const ReconcileOverlapConflictsUnresolved = "ReconcileOverlapConflictsUnresolved";
181
+ export const ReconcileOverlapWorkerRewroteHistory = "ReconcileOverlapWorkerRewroteHistory";
182
+ export const ReconcileOverlapVerificationRed = "ReconcileOverlapVerificationRed";
183
+ export const ReconcileOverlapTopologyRefused = "ReconcileOverlapTopologyRefused";
184
+ export const ReconcileOverlapPushUnauthorized = "ReconcileOverlapPushUnauthorized";
185
+ export const ReconcileOverlapPushRejected = "ReconcileOverlapPushRejected";
186
+ export const ReconcileOverlapPushUnfinalized = "ReconcileOverlapPushUnfinalized";
142
187
  /** Rendered in place of an empty name list, so "none" is never ambiguous. */
143
188
  export const EMPTY_MCP_SERVER_NAME_MARKER = "none";
144
189
  /** Bound the rendered name lists so a pathological registration cannot spam a job row. */
@@ -40,6 +40,8 @@ import { acquireExecutorWorktreeLock, } from "./worktree-lock.js";
40
40
  import { activateJobLog, deactivateJobLog, registerExecutorJobLog, markExecutorJobLogFinished, } from "./job-log-registry.js";
41
41
  import { executorViewerTabsEnabled, openExecutorViewerTab } from "./viewer-tabs.js";
42
42
  import { isImplementationStyleJobType, isRecoveryJobType, isSpawnJobType } from "./job-types.js";
43
+ import { cleanupInterruptedWorktree } from "./interrupted-worktree.js";
44
+ import { lazyProviderCheckPoller, runReconcileOverlapJob, } from "./reconcile-overlap-job.js";
43
45
  import { resolveExecutorJobBaseBranch } from "./base-branch.js";
44
46
  import { resolveExecutorJobIndexScope } from "./index-scope.js";
45
47
  import { validateWorkerFinalization } from "./worker-finalization.js";
@@ -48,6 +50,23 @@ import { buildPrBaseContractLaunchInstruction } from "../pr-base-contract.js";
48
50
  import { resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
49
51
  import { collectGitTelemetry, collectRemoteTrackingSha, createMcpSurfaceObserver, evaluatePreSpawnGitVerification, formatWorkerRateLimitAdvisory, normalizeMcpServerNames, } from "./observation.js";
50
52
  import { isRecognizedServerName, MCP_SERVER_NAME } from "../mcp-identity.js";
53
+ /**
54
+ * Bound on the pre-spawn git tracked-file lookup (BAPI-1104, R81).
55
+ *
56
+ * This runs at the LAST step before every worker spawn, so a hung `git ls-files`
57
+ * would stall spawning rather than merely delay one exemption. Five seconds is
58
+ * far beyond what an index read of a handful of pathspecs takes, and a timeout
59
+ * degrades to "exempt nothing" rather than to a refusal.
60
+ */
61
+ const ENV_FILE_TRACKED_LOOKUP_TIMEOUT_MS = 5_000;
62
+ /**
63
+ * The ONE fixed line emitted when that lookup fails. No path (a worktree path
64
+ * carries an operator's username), no filename, no errno, no command output, no
65
+ * exception text — the same bar `WORKER_ENV_FILE_PRESENT_MESSAGE` and the guard's
66
+ * closed categories hold. It is advisory: the spawn proceeds, with nothing
67
+ * exempted.
68
+ */
69
+ const ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE = "[executor] tracked env-file lookup unavailable; exempting nothing";
51
70
  /** Default runtime for the no-op smoke process (ms). */
52
71
  const DEFAULT_SMOKE_DURATION_MS = 100;
53
72
  /** ProcessClassification maps 1:1 onto the wire ExecutorClassification. */
@@ -596,6 +615,21 @@ control) {
596
615
  if (job.job_type === "smoke") {
597
616
  return runSmokeJob(job, httpClient, options, deps, ownership, observation, control);
598
617
  }
618
+ // BAPI-1152: `reconcile_overlap` takes its OWN branch, after `merge`/`smoke`
619
+ // and before the generic spawn path. It needs a prepared, locked worktree like
620
+ // a spawn job, but its body is the reconcile module's state machine: the
621
+ // payload is validated there BEFORE any subprocess, and the worker — when one
622
+ // is needed at all — is launched through the SAME locked `runSpawnJob` path
623
+ // via a driver, so there is still exactly one worker spawn boundary.
624
+ if (job.job_type === "reconcile_overlap") {
625
+ return await runReconcileOverlapJob(job, {
626
+ fail: async (failure) => {
627
+ await httpClient.fail(job, failure);
628
+ return { status: "failed", reason: "reconcile_overlap_payload" };
629
+ },
630
+ runLockedSpawn: (driver) => runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver),
631
+ }, buildReconcileOverlapSeams(deps));
632
+ }
599
633
  // `implement`/`resume`/`spec_review` and the recovery jobs (`remediate`/
600
634
  // `ci_fix`/`rebase`) all run through the real headless-spawn path — dispatched
601
635
  // via the shared `isSpawnJobType` policy (`job-types.ts`) AFTER the dedicated
@@ -629,6 +663,18 @@ control) {
629
663
  * from the supervised `ProcessRunResult`/`ownership`, not from this adapter).
630
664
  */
631
665
  function createMergeFlowProcess(startMerge) {
666
+ const flow = createFlowProcess(startMerge, (result) => result.ok);
667
+ return { proc: flow.proc, getOutcome: flow.getOutcome, getError: flow.getError };
668
+ }
669
+ /**
670
+ * The generic form of {@link createMergeFlowProcess} (BAPI-1152): adapt any
671
+ * in-process flow into an {@link OwnedChildProcess} so it is supervised by the
672
+ * SAME heartbeat/deadman/timeout path as a worker. `settled` resolves once the
673
+ * flow's own promise has settled — after a timeout or kill the supervised
674
+ * process resolves immediately, but the flow may still be finishing a bounded
675
+ * subprocess, and a caller that is about to reset the worktree must wait for it.
676
+ */
677
+ function createFlowProcess(start, isOk) {
632
678
  const controller = new AbortController();
633
679
  let outcome;
634
680
  let error;
@@ -637,12 +683,12 @@ function createMergeFlowProcess(startMerge) {
637
683
  const waitPromise = new Promise((resolve) => {
638
684
  settleWait = resolve;
639
685
  });
640
- // Start the merge flow immediately; capture its resolution/rejection.
641
- startMerge(controller.signal).then((result) => {
686
+ // Start the flow immediately; capture its resolution/rejection.
687
+ const settled = start(controller.signal).then((result) => {
642
688
  outcome = result;
643
689
  if (!done) {
644
690
  done = true;
645
- settleWait({ exitCode: result.ok ? 0 : 1, signal: null });
691
+ settleWait({ exitCode: isOk(result) ? 0 : 1, signal: null });
646
692
  }
647
693
  }, (err) => {
648
694
  error = err;
@@ -670,6 +716,7 @@ function createMergeFlowProcess(startMerge) {
670
716
  proc,
671
717
  getOutcome: () => outcome,
672
718
  getError: () => error,
719
+ settled,
673
720
  };
674
721
  }
675
722
  /**
@@ -867,6 +914,131 @@ async function runSmokeJob(job, httpClient, options, deps, ownership, observatio
867
914
  }, job, control);
868
915
  return terminalToRunResult(terminal, "completed");
869
916
  }
917
+ /** Build the reconcile module's seams from executor deps (BAPI-1152). */
918
+ function buildReconcileOverlapSeams(deps) {
919
+ return {
920
+ runCommand: deps.runCommand,
921
+ readFile: deps.readFile,
922
+ writeFile: deps.writeFile,
923
+ mkdir: deps.mkdir,
924
+ now: deps.now,
925
+ sleep: deps.sleep,
926
+ env: deps.env,
927
+ pollProviderChecks: lazyProviderCheckPoller({
928
+ env: deps.env,
929
+ cwd: deps.cwd,
930
+ homedir: deps.homedir,
931
+ platform: deps.platform,
932
+ readFile: deps.readFile,
933
+ stat: deps.stat,
934
+ }),
935
+ errorLog: deps.errorLog,
936
+ };
937
+ }
938
+ /** Bound on waiting for an aborted driver phase to finish its in-flight command. */
939
+ const DRIVER_PHASE_SETTLE_MAX_MS = 240_000;
940
+ /**
941
+ * Run one driver phase under the SAME heartbeat/deadman/timeout supervision a
942
+ * worker gets (BAPI-1152). A deterministic phase can run for many minutes — local
943
+ * verification commands, a CI wait — and without heartbeats the janitor would
944
+ * re-queue a healthy job. On a timeout or a lost claim the phase is aborted and
945
+ * then AWAITED (bounded) before returning, so the caller never resets the
946
+ * worktree underneath a git command the phase is still running.
947
+ */
948
+ async function superviseDriverPhase(params) {
949
+ const flow = createFlowProcess(params.run, () => true);
950
+ const procResult = await superviseProcess({
951
+ job: params.job,
952
+ httpClient: params.httpClient,
953
+ options: params.options,
954
+ deps: params.deps,
955
+ ownership: params.ownership,
956
+ observation: params.observation,
957
+ proc: flow.proc,
958
+ timeoutSeconds: Math.max(1, params.timeoutSeconds),
959
+ collectTelemetry: params.collectTelemetry,
960
+ control: params.control,
961
+ });
962
+ await Promise.race([flow.settled, params.deps.sleep(DRIVER_PHASE_SETTLE_MAX_MS)]);
963
+ if (procResult.serverStopRequested)
964
+ return { kind: "server_stop" };
965
+ if (params.ownership.abandoned)
966
+ return { kind: "abandoned" };
967
+ if (procResult.classification === "timeout")
968
+ return { kind: "timeout" };
969
+ const error = flow.getError();
970
+ if (error !== undefined)
971
+ return { kind: "error", error };
972
+ const value = flow.getOutcome();
973
+ if (value === undefined)
974
+ return { kind: "error", error: new Error("driver phase produced no outcome") };
975
+ return { kind: "result", value };
976
+ }
977
+ /** Report a driver's terminal decision through the shared terminal-mutation path. */
978
+ async function reportDriverOutcome(outcome, ctx) {
979
+ if (outcome.kind === "complete") {
980
+ const completion = {
981
+ job_type: ctx.job.job_type,
982
+ exit_code: 0,
983
+ classification: "clean_exit",
984
+ result: outcome.result,
985
+ // ONLY a pushed head is reported: the server binds `last_commit_sha` as the
986
+ // job's produced head, and a job that pushed nothing produced none.
987
+ ...(outcome.lastCommitSha ? { last_commit_sha: outcome.lastCommitSha } : {}),
988
+ telemetry: ctx.observation.snapshot(),
989
+ };
990
+ const terminal = await sendTerminalMutationWithAuthReport({
991
+ kind: "complete",
992
+ send: () => ctx.httpClient.complete(ctx.job, completion),
993
+ deps: ctx.deps,
994
+ options: ctx.options,
995
+ ownership: ctx.ownership,
996
+ log: ctx.deps.log,
997
+ }, ctx.job, ctx.control);
998
+ return terminalToRunResult(terminal, "completed");
999
+ }
1000
+ const terminal = await sendTerminalMutationWithAuthReport({
1001
+ kind: "fail",
1002
+ send: () => ctx.httpClient.fail(ctx.job, { ...outcome.failure, telemetry: ctx.observation.snapshot() }),
1003
+ deps: ctx.deps,
1004
+ options: ctx.options,
1005
+ ownership: ctx.ownership,
1006
+ log: ctx.deps.log,
1007
+ }, ctx.job, ctx.control);
1008
+ return terminalToRunResult(terminal, "failed");
1009
+ }
1010
+ /** Restore the worktree and report a driver phase that did not finish. */
1011
+ async function reportDriverPhaseInterruption(phase, ctx) {
1012
+ await ctx.driver.abandon(ctx.driverBaseContext);
1013
+ if (phase.kind === "abandoned") {
1014
+ return { status: "abandoned", reason: ctx.ownership.abandonReason };
1015
+ }
1016
+ if (phase.kind === "server_stop") {
1017
+ await ctx.httpClient.fail(ctx.job, {
1018
+ error_kind: "ServerStopRequested",
1019
+ error_message: "the server requested this run stop; the reconciliation was stopped",
1020
+ classification: "killed",
1021
+ telemetry: ctx.observation.snapshot(),
1022
+ });
1023
+ ctx.ownership.abandoned = true;
1024
+ ctx.ownership.abandonReason = "server_stop";
1025
+ return { status: "abandoned", reason: "server_stop" };
1026
+ }
1027
+ return await reportDriverOutcome({
1028
+ kind: "fail",
1029
+ failure: phase.kind === "timeout"
1030
+ ? {
1031
+ error_kind: "Timeout",
1032
+ error_message: `${ctx.job.job_type} phase exceeded the job timeout`,
1033
+ classification: "timeout",
1034
+ }
1035
+ : {
1036
+ error_kind: "WorkerCrashed",
1037
+ error_message: `${ctx.job.job_type} phase failed: ${secretFreeErrorMessage(phase.error)}`,
1038
+ classification: "crashed",
1039
+ },
1040
+ }, ctx);
1041
+ }
870
1042
  /**
871
1043
  * BAPI-862: the payload key the reconciler stamps with the implement gate's last
872
1044
  * observation reason code. Server-minted and opaque to the executor beyond the
@@ -1031,7 +1203,13 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
1031
1203
  * returns BEFORE either the deny layer or MCP provisioning runs — neither ever
1032
1204
  * touches a worktree that will not become a runnable worker.
1033
1205
  */
1034
- async function runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control) {
1206
+ async function runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control,
1207
+ /**
1208
+ * BAPI-1152: an optional spawn-job driver (`reconcile_overlap`). It runs inside
1209
+ * the SAME lock and the SAME prepared-spawn path as every other spawn job, so it
1210
+ * adds no second worktree entry and no second worker launcher.
1211
+ */
1212
+ driver) {
1035
1213
  // --- Per-job base branch (BAPI-586) ----------------------------------
1036
1214
  // Resolve the effective logical base BEFORE any side effect: the persisted
1037
1215
  // run base (`payload.base_branch`) is authoritative, falling back to the
@@ -1222,6 +1400,7 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
1222
1400
  adapter,
1223
1401
  control,
1224
1402
  lifecycle: spawnLifecycle,
1403
+ driver,
1225
1404
  });
1226
1405
  }
1227
1406
  finally {
@@ -1256,12 +1435,12 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
1256
1435
  * rather than a 500-line indentation nobody can verify by eye.
1257
1436
  */
1258
1437
  async function runPreparedSpawn(params) {
1259
- const { job, httpClient, options, jobOptions, deps, ownership, observation, seams, worktreePath, branch, prep, effectiveBaseBranch, indexScope, permissionPosture, adapter, control, lifecycle, } = params;
1438
+ const { job, httpClient, options, jobOptions, deps, ownership, observation, seams, worktreePath, branch, prep, effectiveBaseBranch, indexScope, permissionPosture, adapter, control, lifecycle, driver, } = params;
1260
1439
  // BAPI-586: PR-producing spawn jobs (implement/resume/remediate/ci_fix/rebase)
1261
1440
  // are told to open the PR against the injected run base. Verdict-only
1262
1441
  // `spec_review` produces no PR, so it is excluded from the instruction while
1263
1442
  // still using the pinned base for its fresh worktree.
1264
- const prompt = isImplementationStyleJobType(job.job_type)
1443
+ let prompt = isImplementationStyleJobType(job.job_type)
1265
1444
  ? `${prep.prompt} ${buildPrBaseContractLaunchInstruction()}`
1266
1445
  : prep.prompt;
1267
1446
  // --- Timeout contract ------------------------------------------------
@@ -1274,6 +1453,49 @@ async function runPreparedSpawn(params) {
1274
1453
  });
1275
1454
  return { status: "failed", reason: "timeout_contract" };
1276
1455
  }
1456
+ // --- Spawn-job driver: the pre-worker phase (BAPI-1152) --------------
1457
+ // Runs INSIDE the lock and before every worker-owned side effect. A driver may
1458
+ // finish the whole job here (a clean, green reconciliation pushes with no
1459
+ // worker at all), or hand back the ONE worker it needs and the pre-spawn state
1460
+ // it has already proven. The job's overall timeout budget spans every phase.
1461
+ const jobDeadlineMs = deps.now() + timeout.timeoutSeconds * 1000;
1462
+ const remainingMs = () => Math.max(0, jobDeadlineMs - deps.now());
1463
+ const driverBaseContext = {
1464
+ worktreePath,
1465
+ branch,
1466
+ preparedHeadSha: prep.expectedHeadSha,
1467
+ effectiveBaseBranch,
1468
+ };
1469
+ const driverTelemetry = () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch);
1470
+ let driverStep;
1471
+ if (driver) {
1472
+ const phase = await superviseDriverPhase({
1473
+ job,
1474
+ httpClient,
1475
+ options: jobOptions,
1476
+ deps,
1477
+ ownership,
1478
+ observation,
1479
+ control,
1480
+ timeoutSeconds: Math.ceil(remainingMs() / 1000),
1481
+ collectTelemetry: driverTelemetry,
1482
+ run: (signal) => driver.beforeWorker({ ...driverBaseContext, signal, remainingMs }),
1483
+ });
1484
+ if (phase.kind !== "result") {
1485
+ return await reportDriverPhaseInterruption(phase, {
1486
+ job, httpClient, options, deps, ownership, observation, control, driver, driverBaseContext,
1487
+ });
1488
+ }
1489
+ if (phase.value.kind === "finish") {
1490
+ return await reportDriverOutcome(phase.value.outcome, {
1491
+ job, httpClient, options, deps, ownership, observation, control,
1492
+ });
1493
+ }
1494
+ driverStep = phase.value;
1495
+ // The driver's prompt is used VERBATIM: it is server-rendered, and the PR-base
1496
+ // launch instruction would tell a worker with no push authority to open a PR.
1497
+ prompt = driverStep.prompt;
1498
+ }
1277
1499
  // --- Pre-spawn git verification (FAIL-CLOSED, BAPI-731 WS-E) ---------
1278
1500
  // FIRST of the pre-spawn contracts, and deliberately BEFORE every
1279
1501
  // executor-owned write to the worktree (deny layer, MCP provisioning, worker
@@ -1287,8 +1509,15 @@ async function runPreparedSpawn(params) {
1287
1509
  // untracked-clean / head-verify behavior in `worktree-core.ts`; it is an
1288
1510
  // additional independent check on top of them.
1289
1511
  const preSpawnTelemetry = await collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch);
1290
- const verification = evaluatePreSpawnGitVerification(preSpawnTelemetry, prep.expectedHeadSha);
1512
+ // BAPI-1152: a driver proves its own pre-spawn state (a reconciliation worker
1513
+ // may start inside an in-progress merge, which the generic "clean at the
1514
+ // prepared commit" check would refuse by design).
1515
+ const verification = driverStep
1516
+ ? driverStep.preSpawnVerification
1517
+ : evaluatePreSpawnGitVerification(preSpawnTelemetry, prep.expectedHeadSha);
1291
1518
  if (!verification.ok) {
1519
+ if (driver)
1520
+ await driver.abandon(driverBaseContext);
1292
1521
  // Record the attempt boundary even for a refusal: an attempt that never
1293
1522
  // spawned still started and ended at the observed commit, so the reconciler
1294
1523
  // sees a machine-readable "went nowhere" rather than a silent gap.
@@ -1538,7 +1767,12 @@ async function runPreparedSpawn(params) {
1538
1767
  // BAPI-1020 — becomes `BAPI_WORKER_BRANCH` in the worker environment, which
1539
1768
  // is the deterministic guard's only source of branch identity. Same
1540
1769
  // provenance as the deny-provisioning value above: the prepared worktree.
1541
- workerBranch: branch,
1770
+ //
1771
+ // BAPI-1152: a driver that holds the push itself OMITS it, so the guard
1772
+ // denies every `git push` the worker attempts — the worker has no remote
1773
+ // write authority at all.
1774
+ ...(driver?.omitWorkerBranch ? {} : { workerBranch: branch }),
1775
+ ...(driverStep ? { declaredTouchedFiles: driverStep.declaredTouchedFiles } : {}),
1542
1776
  parentEnv: deps.env,
1543
1777
  });
1544
1778
  assertUsableSpawnShape(spawnShape, adapter);
@@ -1599,6 +1833,49 @@ async function runPreparedSpawn(params) {
1599
1833
  // enforcement: no `stat`, `readlink`, or `readFile` is reachable from here.
1600
1834
  lstat: (target) => lstat(target),
1601
1835
  unlink: (target) => unlink(target),
1836
+ // BAPI-1104 (R81): which candidates git reports as TRACKED. This repository's
1837
+ // own `.env.example` is tracked content, not an operator's environment file,
1838
+ // and the `.env.` prefix rule was deleting it from every worker worktree.
1839
+ //
1840
+ // `deps.runCommand` is `execFile` with `shell: false`, so the basenames are
1841
+ // delivered as inert argv elements and no shell parses them. `--` is
1842
+ // mandatory: it stops git reading any argument as an option (unreachable for
1843
+ // `.env*` names, but the separator is what makes that a property rather than
1844
+ // an argument). `--error-unmatch` is deliberately NOT used — it exits 1 and
1845
+ // prints nothing for the WHOLE invocation as soon as one pathspec is
1846
+ // untracked, so on the mixed tracked/untracked worktree this feature exists
1847
+ // for it would silently report "nothing is tracked" and delete the
1848
+ // `.env.example` anyway. Plain `ls-files` lists only tracked paths and exits
1849
+ // zero, so the tracked subset falls out of stdout directly.
1850
+ listTrackedNames: async (root, candidates) => {
1851
+ let result;
1852
+ try {
1853
+ result = await deps.runCommand("git", ["ls-files", "-z", "--", ...candidates], { cwd: root, timeoutMs: ENV_FILE_TRACKED_LOOKUP_TIMEOUT_MS });
1854
+ }
1855
+ catch {
1856
+ // `runCommand` surfaces a non-zero exit as `exitCode` rather than
1857
+ // throwing, so reaching here means the process could not be run at all.
1858
+ deps.errorLog(ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE);
1859
+ return [];
1860
+ }
1861
+ if (result.exitCode !== 0 || typeof result.stdout !== "string") {
1862
+ // Fail-OPEN into "nothing is exempt": the guard then strips every match,
1863
+ // which is strictly safer than refusing a spawn it can still confirm
1864
+ // clean. One fixed line, and nothing from the command in it — stdout
1865
+ // would carry worktree-relative paths and stderr an errno.
1866
+ deps.errorLog(ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE);
1867
+ return [];
1868
+ }
1869
+ // NUL-delimited, never logged. Filtered to the exact candidate set here as
1870
+ // well as in the guard: the seam's contract is basenames-in / basenames-out
1871
+ // restricted to what was asked about, and a producer that honors its own
1872
+ // contract is easier to reason about than one that relies on its consumer
1873
+ // to clean up after it. The guard's re-intersection stays the enforcement.
1874
+ const asked = new Set(candidates);
1875
+ return result.stdout
1876
+ .split("\0")
1877
+ .filter((name) => name.length > 0 && asked.has(name));
1878
+ },
1602
1879
  platform: deps.platform,
1603
1880
  });
1604
1881
  if (!stripped.ok) {
@@ -1676,7 +1953,9 @@ async function runPreparedSpawn(params) {
1676
1953
  ownership,
1677
1954
  observation,
1678
1955
  proc: supervisedProc,
1679
- timeoutSeconds: timeout.timeoutSeconds,
1956
+ // BAPI-1152: a driver's worker gets what is LEFT of the job budget, since the
1957
+ // pre-worker phase has already spent some of it.
1958
+ timeoutSeconds: driver ? Math.max(1, Math.ceil(remainingMs() / 1000)) : timeout.timeoutSeconds,
1680
1959
  collectTelemetry,
1681
1960
  collectRemoteMarker,
1682
1961
  mcpSurface,
@@ -1726,6 +2005,8 @@ async function runPreparedSpawn(params) {
1726
2005
  if (procResult.serverStopRequested) {
1727
2006
  await closeWorkerLog();
1728
2007
  await finalizeRegistry();
2008
+ if (driver)
2009
+ await driver.abandon(driverBaseContext);
1729
2010
  await httpClient.fail(job, {
1730
2011
  error_kind: "ServerStopRequested",
1731
2012
  error_message: "the server requested this run stop; the owned worker was terminated",
@@ -1753,6 +2034,8 @@ async function runPreparedSpawn(params) {
1753
2034
  await reportWorkerLogSessionInvariant(tee, deps);
1754
2035
  if (ownership.abandoned) {
1755
2036
  await finalizeRegistry();
2037
+ if (driver)
2038
+ await driver.abandon(driverBaseContext);
1756
2039
  return { status: "abandoned", reason: ownership.abandonReason };
1757
2040
  }
1758
2041
  // --- MCP surface containment verdict (FAIL-CLOSED, BAPI-790) ----------
@@ -1773,6 +2056,8 @@ async function runPreparedSpawn(params) {
1773
2056
  const surfaceVerdict = mcpSurface.verdict();
1774
2057
  if (surfaceVerdict.kind === "mismatch") {
1775
2058
  await finalizeRegistry();
2059
+ if (driver)
2060
+ await driver.abandon(driverBaseContext);
1776
2061
  await httpClient.fail(job, {
1777
2062
  error_kind: McpSurfaceMismatch,
1778
2063
  error_message: formatMcpSurfaceMismatch(surfaceVerdict.expected, surfaceVerdict.observed),
@@ -1800,6 +2085,8 @@ async function runPreparedSpawn(params) {
1800
2085
  procResult.classification === "crashed" &&
1801
2086
  procResult.stdoutExcerpt.trim().length === 0) {
1802
2087
  await finalizeRegistry();
2088
+ if (driver)
2089
+ await driver.abandon(driverBaseContext);
1803
2090
  await httpClient.fail(job, {
1804
2091
  error_kind: WorkerStartupFatal,
1805
2092
  error_message: WORKER_STARTUP_FATAL_MESSAGE,
@@ -1826,6 +2113,31 @@ async function runPreparedSpawn(params) {
1826
2113
  "for this spawn");
1827
2114
  }
1828
2115
  const git = observation.git();
2116
+ // BAPI-1152: a driver owns everything after a clean worker exit — staging, the
2117
+ // governance scan, the commits, the guarded push, and finalization.
2118
+ if (driver && procResult.classification === "clean_exit") {
2119
+ await finalizeRegistry();
2120
+ const phase = await superviseDriverPhase({
2121
+ job,
2122
+ httpClient,
2123
+ options: jobOptions,
2124
+ deps,
2125
+ ownership,
2126
+ observation,
2127
+ control,
2128
+ timeoutSeconds: Math.max(1, Math.ceil(remainingMs() / 1000)),
2129
+ collectTelemetry: driverTelemetry,
2130
+ run: (signal) => driver.afterWorker({ ...driverBaseContext, signal, remainingMs }, { classification: procResult.classification, exitCode: procResult.exitCode }),
2131
+ });
2132
+ if (phase.kind !== "result") {
2133
+ return await reportDriverPhaseInterruption(phase, {
2134
+ job, httpClient, options, deps, ownership, observation, control, driver, driverBaseContext,
2135
+ });
2136
+ }
2137
+ return await reportDriverOutcome(phase.value, {
2138
+ job, httpClient, options, deps, ownership, observation, control,
2139
+ });
2140
+ }
1829
2141
  if (procResult.classification === "clean_exit") {
1830
2142
  // Verdict jobs (`spec_review`) forward the fixed-path artifact verbatim; a
1831
2143
  // missing/invalid artifact is a fail-loud `MissingVerdictArtifact`.
@@ -1968,6 +2280,19 @@ async function runPreparedSpawn(params) {
1968
2280
  // a verdict and a reason code only — never the matched line — so nothing from
1969
2281
  // the worker's stream reaches `/fail` through this path.
1970
2282
  await finalizeRegistry();
2283
+ // BAPI-1152: an interrupted worker (timeout or kill) can leave a stale
2284
+ // `index.lock` or an in-progress merge behind — the BAPI-1111 residue that made
2285
+ // the next job's reset fail. Removed for EVERY spawn job, never with a hard
2286
+ // reset; only a driver (`reconcile_overlap`) additionally resets to its anchor.
2287
+ if (procResult.classification === "timeout" || procResult.classification === "killed") {
2288
+ await cleanupInterruptedWorktree(worktreePath, {
2289
+ runCommand: deps.runCommand,
2290
+ removeFile: deps.removeFile ?? ((filePath) => rm(filePath)),
2291
+ errorLog: deps.errorLog,
2292
+ });
2293
+ }
2294
+ if (driver)
2295
+ await driver.abandon(driverBaseContext);
1971
2296
  const authDetection = adapter.authFailureDetection;
1972
2297
  const authFailure = authDetection?.supported === true &&
1973
2298
  authDetection.value.classify(procResult.stdoutExcerpt).notAuthenticated;
@@ -2,9 +2,10 @@
2
2
  * Shared executor job-type policy (BAPI-542, TDD §9).
3
3
  *
4
4
  * Single source of truth for which claimed job types the executor spawns and
5
- * which of those are RECOVERY jobs. Recovery jobs (`remediate`/`ci_fix`/`rebase`)
6
- * resume the ticket's existing branch/PR through the `/implement-ticket <KEY>
7
- * --auto` correction loop rather than cutting a fresh branch off base. Dispatch
5
+ * which of those are RECOVERY jobs. Recovery jobs (`remediate`/`ci_fix`/`rebase`/
6
+ * `reconcile_overlap`) resume the ticket's existing branch/PR rather than cutting
7
+ * a fresh branch off base the first three through the `/implement-ticket <KEY>
8
+ * --auto` correction loop, `reconcile_overlap` through its own job body. Dispatch
8
9
  * (`job-runner.ts` `runClaimedJob`), prompt synthesis (`worker-command.ts`
9
10
  * `resolveExecutorPrompt`), and worktree reuse (`job-runner.ts` `prepareSpawn` →
10
11
  * `worktree.ts` `ensureExecutorWorktree`) all read from here so the recovery /
@@ -17,21 +18,33 @@
17
18
  /**
18
19
  * Recovery job types: enqueued by the server-side reconciler when a gate is
19
20
  * ACTIONABLE (review changes requested → `remediate`, CI failed → `ci_fix`,
20
- * merge conflict → `rebase`). They re-enter the correction loop on the ticket's
21
- * EXISTING branch/PR, so the executor reuses that branch instead of cutting a
22
- * fresh one off base.
21
+ * merge conflict → `rebase`, overlapping sibling at `code_review`
22
+ * `reconcile_overlap`). They work on the ticket's EXISTING branch/PR, so the
23
+ * executor reuses that branch instead of cutting a fresh one off base.
24
+ *
25
+ * BAPI-1152: `reconcile_overlap` is a recovery job for worktree reuse and prompt
26
+ * resolution, but it does NOT re-enter `/implement-ticket`: it always carries an
27
+ * explicit server-rendered `payload.prompt`, which wins over the synthesis in
28
+ * `worker-command.ts`, and `job-runner.ts` dispatches it to its own body.
23
29
  */
24
30
  export const RECOVERY_JOB_TYPES = new Set([
25
31
  "remediate",
26
32
  "ci_fix",
27
33
  "rebase",
34
+ "reconcile_overlap",
28
35
  ]);
29
36
  /**
30
37
  * Real headless-spawn job types (worktree + worker LLM): the first-pass
31
38
  * `implement`, the `resume` continuation (own pre-spawn protocol), the
32
- * `spec_review` verdict job, and the three recovery jobs. `merge` (deterministic,
39
+ * `spec_review` verdict job, and the recovery jobs. `merge` (deterministic,
33
40
  * no worktree/worker) and `smoke` (no-op acceptance) are intentionally EXCLUDED —
34
41
  * they have dedicated dispatch branches BEFORE the generic spawn path.
42
+ *
43
+ * BAPI-1152: `reconcile_overlap` is here because it needs a prepared, locked
44
+ * worktree and MAY spawn a worker — but only CONDITIONALLY. A clean merge whose
45
+ * local checks pass is pushed with no worker at all. It also has a dedicated
46
+ * dispatch branch ahead of the generic spawn path; membership here is what makes
47
+ * worktree preparation and `isImplementationStyleJobType` treat it as a spawn job.
35
48
  */
36
49
  export const SPAWN_JOB_TYPES = new Set([
37
50
  "implement",
@@ -40,6 +53,7 @@ export const SPAWN_JOB_TYPES = new Set([
40
53
  "remediate",
41
54
  "ci_fix",
42
55
  "rebase",
56
+ "reconcile_overlap",
43
57
  ]);
44
58
  /** True only for the exact lowercase recovery job types. */
45
59
  export function isRecoveryJobType(jobType) {
@@ -52,8 +66,10 @@ export function isSpawnJobType(jobType) {
52
66
  /**
53
67
  * Implementation-style spawn jobs (BAPI-551): every spawn job type expected to
54
68
  * end with a pushed origin branch or an opened PR — `implement`, `resume`, and
55
- * the recovery jobs. Excludes `spec_review`, the one spawn type that is a
56
- * verdict job (it produces `.conductor/result.json` instead of a branch/PR).
69
+ * the recovery jobs (`reconcile_overlap` included: when it pushes, finalization
70
+ * verifies the push against origin). Excludes `spec_review`, the one spawn type
71
+ * that is a verdict job (it produces `.conductor/result.json` instead of a
72
+ * branch/PR).
57
73
  */
58
74
  export function isImplementationStyleJobType(jobType) {
59
75
  return isSpawnJobType(jobType) && jobType !== "spec_review";