@bridge_gpt/mcp-server 0.2.62 → 0.2.66
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 +66 -15
- package/build/agent-launchers/claude-executor-adapter.js +145 -20
- package/build/agent-launchers/executor-adapter.js +81 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +565 -32
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +351 -0
- package/build/conductor/epic-implementer-cli.js +154 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +45 -12
- package/build/conductor/recovery-cli.js +859 -27
- package/build/conductor/recovery-operations.js +574 -5
- package/build/conductor/repair-contract.js +69 -3
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +52 -13
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +3 -2
- package/build/doctor.js +147 -21
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1537 -125
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +41 -33
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +58 -19
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +291 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +12 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +91 -4
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +61 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +1550 -55
- package/build/executor/job-types.js +9 -6
- package/build/executor/preflight.js +189 -1
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +366 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +168 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +13 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +70 -34
- package/build/executor/worker-finalization.js +168 -26
- package/build/executor/worker-guard-hook.js +75 -14
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +594 -1152
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +231 -41
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/merge-pull-request.js +1 -1
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +146 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +208 -125
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +5 -4
- package/build/worktree-core.js +6 -5
- package/docs/CONDUCTOR.md +116 -2
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +2 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -132,3 +132,145 @@ export function commitResidue(telemetry) {
|
|
|
132
132
|
}
|
|
133
133
|
return out;
|
|
134
134
|
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// BAPI-1296 — the CI-only fixed-path result reader.
|
|
137
|
+
//
|
|
138
|
+
// A `ci_fix` worker writes `<worktree>/.conductor/result.json` with its per-check
|
|
139
|
+
// mapping (and, optionally, an external-attribution diagnosis). Before this
|
|
140
|
+
// ticket the executor never read that file for `ci_fix`, so the mapping the
|
|
141
|
+
// server's exit contract compares against never arrived and every clean exit
|
|
142
|
+
// looked incomplete. This reader transports it — bounded, allowlisted, and
|
|
143
|
+
// read only after THIS attempt's worker exited (the locked pre-spawn cleanup in
|
|
144
|
+
// `job-runner.ts` removed any earlier attempt's file) — and nothing else.
|
|
145
|
+
//
|
|
146
|
+
// It is TELEMETRY transport, not certification: the executor never decides
|
|
147
|
+
// that coverage is complete because the process exited cleanly, never lets the
|
|
148
|
+
// file supply branch/head/PR identifiers, and never lets it name a transition.
|
|
149
|
+
// The server re-derives completeness from the job row's dispatched set.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
/** Largest `ci_fix` result artifact read, in bytes. */
|
|
152
|
+
export const CI_FIX_RESULT_MAX_BYTES = 16_384;
|
|
153
|
+
/** Most entries in `check_fix_claims` (mirrors the dispatch cap). */
|
|
154
|
+
export const CI_FIX_CLAIMS_MAX_ENTRIES = 32;
|
|
155
|
+
/** Longest single claim value, in UTF-8 bytes. */
|
|
156
|
+
export const CI_FIX_CLAIM_MAX_BYTES = 512;
|
|
157
|
+
/** Longest attribution reason, in UTF-8 bytes. */
|
|
158
|
+
export const CI_FIX_ATTRIBUTION_REASON_MAX_BYTES = 300;
|
|
159
|
+
/** The per-check mapping key (Python `CI_FIX_CHECK_CLAIMS_KEY`). */
|
|
160
|
+
export const CI_FIX_CHECK_CLAIMS_KEY = "check_fix_claims";
|
|
161
|
+
/** The attribution keys and sentinel (Python `CI_FIX_ATTRIBUTION_*`). */
|
|
162
|
+
export const CI_FIX_ATTRIBUTION_KEY = "ci_failure_attribution";
|
|
163
|
+
export const CI_FIX_ATTRIBUTION_REASON_KEY = "ci_failure_attribution_reason";
|
|
164
|
+
export const CI_FIX_ATTRIBUTION_EXTERNAL = "external";
|
|
165
|
+
/**
|
|
166
|
+
* Executor-owned status of the artifact, carried at `result.check_fix_claims_status`
|
|
167
|
+
* so an operator can tell "the worker wrote nothing" from "the worker wrote
|
|
168
|
+
* something unusable" from "the worker covered only part of the set". It is the
|
|
169
|
+
* executor's own observation, never a worker-supplied value.
|
|
170
|
+
*/
|
|
171
|
+
export const CI_FIX_CLAIMS_STATUS_KEY = "check_fix_claims_status";
|
|
172
|
+
const CI_FIX_RESULT_ALLOWED_KEYS = new Set([
|
|
173
|
+
CI_FIX_CHECK_CLAIMS_KEY,
|
|
174
|
+
CI_FIX_ATTRIBUTION_KEY,
|
|
175
|
+
CI_FIX_ATTRIBUTION_REASON_KEY,
|
|
176
|
+
]);
|
|
177
|
+
function isPlainRecord(value) {
|
|
178
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Read and validate `<worktree>/.conductor/result.json` for a `ci_fix` job.
|
|
182
|
+
*
|
|
183
|
+
* `requiredChecks` is the executor-validated dispatched union. A claim for any
|
|
184
|
+
* name outside it, an extra top-level key, an oversized/blank value, an unpaired
|
|
185
|
+
* attribution, a non-regular file, a file over {@link CI_FIX_RESULT_MAX_BYTES},
|
|
186
|
+
* or invalid JSON makes the whole artifact `invalid` — nothing from it is
|
|
187
|
+
* forwarded. A valid artifact that covers only part of the set is `partial` and
|
|
188
|
+
* IS forwarded, so the server's contract check sees exactly what was claimed.
|
|
189
|
+
* Never throws; `code` is a fixed, secret-free token.
|
|
190
|
+
*/
|
|
191
|
+
export async function readCiFixResultArtifact(worktreePath, requiredChecks, deps) {
|
|
192
|
+
const artifactPath = path.join(worktreePath, ".conductor", "result.json");
|
|
193
|
+
if (deps.lstatPath) {
|
|
194
|
+
let meta;
|
|
195
|
+
try {
|
|
196
|
+
meta = await deps.lstatPath(artifactPath);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return { status: "invalid", code: "artifact_unreadable" };
|
|
200
|
+
}
|
|
201
|
+
if (meta === null)
|
|
202
|
+
return { status: "missing", code: "artifact_absent" };
|
|
203
|
+
if (meta.isDirectory || meta.isSymbolicLink) {
|
|
204
|
+
return { status: "invalid", code: "artifact_not_regular_file" };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = await deps.readFile(artifactPath);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
const code = err && typeof err === "object" ? err.code : undefined;
|
|
213
|
+
return code === "ENOENT"
|
|
214
|
+
? { status: "missing", code: "artifact_absent" }
|
|
215
|
+
: { status: "invalid", code: "artifact_unreadable" };
|
|
216
|
+
}
|
|
217
|
+
if (typeof raw !== "string")
|
|
218
|
+
return { status: "invalid", code: "artifact_unreadable" };
|
|
219
|
+
if (byteLength(raw) > CI_FIX_RESULT_MAX_BYTES) {
|
|
220
|
+
return { status: "invalid", code: "artifact_oversized" };
|
|
221
|
+
}
|
|
222
|
+
let parsed;
|
|
223
|
+
try {
|
|
224
|
+
parsed = JSON.parse(raw);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return { status: "invalid", code: "artifact_not_json" };
|
|
228
|
+
}
|
|
229
|
+
if (!isPlainRecord(parsed))
|
|
230
|
+
return { status: "invalid", code: "artifact_not_object" };
|
|
231
|
+
for (const key of Object.keys(parsed)) {
|
|
232
|
+
if (!CI_FIX_RESULT_ALLOWED_KEYS.has(key)) {
|
|
233
|
+
return { status: "invalid", code: "artifact_unexpected_key" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const claims = parsed[CI_FIX_CHECK_CLAIMS_KEY];
|
|
237
|
+
if (!isPlainRecord(claims))
|
|
238
|
+
return { status: "invalid", code: "claims_not_object" };
|
|
239
|
+
const claimNames = Object.keys(claims);
|
|
240
|
+
if (claimNames.length > CI_FIX_CLAIMS_MAX_ENTRIES) {
|
|
241
|
+
return { status: "invalid", code: "claims_too_many" };
|
|
242
|
+
}
|
|
243
|
+
const required = new Set(requiredChecks);
|
|
244
|
+
const validatedClaims = {};
|
|
245
|
+
for (const name of claimNames) {
|
|
246
|
+
const value = claims[name];
|
|
247
|
+
if (!required.has(name))
|
|
248
|
+
return { status: "invalid", code: "claims_undispatched_check" };
|
|
249
|
+
if (typeof value !== "string" ||
|
|
250
|
+
value.trim().length === 0 ||
|
|
251
|
+
byteLength(value) > CI_FIX_CLAIM_MAX_BYTES) {
|
|
252
|
+
return { status: "invalid", code: "claims_value_invalid" };
|
|
253
|
+
}
|
|
254
|
+
validatedClaims[name] = value;
|
|
255
|
+
}
|
|
256
|
+
const fields = { [CI_FIX_CHECK_CLAIMS_KEY]: validatedClaims };
|
|
257
|
+
const hasAttribution = Object.prototype.hasOwnProperty.call(parsed, CI_FIX_ATTRIBUTION_KEY);
|
|
258
|
+
const hasReason = Object.prototype.hasOwnProperty.call(parsed, CI_FIX_ATTRIBUTION_REASON_KEY);
|
|
259
|
+
if (hasAttribution || hasReason) {
|
|
260
|
+
const attribution = parsed[CI_FIX_ATTRIBUTION_KEY];
|
|
261
|
+
const reason = parsed[CI_FIX_ATTRIBUTION_REASON_KEY];
|
|
262
|
+
if (!hasAttribution ||
|
|
263
|
+
!hasReason ||
|
|
264
|
+
typeof attribution !== "string" ||
|
|
265
|
+
attribution.trim().toLowerCase() !== CI_FIX_ATTRIBUTION_EXTERNAL ||
|
|
266
|
+
typeof reason !== "string" ||
|
|
267
|
+
reason.trim().length === 0 ||
|
|
268
|
+
byteLength(reason) > CI_FIX_ATTRIBUTION_REASON_MAX_BYTES) {
|
|
269
|
+
return { status: "invalid", code: "attribution_invalid" };
|
|
270
|
+
}
|
|
271
|
+
fields[CI_FIX_ATTRIBUTION_KEY] = CI_FIX_ATTRIBUTION_EXTERNAL;
|
|
272
|
+
fields[CI_FIX_ATTRIBUTION_REASON_KEY] = reason;
|
|
273
|
+
}
|
|
274
|
+
const covered = requiredChecks.every((name) => Object.prototype.hasOwnProperty.call(validatedClaims, name));
|
|
275
|
+
return { status: covered ? "complete" : "partial", fields };
|
|
276
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* §16 — the amended proposal explicitly rejected `fetch && reset --hard`).
|
|
15
15
|
*/
|
|
16
16
|
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
17
|
-
import { ExecutorNamedError, WorktreeLostBeforePush } from "./job-errors.js";
|
|
17
|
+
import { ContinuationLineageMismatch, ContinuationWorktreeMissing, ExecutorNamedError, WorktreeLostBeforePush, } from "./job-errors.js";
|
|
18
18
|
import { assertWorktreeOnBranch, listGitWorktrees, findWorktreeByBranch, remoteBranchExists as remoteBranchExistsDefault, } from "./worktree-inspection.js";
|
|
19
19
|
/** The canonical WIP checkpoint commit message (executor-run, deterministic). */
|
|
20
20
|
export const RESUME_WIP_COMMIT_MESSAGE = "wip: auto-checkpoint before resume";
|
|
@@ -119,13 +119,13 @@ export async function recreateResumeWorktreeFromPushedBranch(job, expectedBranch
|
|
|
119
119
|
* is always clean and complete. No-op cleanly when the tree is already clean.
|
|
120
120
|
* NEVER discards work.
|
|
121
121
|
*/
|
|
122
|
-
export async function autoCheckpointDirtyState(worktreePath, seams) {
|
|
122
|
+
export async function autoCheckpointDirtyState(worktreePath, seams, message = RESUME_WIP_COMMIT_MESSAGE) {
|
|
123
123
|
const status = await seams.runCommand("git", ["status", "--porcelain"], { cwd: worktreePath });
|
|
124
124
|
if (status.stdout.trim().length === 0) {
|
|
125
125
|
return { committed: false };
|
|
126
126
|
}
|
|
127
127
|
await seams.runCommand("git", ["add", "-A"], { cwd: worktreePath });
|
|
128
|
-
await seams.runCommand("git", ["commit", "-m",
|
|
128
|
+
await seams.runCommand("git", ["commit", "-m", message], {
|
|
129
129
|
cwd: worktreePath,
|
|
130
130
|
});
|
|
131
131
|
return { committed: true };
|
|
@@ -144,6 +144,75 @@ export async function readResumeGitDiff(worktreePath, baseBranch, seams) {
|
|
|
144
144
|
export function fillResumePromptPlaceholders(template, gitLog, gitDiff) {
|
|
145
145
|
return template.split("{{GIT_LOG}}").join(gitLog).split("{{GIT_DIFF}}").join(gitDiff);
|
|
146
146
|
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// BAPI-1300 — parked-child continuation: retained-work verification
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/** The deterministic WIP checkpoint message for a continuation. */
|
|
151
|
+
export const CONTINUATION_WIP_COMMIT_MESSAGE = "wip: auto-checkpoint before continuation";
|
|
152
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
153
|
+
/** Read the continuation binding from a claimed job's payload (pure). */
|
|
154
|
+
export function readContinuationBinding(job) {
|
|
155
|
+
const payload = job.payload && typeof job.payload === "object" ? job.payload : null;
|
|
156
|
+
const raw = payload?.continuation;
|
|
157
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
158
|
+
return null;
|
|
159
|
+
const block = raw;
|
|
160
|
+
if (block.retained_worktree !== true)
|
|
161
|
+
return null;
|
|
162
|
+
const receiptId = asString(block.receipt_id);
|
|
163
|
+
if (!receiptId)
|
|
164
|
+
return null;
|
|
165
|
+
const head = asString(block.bound_head_sha)?.toLowerCase() ?? null;
|
|
166
|
+
return {
|
|
167
|
+
receiptId,
|
|
168
|
+
branch: asString(block.branch) ?? null,
|
|
169
|
+
boundHeadSha: head !== null && FULL_SHA.test(head) ? head : null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Prove the retained worktree's HEAD descends from the authorized head.
|
|
174
|
+
*
|
|
175
|
+
* Run BEFORE the WIP checkpoint: a checkpoint commits onto whatever HEAD is, so
|
|
176
|
+
* checking afterwards would already have written onto an unrelated history. A
|
|
177
|
+
* HEAD that equals the bound head, or has it as an ancestor (the worker's own
|
|
178
|
+
* later local commits), is accepted; anything else — including a bound head the
|
|
179
|
+
* worktree does not contain at all — is refused. No bound head (nothing was ever
|
|
180
|
+
* pushed) leaves only the branch check, which the caller has already made.
|
|
181
|
+
*/
|
|
182
|
+
export async function verifyContinuationHeadLineage(worktreePath, boundHeadSha, seams) {
|
|
183
|
+
if (boundHeadSha === null)
|
|
184
|
+
return;
|
|
185
|
+
const result = await seams.runCommand("git", ["merge-base", "--is-ancestor", boundHeadSha, "HEAD"], { cwd: worktreePath });
|
|
186
|
+
if (result.exitCode !== 0) {
|
|
187
|
+
throw new ExecutorNamedError(ContinuationLineageMismatch, `continuation worktree HEAD does not descend from the authorized head ${boundHeadSha.slice(0, 12)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Locate and verify a continuation's RETAINED worktree, then checkpoint it.
|
|
192
|
+
*
|
|
193
|
+
* Order: resolve the existing worktree (never recreate) -> verify its branch ->
|
|
194
|
+
* verify its head lineage -> WIP-checkpoint dirty state. A missing worktree is a
|
|
195
|
+
* refusal ({@link ContinuationWorktreeMissing}), not a recreation from origin:
|
|
196
|
+
* recreation cannot restore unpushed commits, and continuing without them would
|
|
197
|
+
* silently discard exactly the work the continuation exists to carry. The
|
|
198
|
+
* checkpoint is recorded as lineage of the authorized head; it never adopts an
|
|
199
|
+
* arbitrary head. No destructive Git operation is ever issued.
|
|
200
|
+
*/
|
|
201
|
+
export async function prepareRetainedContinuationWorktree(job, binding, seams) {
|
|
202
|
+
const fields = readResumePayloadFields(job);
|
|
203
|
+
const expectedBranch = asString(job.expected_branch) ?? fields.expectedBranch ?? binding.branch ?? undefined;
|
|
204
|
+
if (!expectedBranch) {
|
|
205
|
+
throw new ExecutorNamedError("ResumeMissingExpectedBranch", `continuation job ${job.id} has no expected_branch`);
|
|
206
|
+
}
|
|
207
|
+
const worktreePath = await resolveExistingResumeWorktreePath(fields, expectedBranch, seams);
|
|
208
|
+
if (worktreePath === null) {
|
|
209
|
+
throw new ExecutorNamedError(ContinuationWorktreeMissing, `continuation for '${expectedBranch}' found no retained worktree; it is never recreated from origin because unpushed work cannot be recovered that way`);
|
|
210
|
+
}
|
|
211
|
+
await assertWorktreeOnBranch(seams.runCommand, worktreePath, expectedBranch);
|
|
212
|
+
await verifyContinuationHeadLineage(worktreePath, binding.boundHeadSha, seams);
|
|
213
|
+
const checkpoint = await autoCheckpointDirtyState(worktreePath, seams, CONTINUATION_WIP_COMMIT_MESSAGE);
|
|
214
|
+
return { worktreePath, branch: expectedBranch, checkpointed: checkpoint.committed };
|
|
215
|
+
}
|
|
147
216
|
/**
|
|
148
217
|
* Orchestrate the resume pre-spawn protocol in the required order:
|
|
149
218
|
* 1. read payload fields
|
|
@@ -165,14 +234,28 @@ export async function prepareResumeSpawn(job, options, seams) {
|
|
|
165
234
|
// Fail loud on a missing/empty prompt template BEFORE any worktree resolution
|
|
166
235
|
// or WIP-checkpoint commit — never spawn an empty-prompt worker.
|
|
167
236
|
const promptTemplate = resolveResumePromptTemplate(job, fields);
|
|
237
|
+
// BAPI-1300: a resume dispatched as a parked-child continuation carries a
|
|
238
|
+
// binding and follows the retained-work protocol — never recreated from origin,
|
|
239
|
+
// and its head lineage is proven before the checkpoint. Every other resume job
|
|
240
|
+
// keeps the original protocol below unchanged.
|
|
241
|
+
const continuation = readContinuationBinding(job);
|
|
168
242
|
let worktreePath = await resolveExistingResumeWorktreePath(fields, expectedBranch, seams);
|
|
169
243
|
if (worktreePath === null) {
|
|
244
|
+
if (continuation !== null) {
|
|
245
|
+
throw new ExecutorNamedError(ContinuationWorktreeMissing, `continuation for '${expectedBranch}' found no retained worktree; it is never recreated from origin because unpushed work cannot be recovered that way`);
|
|
246
|
+
}
|
|
170
247
|
worktreePath = await recreateResumeWorktreeFromPushedBranch(job, expectedBranch, seams);
|
|
171
248
|
}
|
|
172
249
|
// Branch verification BEFORE any local mutation — never checkpoint/commit on
|
|
173
250
|
// the wrong branch.
|
|
174
251
|
await assertWorktreeOnBranch(seams.runCommand, worktreePath, expectedBranch);
|
|
175
|
-
|
|
252
|
+
if (continuation !== null) {
|
|
253
|
+
await verifyContinuationHeadLineage(worktreePath, continuation.boundHeadSha, seams);
|
|
254
|
+
await autoCheckpointDirtyState(worktreePath, seams, CONTINUATION_WIP_COMMIT_MESSAGE);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await autoCheckpointDirtyState(worktreePath, seams);
|
|
258
|
+
}
|
|
176
259
|
const gitLog = await readResumeGitLog(worktreePath, baseBranch, seams);
|
|
177
260
|
const gitDiff = await readResumeGitDiff(worktreePath, baseBranch, seams);
|
|
178
261
|
const prompt = fillResumePromptPlaceholders(promptTemplate, gitLog, gitDiff);
|
package/build/executor/runner.js
CHANGED
|
@@ -10,10 +10,14 @@ import { collectExecutorPreflight, buildClaimManifest, createDenyProbeCache, } f
|
|
|
10
10
|
import { runClaimedJob, runIntegrationCorrectionSpawn } from "./job-runner.js";
|
|
11
11
|
import { appendToActiveJobLogs } from "./job-log-registry.js";
|
|
12
12
|
import { createLiveWorkerRegistry, } from "./live-worker-registry.js";
|
|
13
|
+
import os from "node:os";
|
|
13
14
|
import { sweepExecutorWorktrees } from "./worktree-gc.js";
|
|
15
|
+
import { createMacProcessInspector, hostIdentityDigest, recoverInterruptedOwnership, } from "./process-containment.js";
|
|
16
|
+
import { resolveExecutorStateDir } from "./job-log-registry.js";
|
|
14
17
|
import { INTEGRATION_ACTION_PROTOCOL_VERSION, } from "./types.js";
|
|
15
18
|
import { buildIntegrationCorrectionJob, createIntegrationCorrectionLeaseAdapter, runIntegrationAction, } from "./integration-job.js";
|
|
16
19
|
import { validateBranchName } from "../base-ref.js";
|
|
20
|
+
import { isTerminalEpicRunStatus } from "../epic-run-status.js";
|
|
17
21
|
import { ensureEpicIntegrationPullRequest, createProductionEpicIntegrationGhRunner, formatEpicIntegrationPullRequestOutcome, EPIC_BRANCH_PREFIX, } from "../epic-integration-pr.js";
|
|
18
22
|
/**
|
|
19
23
|
* Signals that request a graceful executor shutdown (BAPI-828).
|
|
@@ -96,6 +100,16 @@ export function formatProcessHeartbeatFailureDiagnostic() {
|
|
|
96
100
|
export function formatProcessHeartbeatRecoveredDiagnostic() {
|
|
97
101
|
return "executor: process-heartbeat delivery has recovered.";
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* BAPI-1279 — the diagnostic for a failed run-state read under
|
|
105
|
+
* `--exit-when-run-terminal`. Fixed text plus a closed reason token: no body,
|
|
106
|
+
* no URL, no credential. The executor keeps claiming; a failed read never
|
|
107
|
+
* causes an exit.
|
|
108
|
+
*/
|
|
109
|
+
export function formatRunTerminalReadFailureDiagnostic(reason) {
|
|
110
|
+
return (`executor: could not read the scoped run's state (reason=${reason}); ` +
|
|
111
|
+
"staying up and claiming until it can be read.");
|
|
112
|
+
}
|
|
99
113
|
/** Seconds a correction worker may run: what the action deadline leaves, less a margin. */
|
|
100
114
|
function correctionTimeoutSeconds(deadlineAt, nowMs) {
|
|
101
115
|
const deadline = Date.parse(deadlineAt);
|
|
@@ -103,6 +117,23 @@ function correctionTimeoutSeconds(deadlineAt, nowMs) {
|
|
|
103
117
|
return 60;
|
|
104
118
|
return Math.max(60, Math.floor((deadline - nowMs) / 1000) - 60);
|
|
105
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* BAPI-1301: the read/remove-only record filesystem startup recovery uses, built
|
|
122
|
+
* from the executor's injected deps. Without a `readdir` seam there is nothing
|
|
123
|
+
* to list, so recovery is a no-op; recovery never writes a record.
|
|
124
|
+
*/
|
|
125
|
+
function recoveryRecordFs(deps) {
|
|
126
|
+
const refuseWrite = async () => {
|
|
127
|
+
throw new Error("startup ownership recovery never writes records");
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
mkdir: refuseWrite,
|
|
131
|
+
writeFile: refuseWrite,
|
|
132
|
+
readFile: (filePath) => deps.readFile(filePath),
|
|
133
|
+
readdir: deps.readdir ? (dirPath) => deps.readdir(dirPath) : async () => [],
|
|
134
|
+
removeFile: deps.removeFile ? (filePath) => deps.removeFile(filePath) : async () => { },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
106
137
|
/**
|
|
107
138
|
* Run the executor loop. Returns 0 on the `once` path (a single cycle drained);
|
|
108
139
|
* in continuous mode it does not return (the process is long-lived).
|
|
@@ -120,7 +151,43 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
120
151
|
// optional mtime boundary (keeps GC conservative → retains).
|
|
121
152
|
statMtimeMs: deps.statMtimeMs ?? (async () => null),
|
|
122
153
|
platform: deps.platform,
|
|
154
|
+
// BAPI-1330: finds the per-attempt worker logs for the activity probe.
|
|
155
|
+
...(deps.readdir ? { listDir: deps.readdir } : {}),
|
|
123
156
|
}));
|
|
157
|
+
// BAPI-1301 — before the first claim, inspect ownership records left by a
|
|
158
|
+
// previous executor process on this host. Each record holds the real per-job
|
|
159
|
+
// token, so the sweep can only ever reach processes that job tagged. The
|
|
160
|
+
// SERVER still parks such a job as `cleanup_unconfirmed` (it cannot know this
|
|
161
|
+
// inspection happened); the verdict here is logged for the operator.
|
|
162
|
+
const recoverOwnedProcesses = seams.recoverOwnedProcesses ??
|
|
163
|
+
(async () => {
|
|
164
|
+
const results = await recoverInterruptedOwnership({
|
|
165
|
+
stateDir: resolveExecutorStateDir({ env: deps.env, tmpdir: () => os.tmpdir(), platform: deps.platform }),
|
|
166
|
+
inspector: createMacProcessInspector({
|
|
167
|
+
runCommand: deps.runCommand,
|
|
168
|
+
selfPid: process.pid,
|
|
169
|
+
platform: deps.platform,
|
|
170
|
+
}),
|
|
171
|
+
deps: { now: deps.now, sleep: deps.sleep, log: deps.errorLog },
|
|
172
|
+
// Recovery only lists, reads and removes records, so it goes through the
|
|
173
|
+
// executor's own filesystem seams: an embedded caller or a test with
|
|
174
|
+
// in-memory deps never touches the real state directory.
|
|
175
|
+
fsDeps: recoveryRecordFs(deps),
|
|
176
|
+
deadlineMs: 30_000,
|
|
177
|
+
hostDigest: hostIdentityDigest(os.hostname()),
|
|
178
|
+
selfPid: process.pid,
|
|
179
|
+
});
|
|
180
|
+
for (const r of results) {
|
|
181
|
+
deps.errorLog(`[executor] startup ownership inspection: job ${r.jobId} cleanup ${r.verdict.verdict} ` +
|
|
182
|
+
`(survivors=${r.verdict.survivors})`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
await recoverOwnedProcesses();
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
deps.errorLog(`[executor] startup ownership inspection failed (non-fatal): ${err instanceof Error ? err.message.slice(0, 200) : "unknown error"}`);
|
|
190
|
+
}
|
|
124
191
|
const active = new Map();
|
|
125
192
|
// BAPI-1259 — integration actions share `active` (and therefore the same
|
|
126
193
|
// capacity budget and shutdown drain) with ordinary jobs, keyed by negative
|
|
@@ -293,6 +360,50 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
293
360
|
deps.errorLog("executor: epic wind-down observation failed unexpectedly (advisory only).");
|
|
294
361
|
}
|
|
295
362
|
};
|
|
363
|
+
// BAPI-1279 — `--exit-when-run-terminal`. Only ever true for a process scoped
|
|
364
|
+
// to exactly one run; the CLI refuses the flag otherwise, and this re-check
|
|
365
|
+
// keeps an embedded caller from arming it on an ambiguous scope.
|
|
366
|
+
const exitWhenRunTerminal = options.exitWhenRunTerminal === true && observedEpicRunId !== null;
|
|
367
|
+
let runTerminal = false;
|
|
368
|
+
// The last failure reason reported, so a long outage logs once per change
|
|
369
|
+
// rather than once per poll.
|
|
370
|
+
let lastRunStateFailure = null;
|
|
371
|
+
/**
|
|
372
|
+
* Is the scoped run finished? Called only between claim cycles while this
|
|
373
|
+
* process holds NO job. The wind-down observer runs FIRST, so a run that just
|
|
374
|
+
* reached `done` still gets its integration-PR ready request before the
|
|
375
|
+
* executor retires. Any failed read returns `false`: an executor that cannot
|
|
376
|
+
* see its run keeps claiming rather than guessing it is done.
|
|
377
|
+
*/
|
|
378
|
+
const scopedRunIsTerminal = async () => {
|
|
379
|
+
if (!exitWhenRunTerminal || observedEpicRunId === null)
|
|
380
|
+
return false;
|
|
381
|
+
await observeEpicRunCompletion();
|
|
382
|
+
const readState = httpClient.readEpicRunState;
|
|
383
|
+
if (typeof readState !== "function")
|
|
384
|
+
return false;
|
|
385
|
+
let reason = null;
|
|
386
|
+
let terminal = false;
|
|
387
|
+
try {
|
|
388
|
+
const result = await readState.call(httpClient, observedEpicRunId, options.repoName);
|
|
389
|
+
if (result.ok)
|
|
390
|
+
terminal = isTerminalEpicRunStatus(result.state.status);
|
|
391
|
+
else
|
|
392
|
+
reason = result.reason;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
reason = "unexpected";
|
|
396
|
+
}
|
|
397
|
+
if (reason !== null) {
|
|
398
|
+
if (reason !== lastRunStateFailure) {
|
|
399
|
+
deps.errorLog(formatRunTerminalReadFailureDiagnostic(reason));
|
|
400
|
+
}
|
|
401
|
+
lastRunStateFailure = reason;
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
lastRunStateFailure = null;
|
|
405
|
+
return terminal;
|
|
406
|
+
};
|
|
296
407
|
// BAPI-722: ONE deny-probe cache per `runExecutor` invocation, created OUTSIDE
|
|
297
408
|
// the claim loop below — that scope is the whole feature. A cache created inside
|
|
298
409
|
// the loop would be discarded every cycle and re-probe exactly as before; a
|
|
@@ -448,6 +559,7 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
448
559
|
* a failure is reported to stderr and the loop keeps going. It cannot fail a
|
|
449
560
|
* claim, fail a job, or end the run.
|
|
450
561
|
*/
|
|
562
|
+
let latestCorrectionQualification = null;
|
|
451
563
|
const runProcessHeartbeatLoop = async () => {
|
|
452
564
|
const publish = httpClient.processHeartbeat;
|
|
453
565
|
// A client without the capability (every object-literal test fake) simply
|
|
@@ -461,11 +573,39 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
461
573
|
component: "executor",
|
|
462
574
|
instance_id: options.executorId,
|
|
463
575
|
repo_names: repoNames,
|
|
576
|
+
// BAPI-1279 — the claim MODE, so the server can report whether a durable
|
|
577
|
+
// repo-wide executor covers the repository.
|
|
578
|
+
//
|
|
579
|
+
// BAPI-1282 — plus the full run id, but ONLY for an executor scoped to
|
|
580
|
+
// exactly one run. This deliberately reverses BAPI-1279's "never the run
|
|
581
|
+
// ids": the server needs the complete id to check, per repository, that
|
|
582
|
+
// the run this executor served is `done`/`abandoned`, so a cleanly retired
|
|
583
|
+
// executor's stale row stops reading as a lost executor — even when this
|
|
584
|
+
// process never reaches the server again. The id is one bounded, non-secret,
|
|
585
|
+
// server-minted UUID the operator already passed as `--epic-run-id`. The
|
|
586
|
+
// 8-character prefix inside `drive-epic-<runid8>-<host>` is not a substitute:
|
|
587
|
+
// it is ambiguous, and absent for a hand-chosen `--executor-id`. A
|
|
588
|
+
// multi-run or repo-wide executor publishes no binding.
|
|
589
|
+
metadata: options.epicRunIds === undefined
|
|
590
|
+
? { claim_mode: "repo_wide" }
|
|
591
|
+
: observedEpicRunId !== null
|
|
592
|
+
? { claim_mode: "scoped", epic_run_id: observedEpicRunId }
|
|
593
|
+
: { claim_mode: "scoped" },
|
|
464
594
|
};
|
|
595
|
+
const baseMetadata = request.metadata;
|
|
465
596
|
// Suppression state: one message per CHANGE of delivery state, so a long
|
|
466
597
|
// outage produces one line rather than one per minute.
|
|
467
598
|
let lastFailed = false;
|
|
468
599
|
for (;;) {
|
|
600
|
+
request.metadata = {
|
|
601
|
+
...baseMetadata,
|
|
602
|
+
...(latestCorrectionQualification
|
|
603
|
+
? { correction_intent: latestCorrectionQualification }
|
|
604
|
+
: {}),
|
|
605
|
+
...(latestCorrectionQualification?.qualified === true
|
|
606
|
+
? { ci_evidence: { version: 1, qualified: true } }
|
|
607
|
+
: {}),
|
|
608
|
+
};
|
|
469
609
|
let outcome;
|
|
470
610
|
try {
|
|
471
611
|
outcome = await publish.call(httpClient, request);
|
|
@@ -654,6 +794,16 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
654
794
|
try {
|
|
655
795
|
for (;;) {
|
|
656
796
|
const report = await collectPreflight(options, deps, preflightSeams);
|
|
797
|
+
latestCorrectionQualification = report.correctionIntentQualified === true &&
|
|
798
|
+
report.correctionQualificationEvidence?.hook_installed === true &&
|
|
799
|
+
report.correctionQualificationEvidence.nested_denial === true
|
|
800
|
+
? {
|
|
801
|
+
version: 1, qualified: true, hook_installed: true,
|
|
802
|
+
nested_denial: true,
|
|
803
|
+
fixture_id: report.correctionQualificationEvidence.fixture_id,
|
|
804
|
+
runtime: report.correctionQualificationEvidence.runtime,
|
|
805
|
+
}
|
|
806
|
+
: null;
|
|
657
807
|
// First heartbeat goes out immediately once preflight passes, so a freshly
|
|
658
808
|
// started executor is visible to the server without waiting one interval.
|
|
659
809
|
// Gated on `report.ok` because an executor that preflight refuses is not
|
|
@@ -758,6 +908,16 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
758
908
|
await maybeSweepWorktrees();
|
|
759
909
|
}
|
|
760
910
|
}
|
|
911
|
+
// BAPI-1279 — retire once the scoped run is finished and nothing is held.
|
|
912
|
+
// Checked whether or not preflight passed: an executor that preflight
|
|
913
|
+
// keeps refusing still holds a sleep assertion and a record, and must not
|
|
914
|
+
// outlive its run. `break` takes the SAME path a `--once` pass does:
|
|
915
|
+
// drain, stop the heartbeat loop, unsubscribe, exit 0.
|
|
916
|
+
if (!shutdownRequested && active.size === 0 && (await scopedRunIsTerminal())) {
|
|
917
|
+
runTerminal = true;
|
|
918
|
+
deps.errorLog(`executor: run ${observedEpicRunId} is finished and no job is held; exiting.`);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
761
921
|
if (options.once || shutdownRequested)
|
|
762
922
|
break;
|
|
763
923
|
// Sample the clock immediately around the ACTUAL sleep, so the measured gap
|
|
@@ -807,6 +967,14 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
807
967
|
}
|
|
808
968
|
}
|
|
809
969
|
}
|
|
970
|
+
if (runTerminal && observedEpicRunId !== null && seams.onRunTerminal) {
|
|
971
|
+
try {
|
|
972
|
+
await seams.onRunTerminal(observedEpicRunId);
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
/* record cleanup must never change a clean terminal exit */
|
|
976
|
+
}
|
|
977
|
+
}
|
|
810
978
|
// A graceful signal shutdown is a NORMAL executor exit, not an error: the run did
|
|
811
979
|
// exactly what it was asked to do. A distinct nonzero code here would make every
|
|
812
980
|
// deliberate operator stop look like a crash to launchd, systemd, and the CLI
|
|
@@ -25,10 +25,11 @@ export const CONDUCTOR_CRITIQUE_FILE = "critique.md";
|
|
|
25
25
|
/**
|
|
26
26
|
* The files a prior attempt may leave behind, and the ONLY ones removed here.
|
|
27
27
|
*
|
|
28
|
-
* `worker.lock` and
|
|
29
|
-
* coordination state, and
|
|
30
|
-
*
|
|
31
|
-
* file inside it are left
|
|
28
|
+
* `worker.lock` and the worker logs are deliberately absent: the lock is live
|
|
29
|
+
* coordination state, and each attempt's log is forensic evidence that its owner
|
|
30
|
+
* (`worker-log.ts`) creates exclusively and never truncates (BAPI-1330). The
|
|
31
|
+
* `.conductor/` directory itself and any unrelated file inside it are left
|
|
32
|
+
* untouched.
|
|
32
33
|
*/
|
|
33
34
|
export const STALE_ATTEMPT_ARTIFACT_FILES = [
|
|
34
35
|
CONDUCTOR_RESULT_FILE,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test support (BAPI-1302): build a required-verification declaration exactly as
|
|
3
|
+
* the server's `declaration_from_plan_node` seals it, so executor tests exercise
|
|
4
|
+
* the real strict reader rather than a hand-written shape.
|
|
5
|
+
*/
|
|
6
|
+
import { computeDeclarationDigest, declaredCommandIdentity } from "./completion-supervision.js";
|
|
7
|
+
export function sealDeclaration(commands, provenance = commands.length > 0 ? "declared" : "absent") {
|
|
8
|
+
const body = {
|
|
9
|
+
schema_version: 1,
|
|
10
|
+
provenance,
|
|
11
|
+
plan_version: 7,
|
|
12
|
+
plan_hash: "a".repeat(64),
|
|
13
|
+
commands: commands.map((c, position) => ({
|
|
14
|
+
identity: declaredCommandIdentity(position, c),
|
|
15
|
+
position,
|
|
16
|
+
...c,
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
return { ...body, digest: computeDeclarationDigest(body) };
|
|
20
|
+
}
|
package/build/executor/types.js
CHANGED
|
@@ -42,6 +42,19 @@ export const EXECUTOR_CLASSIFICATION_VALUES = [
|
|
|
42
42
|
"no_progress",
|
|
43
43
|
"lease_expired",
|
|
44
44
|
];
|
|
45
|
+
/**
|
|
46
|
+
* BAPI-1311 — the capability an executor declares when it can run a FOCUSED
|
|
47
|
+
* merge-stage `rebase`: validate `payload.focused_rebase` before any worker
|
|
48
|
+
* effect, use the server-rendered prompt verbatim, and supervise the post-push
|
|
49
|
+
* mergeability/required-check wait under the job's own claim and deadline.
|
|
50
|
+
* Mirrors `FOCUSED_REBASE_CAPABILITY` in
|
|
51
|
+
* `src/python/llms/executor_completion_contract.py`. Independent of completion
|
|
52
|
+
* evidence: `rebase` is not a covered job type.
|
|
53
|
+
*/
|
|
54
|
+
export const FOCUSED_REBASE_CAPABILITY = "focused_rebase.v1";
|
|
55
|
+
export const SCOPE_WAIT_CAPABILITY = "scope_wait.v1";
|
|
56
|
+
/** The focused context version; the same string as the capability on purpose. */
|
|
57
|
+
export const FOCUSED_REBASE_CONTEXT_VERSION = FOCUSED_REBASE_CAPABILITY;
|
|
45
58
|
/**
|
|
46
59
|
* BAPI-1258 — the terminal provider-capacity protocol contract, mirroring
|
|
47
60
|
* `src/python/llms/executor_capacity_contract.py`. Closed values: an executor
|