@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
package/build/executor/env.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* written BEFORE spawn — see `../mcp-provisioning.js` wiring in
|
|
9
9
|
* `job-runner.ts`), never via env vars. This builder still omits
|
|
10
10
|
* `BRIDGE_MCP_PROFILE` (an absent profile selects the intended `core` tool
|
|
11
|
-
* group default), every
|
|
11
|
+
* group default), every Epic Implementer env key under BOTH the supported
|
|
12
|
+
* `BAPI_EPIC_IMPLEMENTER_*` prefix and the legacy Conductor prefix (BAPI-1190),
|
|
13
|
+
* the worker Node-path key under both its supported and legacy spellings (the
|
|
12
14
|
* shim's node executable is resolved into the registration's command/args at
|
|
13
15
|
* provisioning time instead), and all secret-bearing keys. It constructs a
|
|
14
16
|
* FRESH object from a strict allowlist of non-secret operational keys — it
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
import { PR_BASE_BRANCH_ENV_VAR, WORKER_BRANCH_ENV_VAR } from "../pr-base-contract.js";
|
|
56
58
|
import { DECLARED_TOUCHED_FILES_ENV, normalizeDeclaredTouchedFiles, } from "../conductor/file-scope-guard.js";
|
|
57
59
|
import { INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope } from "../index-scope-contract.js";
|
|
60
|
+
import { EPIC_IMPLEMENTER_ENV_PREFIX, EPIC_IMPLEMENTER_NODE_PATH_ENV, LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX, LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV, } from "../epic-implementer-env.js";
|
|
58
61
|
/** Non-secret operational keys forwarded to the worker when present. */
|
|
59
62
|
const ALLOWED_ENV_KEYS = [
|
|
60
63
|
"PATH",
|
|
@@ -116,7 +119,10 @@ const SECRET_NAME_FRAGMENTS = ["TOKEN", "SECRET", "PASSWORD", "API_KEY"];
|
|
|
116
119
|
*/
|
|
117
120
|
export const EXPLICIT_DENY_KEYS = [
|
|
118
121
|
"BRIDGE_MCP_PROFILE",
|
|
119
|
-
|
|
122
|
+
EPIC_IMPLEMENTER_NODE_PATH_ENV,
|
|
123
|
+
// BAPI-1190: the legacy spelling stays denied so a stale ambient value from an
|
|
124
|
+
// older package can never reach a worker during a version-skew window.
|
|
125
|
+
LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,
|
|
120
126
|
"BAPI_API_KEY",
|
|
121
127
|
// ANTHROPIC_API_KEY has NO passthrough exception (BAPI-791): it stays denied
|
|
122
128
|
// absolutely, unlike CLAUDE_CODE_OAUTH_TOKEN below, which is denied here too
|
|
@@ -164,15 +170,78 @@ export const EXPLICIT_DENY_KEYS = [
|
|
|
164
170
|
// the allowlist.
|
|
165
171
|
...WORKER_SENTINEL_DATABASE_ENV_KEYS,
|
|
166
172
|
];
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Executor-PROCESS GitHub credential deny-list (BAPI-1137, R-4)
|
|
175
|
+
//
|
|
176
|
+
// Everything above this line governs what reaches a spawned WORKER. This block
|
|
177
|
+
// governs the executor process itself, and it exists because the worker
|
|
178
|
+
// allowlist cannot reach far enough: a manager-level environment injection — a
|
|
179
|
+
// `launchctl setenv`, a systemd drop-in, a `~/.zprofile` that ran before the
|
|
180
|
+
// operator started the executor by hand — puts a GitHub write token in the
|
|
181
|
+
// executor's own environment, where a subprocess that builds its own environment
|
|
182
|
+
// (or any future code path that reads one) could still find it.
|
|
183
|
+
//
|
|
184
|
+
// The durable-host claim is that the host holds NO STANDING GitHub write
|
|
185
|
+
// credential. A unit that declares none (see `service-unit.ts`) and a process
|
|
186
|
+
// that scrubs any it inherited are the two halves of that claim; without the
|
|
187
|
+
// second, the first only proves the token did not come from the file Bridge
|
|
188
|
+
// generated.
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
/**
|
|
191
|
+
* The GitHub write-credential variable names the executor process refuses to
|
|
192
|
+
* carry. Exactly the two names this repository's environment-safety conventions
|
|
193
|
+
* already recognize (`EXPLICIT_DENY_KEYS` above denies the same pair for a
|
|
194
|
+
* worker), so there is ONE vocabulary for "a GitHub write credential" rather
|
|
195
|
+
* than a second, drifting list.
|
|
196
|
+
*/
|
|
197
|
+
export const EXECUTOR_PROCESS_GITHUB_CREDENTIAL_DENY_KEYS = [
|
|
198
|
+
"GH_TOKEN",
|
|
199
|
+
"GITHUB_TOKEN",
|
|
200
|
+
];
|
|
201
|
+
/**
|
|
202
|
+
* Delete every prohibited GitHub credential variable from the executor's own
|
|
203
|
+
* environment, and report only HOW MANY were removed.
|
|
204
|
+
*
|
|
205
|
+
* Three properties are load-bearing and are what the return type is shaped
|
|
206
|
+
* around:
|
|
207
|
+
*
|
|
208
|
+
* - **Names are compared case-insensitively.** `GH_TOKEN`, `gh_token`, and
|
|
209
|
+
* `Gh_Token` are the same variable to a shell on a case-preserving platform,
|
|
210
|
+
* and a scrubber that matched only the canonical spelling would leave the
|
|
211
|
+
* other two in place.
|
|
212
|
+
* - **Values are never read.** Nothing here dereferences a prohibited key
|
|
213
|
+
* before deleting it, so no credential value can be logged, returned,
|
|
214
|
+
* compared, or classified — not even as "looks like a PAT". A count is the
|
|
215
|
+
* most a caller can learn.
|
|
216
|
+
* - **It mutates in place.** The executor spawns children that inherit
|
|
217
|
+
* `process.env`; returning a filtered COPY would leave the real environment
|
|
218
|
+
* untouched and the guarantee imaginary.
|
|
219
|
+
*/
|
|
220
|
+
export function scrubExecutorProcessGithubCredentials(env = process.env) {
|
|
221
|
+
const prohibited = new Set(EXECUTOR_PROCESS_GITHUB_CREDENTIAL_DENY_KEYS.map((key) => key.toUpperCase()));
|
|
222
|
+
let removedCount = 0;
|
|
223
|
+
// A snapshot of the key names, because deleting while iterating the live
|
|
224
|
+
// object is exactly the kind of thing that silently skips an entry.
|
|
225
|
+
for (const key of Object.keys(env)) {
|
|
226
|
+
if (!prohibited.has(key.toUpperCase()))
|
|
227
|
+
continue;
|
|
228
|
+
delete env[key];
|
|
229
|
+
removedCount++;
|
|
230
|
+
}
|
|
231
|
+
return { removedCount };
|
|
232
|
+
}
|
|
167
233
|
/**
|
|
168
234
|
* True only when `key` is a safe, allowlisted operational key. Exported so the
|
|
169
235
|
* allowlist is locally auditable/testable. A key is allowed iff it is in the
|
|
170
|
-
* allowlist AND is not explicitly denied, does not begin with
|
|
171
|
-
*
|
|
236
|
+
* allowlist AND is not explicitly denied, does not begin with the supported
|
|
237
|
+
* `BAPI_EPIC_IMPLEMENTER_` prefix or the legacy Conductor prefix, and does not
|
|
238
|
+
* contain a secret-name fragment.
|
|
172
239
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
240
|
+
* Both prefix rejections are WHOLE-PREFIX rules, not enumerated lists, which is
|
|
241
|
+
* what makes them hold for keys added later. BAPI-1190 keeps the legacy prefix
|
|
242
|
+
* rejected alongside the renamed one, so a stale name inherited during a
|
|
243
|
+
* version-skew window is filtered too. BAPI-722's
|
|
244
|
+
* `BAPI_EPIC_IMPLEMENTER_DENY_PROBE_TIMEOUT_MS` is rejected twice over — it is absent from
|
|
176
245
|
* `ALLOWED_ENV_KEYS` and it carries the prefix — so it stays executor-process
|
|
177
246
|
* configuration and can never reach a spawned worker. That is deliberate: a worker
|
|
178
247
|
* runs no deny probe, and letting job-side state influence the executor's own
|
|
@@ -183,7 +252,9 @@ export function isExecutorEnvKeyAllowed(key) {
|
|
|
183
252
|
return false;
|
|
184
253
|
if (EXPLICIT_DENY_KEYS.includes(key))
|
|
185
254
|
return false;
|
|
186
|
-
if (key.startsWith(
|
|
255
|
+
if (key.startsWith(EPIC_IMPLEMENTER_ENV_PREFIX))
|
|
256
|
+
return false;
|
|
257
|
+
if (key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX))
|
|
187
258
|
return false;
|
|
188
259
|
const upper = key.toUpperCase();
|
|
189
260
|
if (SECRET_NAME_FRAGMENTS.some((fragment) => upper.includes(fragment)))
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* interval. This is TELEMETRY ONLY (R14): the marker is never persisted, never
|
|
23
23
|
* reported as `last_commit_sha`, and never drives an executor decision — the
|
|
24
24
|
* reconciler remains the sole consumer and the sole transition authority
|
|
25
|
-
* (`docs/claude/
|
|
25
|
+
* (`docs/claude/epic-implementer-standing-rules.md`).
|
|
26
26
|
*
|
|
27
27
|
* - SERVER STOP (WS-B executor half). A heartbeat response carrying the
|
|
28
28
|
* server's stop request kills the owned worker like `stale_claim` does, but
|
package/build/executor/index.js
CHANGED
|
@@ -7,16 +7,33 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { runExecutorCli, getExecutorUsage, parseExecutorArgs } from "./cli.js";
|
|
9
9
|
export { runExecutor } from "./runner.js";
|
|
10
|
-
// Persistent-service packaging (BAPI-688)
|
|
11
|
-
//
|
|
10
|
+
// Persistent-service packaging (BAPI-688), the file-generation leg of
|
|
11
|
+
// durable-host mode (BAPI-1137). The CLI entry plus the PURE helpers doctor
|
|
12
|
+
// needs to enumerate and inspect generated units — doctor must share this
|
|
12
13
|
// module's naming/format contract instead of maintaining a second parser.
|
|
14
|
+
//
|
|
15
|
+
// This group grants GENERATION only. It confers no lifecycle capability, which
|
|
16
|
+
// is why the lifecycle exports stay in their own group below: a caller that
|
|
17
|
+
// imports the generator still cannot start a persistent daemon.
|
|
13
18
|
export { runExecutorInstallServiceCli, getExecutorInstallServiceUsage, buildExecutorServiceArtifact, writeExecutorServiceArtifact, inspectExecutorServiceArtifact, executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename,
|
|
14
19
|
// Programmatic, lifecycle-free plan/write seam (BAPI-779) — how
|
|
15
20
|
// `install conductor` reaches the generator without shelling out to the CLI.
|
|
16
|
-
planExecutorServiceInstall, writeExecutorServicePlan, resolvePackagedExecutorInvocation,
|
|
21
|
+
planExecutorServiceInstall, writeExecutorServicePlan, resolvePackagedExecutorInvocation,
|
|
22
|
+
// Durable-host credential declaration contract (BAPI-1137, R-4). doctor needs
|
|
23
|
+
// the shared inspector and the postcondition helper so its diagnostic and the
|
|
24
|
+
// generator's write check are literally the same rule; exporting the allowlist
|
|
25
|
+
// and the argument-contract predicate keeps the contract locally auditable
|
|
26
|
+
// rather than discoverable only by reading the renderer.
|
|
27
|
+
inspectExecutorServiceCredentialDeclaration, assertExecutorServiceCredentialPostcondition, checkExecutorServiceArgumentContract, isAllowedExecutorServiceUnitEnvKey, executorLaunchdLogPathsForId, EXECUTOR_SERVICE_BASE_URL_ENV, EXECUTOR_SERVICE_PATH_ENV, EXECUTOR_SERVICE_UNIT_ENV_ALLOWLIST, EXECUTOR_SERVICE_UNIT_FILE_MODE, } from "./service-unit.js";
|
|
17
28
|
// Consented lifecycle (BAPI-779, R-1). Deliberately a SEPARATE module from the
|
|
18
29
|
// generator above: importing the writer never grants the ability to start a
|
|
19
30
|
// persistent daemon.
|
|
20
|
-
export { buildExecutorServiceStartPlan, buildExecutorServiceStateProbe, formatExecutorServiceStartFailure, inspectExecutorServiceState, startExecutorService,
|
|
31
|
+
export { buildExecutorServiceStartPlan, buildExecutorServiceStateProbe, formatExecutorServiceStartFailure, inspectExecutorServiceState, startExecutorService,
|
|
32
|
+
// Durability prerequisites (BAPI-1137). Exported SEPARATELY from the
|
|
33
|
+
// generation group above, and alongside — not inside — the start group: these
|
|
34
|
+
// are strictly read-only probes, so importing them grants the ability to ASK
|
|
35
|
+
// whether this host survives logout, never the ability to change it. Nothing
|
|
36
|
+
// here runs `loginctl enable-linger`.
|
|
37
|
+
buildExecutorServiceDurabilityProbe, inspectExecutorServiceDurability, EXECUTOR_LINGER_REMEDIATION, EXECUTOR_MACOS_SESSION_REMEDIATION, } from "./service-lifecycle.js";
|
|
21
38
|
export { collectExecutorInstallPreflight, formatExecutorInstallPreflight, EXECUTOR_INSTALL_REQUIRED_TOOLS, EXECUTOR_INSTALL_CLAUDE_LOGIN_REMEDIATION, } from "./install-preflight.js";
|
|
22
39
|
export { runExecutorWatchCli, getExecutorWatchUsage, parseExecutorWatchArgs, } from "./watch-cli.js";
|
|
@@ -143,12 +143,12 @@ export const WorkerEnvFilePresent = "ContractError.WorkerEnvFilePresent";
|
|
|
143
143
|
* BAPI-1152 — `reconcile_overlap` failure kinds. Each names ONE way the
|
|
144
144
|
* wrapper's merge-into-child could not reach a verified push, so an operator
|
|
145
145
|
* reading the row knows which fact to go look at. None of them is a verdict: a
|
|
146
|
-
*
|
|
147
|
-
*
|
|
146
|
+
* moved head is NOT a failure — it completes with a fact envelope — so it has
|
|
147
|
+
* no kind here.
|
|
148
148
|
*
|
|
149
149
|
* - `ContractError.ReconcileOverlapPayload` — the payload failed validation
|
|
150
150
|
* before any subprocess ran (missing/blank input SHA, unknown schema version,
|
|
151
|
-
* malformed command
|
|
151
|
+
* malformed command record).
|
|
152
152
|
* - `ContractError.ReconcileOverlapBranchMismatch` — the prepared worktree's
|
|
153
153
|
* branch is not the job's top-level `expected_branch`, so the push destination
|
|
154
154
|
* cannot be trusted.
|
|
@@ -19,6 +19,7 @@ import os from "node:os";
|
|
|
19
19
|
import { runHeartbeatLoop } from "./heartbeat.js";
|
|
20
20
|
import { isExecutorNamedError, toExecutorFailure, secretFreeErrorMessage, MissingVerdictArtifact, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
|
|
21
21
|
import { buildConductorMergeAccessForExecutorJob, buildDefaultMergeLocalDeps, runExecutorMergeJob, MERGE_RETRYABLE, MERGE_FAILED, } from "./merge-job.js";
|
|
22
|
+
import { createProductionCiRerunDeps, runCiRerunJob, } from "./ci-rerun-job.js";
|
|
22
23
|
import { createObservationState } from "./observation.js";
|
|
23
24
|
import { getWorktreeMcpRegistrationTargets, normalizeWorktreePathForRegistration, provisionMcpRegistrationForWorktree, } from "../mcp-provisioning.js";
|
|
24
25
|
import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
|
|
@@ -615,6 +616,16 @@ control) {
|
|
|
615
616
|
if (job.job_type === "smoke") {
|
|
616
617
|
return runSmokeJob(job, httpClient, options, deps, ownership, observation, control);
|
|
617
618
|
}
|
|
619
|
+
// BAPI-1160: `ci_rerun` is a deterministic executor action, dispatched here
|
|
620
|
+
// beside `merge`/`smoke` and BEFORE the generic spawn path. It ensures no
|
|
621
|
+
// worktree and spawns no worker, and it must NEVER fall back to one: spawning
|
|
622
|
+
// an LLM worker to "fix" a failure the server already proved to be
|
|
623
|
+
// infrastructure is exactly the behavior this job type replaces. `job-types.ts`
|
|
624
|
+
// keeps it out of both `SPAWN_JOB_TYPES` and `RECOVERY_JOB_TYPES`, so reaching
|
|
625
|
+
// the spawn path below is structurally impossible rather than merely unlikely.
|
|
626
|
+
if (job.job_type === "ci_rerun") {
|
|
627
|
+
return runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control);
|
|
628
|
+
}
|
|
618
629
|
// BAPI-1152: `reconcile_overlap` takes its OWN branch, after `merge`/`smoke`
|
|
619
630
|
// and before the generic spawn path. It needs a prepared, locked worktree like
|
|
620
631
|
// a spawn job, but its body is the reconcile module's state machine: the
|
|
@@ -652,6 +663,121 @@ control) {
|
|
|
652
663
|
});
|
|
653
664
|
return { status: "failed", reason: "unsupported_job_type" };
|
|
654
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Run a deterministic `ci_rerun` job (BAPI-1160).
|
|
668
|
+
*
|
|
669
|
+
* Supervised through the SAME ownership, heartbeat, dead-man, and timeout
|
|
670
|
+
* protocol as {@link runMergeJob} — via {@link createFlowProcess} — so a
|
|
671
|
+
* deterministic job is not a second, weaker lifecycle. The body itself spawns no
|
|
672
|
+
* subprocess of its own beyond the bounded `gh` invocations inside
|
|
673
|
+
* `ci-rerun-job.ts`, and there is NO fallback to a worker on failure.
|
|
674
|
+
*
|
|
675
|
+
* The completion carries only the job's fact envelope. It names no gate,
|
|
676
|
+
* requests no transition, and asserts no verdict: the reconciler re-observes the
|
|
677
|
+
* head independently and remains the sole transition authority (R14 rule 3).
|
|
678
|
+
*/
|
|
679
|
+
async function runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control) {
|
|
680
|
+
// Resolve + enforce payload.timeout_seconds BEFORE any provider work, exactly
|
|
681
|
+
// as the merge branch does.
|
|
682
|
+
const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
|
|
683
|
+
if (!timeout.ok) {
|
|
684
|
+
await httpClient.fail(job, {
|
|
685
|
+
error_kind: "ContractError.Timeout",
|
|
686
|
+
error_message: timeout.error,
|
|
687
|
+
classification: "crashed",
|
|
688
|
+
});
|
|
689
|
+
return { status: "failed", reason: "timeout_contract" };
|
|
690
|
+
}
|
|
691
|
+
const runCiRerun = seams.runCiRerun ??
|
|
692
|
+
((payload) => runCiRerunJob(payload, createProductionCiRerunDeps()));
|
|
693
|
+
const flow = createFlowProcess(() => runCiRerun(job.payload), (result) => result.ok);
|
|
694
|
+
const procResult = await superviseProcess({
|
|
695
|
+
job,
|
|
696
|
+
httpClient,
|
|
697
|
+
options,
|
|
698
|
+
deps,
|
|
699
|
+
ownership,
|
|
700
|
+
observation,
|
|
701
|
+
proc: flow.proc,
|
|
702
|
+
timeoutSeconds: timeout.timeoutSeconds,
|
|
703
|
+
collectTelemetry: async () => ({}),
|
|
704
|
+
control,
|
|
705
|
+
});
|
|
706
|
+
if (ownership.abandoned) {
|
|
707
|
+
return { status: "abandoned", reason: ownership.abandonReason };
|
|
708
|
+
}
|
|
709
|
+
if (procResult.classification === "timeout") {
|
|
710
|
+
const terminal = await sendTerminalMutationWithAuthReport({
|
|
711
|
+
kind: "fail",
|
|
712
|
+
send: () => httpClient.fail(job, {
|
|
713
|
+
error_kind: "CiRerunFailed",
|
|
714
|
+
error_message: "ci_rerun exceeded its job timeout",
|
|
715
|
+
classification: "timeout",
|
|
716
|
+
telemetry: observation.snapshot(),
|
|
717
|
+
}),
|
|
718
|
+
deps,
|
|
719
|
+
options,
|
|
720
|
+
ownership,
|
|
721
|
+
log: deps.log,
|
|
722
|
+
}, job, control);
|
|
723
|
+
return terminalToRunResult(terminal, "failed");
|
|
724
|
+
}
|
|
725
|
+
const outcome = flow.getOutcome();
|
|
726
|
+
if (outcome === undefined) {
|
|
727
|
+
const terminal = await sendTerminalMutationWithAuthReport({
|
|
728
|
+
kind: "fail",
|
|
729
|
+
send: () => httpClient.fail(job, {
|
|
730
|
+
error_kind: "CiRerunFailed",
|
|
731
|
+
error_message: "ci_rerun completed without an outcome",
|
|
732
|
+
classification: "crashed",
|
|
733
|
+
telemetry: observation.snapshot(),
|
|
734
|
+
}),
|
|
735
|
+
deps,
|
|
736
|
+
options,
|
|
737
|
+
ownership,
|
|
738
|
+
log: deps.log,
|
|
739
|
+
}, job, control);
|
|
740
|
+
return terminalToRunResult(terminal, "failed");
|
|
741
|
+
}
|
|
742
|
+
if (outcome.ok) {
|
|
743
|
+
const completion = {
|
|
744
|
+
job_type: "ci_rerun",
|
|
745
|
+
exit_code: 0,
|
|
746
|
+
classification: "clean_exit",
|
|
747
|
+
// The FACT ENVELOPE, verbatim. No gate, no transition, no verdict — the
|
|
748
|
+
// backend model is `extra="forbid"`, so anything else fails validation
|
|
749
|
+
// rather than being silently ignored.
|
|
750
|
+
result: outcome.result,
|
|
751
|
+
telemetry: observation.snapshot(),
|
|
752
|
+
};
|
|
753
|
+
const terminal = await sendTerminalMutationWithAuthReport({
|
|
754
|
+
kind: "complete",
|
|
755
|
+
send: () => httpClient.complete(job, completion),
|
|
756
|
+
deps,
|
|
757
|
+
options,
|
|
758
|
+
ownership,
|
|
759
|
+
log: deps.log,
|
|
760
|
+
}, job, control);
|
|
761
|
+
return terminalToRunResult(terminal, "completed");
|
|
762
|
+
}
|
|
763
|
+
// A refused or failed re-run. The reserved infrastructure attempt stays
|
|
764
|
+
// CONSUMED — see `ci-rerun-job.ts` — and the reason is a closed token, never
|
|
765
|
+
// provider text.
|
|
766
|
+
const terminal = await sendTerminalMutationWithAuthReport({
|
|
767
|
+
kind: "fail",
|
|
768
|
+
send: () => httpClient.fail(job, {
|
|
769
|
+
error_kind: outcome.failure.errorKind,
|
|
770
|
+
error_message: outcome.failure.reason,
|
|
771
|
+
classification: "crashed",
|
|
772
|
+
telemetry: observation.snapshot(),
|
|
773
|
+
}),
|
|
774
|
+
deps,
|
|
775
|
+
options,
|
|
776
|
+
ownership,
|
|
777
|
+
log: deps.log,
|
|
778
|
+
}, job, control);
|
|
779
|
+
return terminalToRunResult(terminal, "failed");
|
|
780
|
+
}
|
|
655
781
|
/**
|
|
656
782
|
* Adapt a `runMerge(...)` promise into an {@link OwnedChildProcess} so the merge
|
|
657
783
|
* flow is supervised by the SAME heartbeat/deadman/timeout path as spawn/smoke
|
|
@@ -2114,7 +2240,7 @@ async function runPreparedSpawn(params) {
|
|
|
2114
2240
|
}
|
|
2115
2241
|
const git = observation.git();
|
|
2116
2242
|
// BAPI-1152: a driver owns everything after a clean worker exit — staging, the
|
|
2117
|
-
//
|
|
2243
|
+
// commits, the guarded push, and finalization.
|
|
2118
2244
|
if (driver && procResult.classification === "clean_exit") {
|
|
2119
2245
|
await finalizeRegistry();
|
|
2120
2246
|
const phase = await superviseDriverPhase({
|
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
* resolution, but it does NOT re-enter `/implement-ticket`: it always carries an
|
|
27
27
|
* explicit server-rendered `payload.prompt`, which wins over the synthesis in
|
|
28
28
|
* `worker-command.ts`, and `job-runner.ts` dispatches it to its own body.
|
|
29
|
+
*
|
|
30
|
+
* BAPI-1160: `ci_rerun` is DELIBERATELY ABSENT from this set and from
|
|
31
|
+
* `SPAWN_JOB_TYPES` below. It is a deterministic actuator — one bounded
|
|
32
|
+
* `gh run rerun --failed` and a bounded poll — with no worktree, no branch to
|
|
33
|
+
* reuse, no prompt, and no worker. It has a dedicated dispatch branch in
|
|
34
|
+
* `job-runner.ts` beside `merge` and `smoke`, ahead of the generic spawn path,
|
|
35
|
+
* and it must NEVER fall back to a worker subprocess: spawning an LLM worker to
|
|
36
|
+
* "fix" a failure the server already PROVED to be infrastructure is the exact
|
|
37
|
+
* behavior this job type exists to replace.
|
|
29
38
|
*/
|
|
30
39
|
export const RECOVERY_JOB_TYPES = new Set([
|
|
31
40
|
"remediate",
|
|
@@ -37,8 +46,9 @@ export const RECOVERY_JOB_TYPES = new Set([
|
|
|
37
46
|
* Real headless-spawn job types (worktree + worker LLM): the first-pass
|
|
38
47
|
* `implement`, the `resume` continuation (own pre-spawn protocol), the
|
|
39
48
|
* `spec_review` verdict job, and the recovery jobs. `merge` (deterministic,
|
|
40
|
-
* no worktree/worker)
|
|
41
|
-
*
|
|
49
|
+
* no worktree/worker), `smoke` (no-op acceptance), and BAPI-1160's `ci_rerun`
|
|
50
|
+
* (deterministic, no worktree/worker) are intentionally EXCLUDED — they have
|
|
51
|
+
* dedicated dispatch branches BEFORE the generic spawn path.
|
|
42
52
|
*
|
|
43
53
|
* BAPI-1152: `reconcile_overlap` is here because it needs a prepared, locked
|
|
44
54
|
* worktree and MAY spawn a worker — but only CONDITIONALLY. A clean merge whose
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* {@link MERGE_FAILED} `error_kind` (via {@link classifyMergeFailureErrorKind})
|
|
22
22
|
* so the companion re-fire lane can route retry-vs-rebase without string parsing.
|
|
23
23
|
*/
|
|
24
|
-
import { makeLocalMergeExecutor, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
|
|
24
|
+
import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
|
|
25
25
|
import { resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
|
|
26
26
|
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, EPIC_BRANCH_PREFIX, } from "../epic-integration-pr.js";
|
|
27
27
|
import { secretFreeErrorMessage } from "./job-errors.js";
|
|
@@ -166,6 +166,30 @@ export const MERGE_RETRYABLE = "MergeRetryable";
|
|
|
166
166
|
export const MERGE_CONFLICT = "MergeConflict";
|
|
167
167
|
export const MERGE_FAILED = "MergeFailed";
|
|
168
168
|
export const MERGE_REF_MISMATCH = "MergeRefMismatch";
|
|
169
|
+
/**
|
|
170
|
+
* BAPI-1234 — the server's migration-graph merge guard refused the merge. Two
|
|
171
|
+
* kinds, kept distinct because they call for different human responses:
|
|
172
|
+
* - {@link MERGE_MIGRATION_GRAPH_INVALID}: merging this head onto the current
|
|
173
|
+
* base produces a broken Alembic graph. A human renumbers the PR's own
|
|
174
|
+
* never-applied migration.
|
|
175
|
+
* - {@link MERGE_MIGRATION_GRAPH_UNREADABLE}: the merged graph could not be
|
|
176
|
+
* proven either way. An operator checks the provider, then unparks.
|
|
177
|
+
* Like {@link MERGE_REF_MISMATCH}, both are deliberate terminal refusals: never
|
|
178
|
+
* retryable, never a rebase target, and reported as `clean_exit`.
|
|
179
|
+
*/
|
|
180
|
+
export const MERGE_MIGRATION_GRAPH_INVALID = "MergeMigrationGraphInvalid";
|
|
181
|
+
export const MERGE_MIGRATION_GRAPH_UNREADABLE = "MergeMigrationGraphUnreadable";
|
|
182
|
+
/** Server refusal reasons mapped to their dedicated terminal error kinds. */
|
|
183
|
+
const MIGRATION_GRAPH_ERROR_KINDS = new Map([
|
|
184
|
+
["migration_graph_invalid", MERGE_MIGRATION_GRAPH_INVALID],
|
|
185
|
+
["migration_graph_unreadable", MERGE_MIGRATION_GRAPH_UNREADABLE],
|
|
186
|
+
]);
|
|
187
|
+
/** Error kinds that are deliberate refusals rather than crashes. */
|
|
188
|
+
const DELIBERATE_REFUSAL_ERROR_KINDS = new Set([
|
|
189
|
+
MERGE_REF_MISMATCH,
|
|
190
|
+
MERGE_MIGRATION_GRAPH_INVALID,
|
|
191
|
+
MERGE_MIGRATION_GRAPH_UNREADABLE,
|
|
192
|
+
]);
|
|
169
193
|
/** Reasons whose failure is transient/infrastructure and safe to retry. */
|
|
170
194
|
const RETRYABLE_MERGE_REASONS = new Set([
|
|
171
195
|
"gh_pr_view_timeout",
|
|
@@ -190,6 +214,9 @@ const RETRYABLE_MERGE_REASONS = new Set([
|
|
|
190
214
|
* variants covered so a distinct reason never accidentally downgrades to
|
|
191
215
|
* {@link MERGE_FAILED}. Conflict classification is checked first so it is never masked.
|
|
192
216
|
*
|
|
217
|
+
* BAPI-1234: `migration_graph_invalid` / `migration_graph_unreadable` map to
|
|
218
|
+
* their own terminal kinds, in the same early position and for the same reason.
|
|
219
|
+
*
|
|
193
220
|
* BAPI-1021: `ref_mismatch` is classified BEFORE conflict, retryable-prefix, and
|
|
194
221
|
* generic-failure handling, and is deliberately NEVER added to
|
|
195
222
|
* {@link RETRYABLE_MERGE_REASONS} — it is a terminal safety refusal, not a
|
|
@@ -201,6 +228,9 @@ export function classifyMergeFailureErrorKind(response) {
|
|
|
201
228
|
const reason = asString(response.reason ?? undefined);
|
|
202
229
|
if (reason === "ref_mismatch")
|
|
203
230
|
return MERGE_REF_MISMATCH;
|
|
231
|
+
const migrationGraphKind = reason ? MIGRATION_GRAPH_ERROR_KINDS.get(reason) : undefined;
|
|
232
|
+
if (migrationGraphKind)
|
|
233
|
+
return migrationGraphKind;
|
|
204
234
|
if (hasConflictEvent || reason === "gh_merge_conflict")
|
|
205
235
|
return MERGE_CONFLICT;
|
|
206
236
|
if (reason && (reason.startsWith("ci_poll_") || RETRYABLE_MERGE_REASONS.has(reason))) {
|
|
@@ -217,8 +247,10 @@ export function classifyMergeFailureErrorKind(response) {
|
|
|
217
247
|
*
|
|
218
248
|
* BAPI-1021: a {@link MERGE_REF_MISMATCH} classifies as `classification:
|
|
219
249
|
* "clean_exit"` rather than `"crashed"` — it is a deliberate refusal the
|
|
220
|
-
* executor made correctly, not a crash.
|
|
221
|
-
*
|
|
250
|
+
* executor made correctly, not a crash. BAPI-1234 adds the two migration-graph
|
|
251
|
+
* refusal kinds to that set: a `crashed` classification would make the
|
|
252
|
+
* backend's merge re-fire lane retry a merge the server will refuse identically.
|
|
253
|
+
* Every other merge outcome keeps its existing `"crashed"` classification.
|
|
222
254
|
*/
|
|
223
255
|
export function buildMergeJobFailure(response) {
|
|
224
256
|
const reason = asString(response.reason ?? undefined) ?? response.status;
|
|
@@ -226,7 +258,7 @@ export function buildMergeJobFailure(response) {
|
|
|
226
258
|
return {
|
|
227
259
|
error_kind: errorKind,
|
|
228
260
|
error_message: secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),
|
|
229
|
-
classification: errorKind
|
|
261
|
+
classification: DELIBERATE_REFUSAL_ERROR_KINDS.has(errorKind) ? "clean_exit" : "crashed",
|
|
230
262
|
};
|
|
231
263
|
}
|
|
232
264
|
/**
|
|
@@ -321,5 +353,14 @@ export async function runExecutorMergeJob(job, seams) {
|
|
|
321
353
|
* unchanged (no App token is ever placed here).
|
|
322
354
|
*/
|
|
323
355
|
export function buildDefaultMergeLocalDeps(deps, signal) {
|
|
324
|
-
|
|
356
|
+
// BAPI-1136: the executor's merge job runs on the operator's own machine, which
|
|
357
|
+
// is typed `local`, so it reads the local configuration key and no other. The
|
|
358
|
+
// environment is still passed for the allowlisted operational base (PATH, HOME,
|
|
359
|
+
// certificates); it no longer supplies the identity, and an ambient GH_TOKEN in
|
|
360
|
+
// it is dropped rather than inherited.
|
|
361
|
+
return {
|
|
362
|
+
env: deps.env,
|
|
363
|
+
credential: resolveLocalMergeCredential(deps.env, "local"),
|
|
364
|
+
signal,
|
|
365
|
+
};
|
|
325
366
|
}
|
|
@@ -291,7 +291,7 @@ function parseRateLimitEvent(obj) {
|
|
|
291
291
|
const RATE_LIMIT_CONSEQUENCE_TEXT = "The executor keeps claiming: a worker that hits the ceiling fails fast and its " +
|
|
292
292
|
"job classifies as a worker failure under the normal retry budget.";
|
|
293
293
|
/** Fixed runbook reference appended to every rate-limit diagnostic (BAPI-898). */
|
|
294
|
-
const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/claude/epic-
|
|
294
|
+
const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/claude/epic-implementer-v2-operator-runbook.md §8.";
|
|
295
295
|
/**
|
|
296
296
|
* Render the operator-facing rate-limit line (BAPI-828, BAPI-898).
|
|
297
297
|
*
|
|
@@ -11,7 +11,7 @@ import { resolveAllExecutorApiAccess, resolveBaseUrl, EXECUTOR_BASE_URL_REQUIRED
|
|
|
11
11
|
* time out, and letting a job influence the executor's own safety-probe budget
|
|
12
12
|
* would be a privilege inversion.
|
|
13
13
|
*/
|
|
14
|
-
export const DENY_PROBE_TIMEOUT_ENV_KEY = "
|
|
14
|
+
export const DENY_PROBE_TIMEOUT_ENV_KEY = "BAPI_EPIC_IMPLEMENTER_DENY_PROBE_TIMEOUT_MS";
|
|
15
15
|
/**
|
|
16
16
|
* Accepted bounds for the resolved probe timeout. The lower bound keeps a
|
|
17
17
|
* fat-fingered `1` from turning every probe into an instant HANG (which is FATAL —
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Executor-side structured prompt-spec renderer (BAPI-550, TDD §6/§7).
|
|
3
3
|
*
|
|
4
4
|
* The reconciler ships a server-authored prompt template in a `spec_review` job's
|
|
5
|
-
* `payload.prompt_spec` (from `src/python/llms/agents/
|
|
5
|
+
* `payload.prompt_spec` (from `src/python/llms/agents/epic_implementer_jobs/prompt_specs.py`).
|
|
6
6
|
* This module validates that spec and renders it into the single `claude -p`
|
|
7
7
|
* prompt string, substituting each declared `{{PLACEHOLDER}}` marker from real
|
|
8
8
|
* runtime state (ticket spec, base/work branch, git log, prior revise reasons).
|