@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.
- package/README.md +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- 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
|
|
641
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
|
21
|
-
* EXISTING branch/PR, so the
|
|
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
|
|
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
|
|
56
|
-
*
|
|
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";
|