@bridge_gpt/mcp-server 0.2.65 → 0.2.67
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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- 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/model-thresholds-tool.js +185 -0
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { rm, readdir, lstat, unlink } from "node:fs/promises";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import { runHeartbeatLoop } from "./heartbeat.js";
|
|
20
|
-
import { isExecutorNamedError, toExecutorFailure, RebaseNotVerified, secretFreeErrorMessage, MissingVerdictArtifact, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, CiFixContext, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
|
|
20
|
+
import { isExecutorNamedError, toExecutorFailure, RebaseNotVerified, secretFreeErrorMessage, MissingVerdictArtifact, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, WorkerEndedWithPendingBackgroundWork, WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, CiFixContext, 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
22
|
import { createProductionCiRerunDeps, runCiRerunJob, } from "./ci-rerun-job.js";
|
|
23
23
|
import { createObservationState } from "./observation.js";
|
|
@@ -26,13 +26,13 @@ import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter
|
|
|
26
26
|
import { redactAdapterDiagnostic } from "../agent-launchers/executor-adapter.js";
|
|
27
27
|
import { resolveExecutorAgentId } from "./agent-identity.js";
|
|
28
28
|
import { closeStdinBounded, createProcessTerminationController, runProcessWithTimeout, writeStdinBounded, } from "./process.js";
|
|
29
|
-
import { buildNotLaunchedEnvelope, claimRequiresCompletionEvidence,
|
|
29
|
+
import { buildNotLaunchedEnvelope, claimRequiresCompletionEvidence, COMPLETION_CONTRACT_MARKER_VALUES, isCoveredJobType, MAX_EVENT_BYTES, runtimeMatchesSupported, successRefusalReason, verificationVerdictFromFacts, } from "./completion-contract.js";
|
|
30
30
|
import { COMPLETION_CAUSE_ERROR_KINDS, EOF_DRAIN_BOUND_MS, finalizationFacts, finalizationObligations, planCompletionAllowance, readCompletionBinding, readDeclaredChecks, readOptionalWatchers, readRequiredVerification, } from "./completion-supervision.js";
|
|
31
31
|
import { compareFinalArtifact, createRequiredVerificationRunner, createSupervisedCommandLauncher, observeFinalArtifact, OWNERSHIP_POLL_MS, preCheckArtifactState, toCompletionCheckFact, } from "./required-verification.js";
|
|
32
32
|
import { createClosureSupervisor, createRequiredWorkLedger } from "./required-work-ledger.js";
|
|
33
33
|
import { buildCleanupVerdict, CLEANUP_RESIDUALS_NOT_COVERED, createDefaultOwnershipRecordFs, createDescendantTracker, createMacProcessInspector, createOwnershipToken, hostIdentityDigest, ownershipTokenDigest, removeOwnershipRecord, runOwnedProcessSweep, withOwnershipTag, writeOwnershipRecord, } from "./process-containment.js";
|
|
34
34
|
import { resolveCompletionEvidenceCapability } from "../agent-launchers/executor-adapter.js";
|
|
35
|
-
import { prepareResumeSpawn, prepareRetainedContinuationWorktree, readContinuationBinding, } from "./resume-pre-spawn.js";
|
|
35
|
+
import { autoCheckpointDirtyState, PRIOR_ATTEMPT_WIP_COMMIT_MESSAGE, prepareResumeSpawn, prepareRetainedContinuationWorktree, readContinuationBinding, verifyContinuationHeadLineage, } from "./resume-pre-spawn.js";
|
|
36
36
|
import { ciFixContextError, readCiFixDispatchContext, renderPromptSpecPrompt, } from "./prompt-spec.js";
|
|
37
37
|
import { defaultResolveBinPath } from "../start-tickets-conductor.js";
|
|
38
38
|
/** The packaged bin basename of the deterministic worker guard (BAPI-1020). */
|
|
@@ -42,7 +42,7 @@ import { sendTerminalMutationWithRetry } from "./terminal-mutation.js";
|
|
|
42
42
|
import { removeStaleVerdictArtifacts } from "./stale-artifacts.js";
|
|
43
43
|
import { stripWorkerEnvFiles } from "./env-file-guard.js";
|
|
44
44
|
import { isVerdictJobType, readVerdictArtifact } from "./verdict-artifact.js";
|
|
45
|
-
import { createWorkerLogTee, closeWorkerLogTee, distinctWorkerLogSessionIds, formatWorkerLogSessionInvariantWarning, teeAsyncIterable, } from "./worker-log.js";
|
|
45
|
+
import { createWorkerLogClaimId, createWorkerLogTee, closeWorkerLogTee, distinctWorkerLogSessionIds, formatWorkerLogSessionInvariantWarning, teeAsyncIterable, } from "./worker-log.js";
|
|
46
46
|
import { acquireExecutorWorktreeLock, } from "./worktree-lock.js";
|
|
47
47
|
import { resolveExecutorStateDir, activateJobLog, deactivateJobLog, registerExecutorJobLog, markExecutorJobLogFinished, } from "./job-log-registry.js";
|
|
48
48
|
import { executorViewerTabsEnabled, openExecutorViewerTab } from "./viewer-tabs.js";
|
|
@@ -56,8 +56,10 @@ import { FOCUSED_REBASE_JOB_TYPE, focusedRebaseContextError, hasFocusedRebaseCon
|
|
|
56
56
|
import { ensureExecutorWorktree, finalizeRetainedExecutorWorktree, } from "./worktree.js";
|
|
57
57
|
import { buildPrBaseContractLaunchInstruction } from "../pr-base-contract.js";
|
|
58
58
|
import { CI_FIX_JOB_TYPE, resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
|
|
59
|
+
import { CORRECTION_INTENT_CAPABILITY, isRemainingWork, validateCorrectionIntent, } from "./correction-dispatch.js";
|
|
59
60
|
import { collectGitTelemetry, collectRemoteTrackingSha, createMcpSurfaceObserver, evaluatePreSpawnGitVerification, formatWorkerRateLimitAdvisory, normalizeMcpServerNames, } from "./observation.js";
|
|
60
61
|
import { TERMINAL_CAPACITY_PROTOCOL_VERSIONS, TERMINAL_CAPACITY_RUNTIME_CONTRACTS, TERMINAL_CAPACITY_RUNTIMES, } from "./types.js";
|
|
62
|
+
import { probeInstalledCorrectionHook } from "./preflight.js";
|
|
61
63
|
import { isRecognizedServerName, MCP_SERVER_NAME } from "../mcp-identity.js";
|
|
62
64
|
/**
|
|
63
65
|
* Bound on the pre-spawn git tracked-file lookup (BAPI-1104, R81).
|
|
@@ -174,6 +176,9 @@ function buildJobLogRegistryDeps(deps) {
|
|
|
174
176
|
env: deps.env,
|
|
175
177
|
tmpdir: () => os.tmpdir(),
|
|
176
178
|
platform: deps.platform,
|
|
179
|
+
// BAPI-1330: atomic temp-file + rename replacement when the host has it.
|
|
180
|
+
...(deps.rename ? { rename: deps.rename } : {}),
|
|
181
|
+
errorLog: deps.errorLog,
|
|
177
182
|
};
|
|
178
183
|
}
|
|
179
184
|
/**
|
|
@@ -535,6 +540,46 @@ function createNoopProcess(deps, durationMs, message) {
|
|
|
535
540
|
},
|
|
536
541
|
};
|
|
537
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Bounded newline assembly of worker stdout into whole lines for a
|
|
545
|
+
* {@link RequiredWorkLedger} (BAPI-1301 covered path, BAPI-1364 legacy
|
|
546
|
+
* observation). A line that outgrows the event bound is handed over once (the
|
|
547
|
+
* ledger marks it `stream_oversize`) and the rest of it is discarded up to its
|
|
548
|
+
* newline. `onLine` runs after each completed line is ingested.
|
|
549
|
+
*/
|
|
550
|
+
function createLedgerLineFeeder(ledger, onLine) {
|
|
551
|
+
let lineBuffer = "";
|
|
552
|
+
let discardingOversize = false;
|
|
553
|
+
return {
|
|
554
|
+
feed: (chunk) => {
|
|
555
|
+
lineBuffer += chunk;
|
|
556
|
+
let newline;
|
|
557
|
+
while ((newline = lineBuffer.indexOf("\n")) >= 0) {
|
|
558
|
+
const line = lineBuffer.slice(0, newline);
|
|
559
|
+
lineBuffer = lineBuffer.slice(newline + 1);
|
|
560
|
+
if (discardingOversize) {
|
|
561
|
+
discardingOversize = false;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
ledger.ingestLine(line);
|
|
565
|
+
onLine?.();
|
|
566
|
+
}
|
|
567
|
+
if (!discardingOversize && lineBuffer.length > MAX_EVENT_BYTES) {
|
|
568
|
+
ledger.ingestLine(lineBuffer);
|
|
569
|
+
lineBuffer = "";
|
|
570
|
+
discardingOversize = true;
|
|
571
|
+
}
|
|
572
|
+
else if (discardingOversize) {
|
|
573
|
+
lineBuffer = "";
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
flush: () => {
|
|
577
|
+
if (!discardingOversize && lineBuffer.length > 0)
|
|
578
|
+
ledger.ingestLine(lineBuffer);
|
|
579
|
+
lineBuffer = "";
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
538
583
|
/**
|
|
539
584
|
* Run the process and heartbeat loop concurrently; return the process outcome.
|
|
540
585
|
*
|
|
@@ -564,7 +609,10 @@ async function superviseProcess(params) {
|
|
|
564
609
|
...(params.terminationController
|
|
565
610
|
? { terminationController: params.terminationController }
|
|
566
611
|
: {}),
|
|
567
|
-
onStdout: (chunk) =>
|
|
612
|
+
onStdout: (chunk) => {
|
|
613
|
+
params.observation.recordStdout(chunk);
|
|
614
|
+
params.onStdoutObserved?.(chunk);
|
|
615
|
+
},
|
|
568
616
|
...(mcpSurface
|
|
569
617
|
? {
|
|
570
618
|
onStdoutTerminationCheck: (chunk) => {
|
|
@@ -651,6 +699,23 @@ async function sendTerminalMutationWithAuthReport(params, job, control) {
|
|
|
651
699
|
* `/executor/jobs`. Integration actions are NOT ordinary executor jobs.
|
|
652
700
|
*/
|
|
653
701
|
export async function runIntegrationCorrectionSpawn(job, httpClient, options, deps, driver, seams = {}, control) {
|
|
702
|
+
const validated = validateCorrectionIntent(job.payload?.correction_intent, {
|
|
703
|
+
repo_name: job.repo_name,
|
|
704
|
+
epic_run_id: job.epic_run_id ?? null,
|
|
705
|
+
ticket_key: job.ticket_key ?? null,
|
|
706
|
+
expected_branch: job.expected_branch,
|
|
707
|
+
expected_head_sha: job.expected_head_sha,
|
|
708
|
+
});
|
|
709
|
+
if (!validated.ok || validated.intent.kind !== "integration_correction" ||
|
|
710
|
+
(validated.ok && validated.intent.context_ref.action_id !==
|
|
711
|
+
job.payload?.integration_action_id)) {
|
|
712
|
+
const reason = validated.ok ? "correction_contract_invalid" : validated.code;
|
|
713
|
+
await httpClient.fail(job, {
|
|
714
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
715
|
+
error_message: reason, classification: "crashed",
|
|
716
|
+
});
|
|
717
|
+
return { status: "failed", reason };
|
|
718
|
+
}
|
|
654
719
|
const { ownership, observation } = createJobRunState(options, deps);
|
|
655
720
|
return runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver);
|
|
656
721
|
}
|
|
@@ -695,7 +760,11 @@ control) {
|
|
|
695
760
|
// any /fail that has no envelope gets the truthful "not launched" envelope and
|
|
696
761
|
// keeps its own error kind; reports that already carry one are untouched.
|
|
697
762
|
if (claimIsCovered(job)) {
|
|
698
|
-
httpClient = withNotLaunchedEnvelopeFallback(httpClient, job
|
|
763
|
+
httpClient = withNotLaunchedEnvelopeFallback(httpClient, job, {
|
|
764
|
+
jobTimeRuntime: () => jobTimeCompletionRuntimes.get(ownership) ?? null,
|
|
765
|
+
preflightRuntime: _report?.completionEvidenceQualified === true ? (_report.completionRuntime ?? null) : null,
|
|
766
|
+
agentId: resolveExecutorAgentId(options.agentId),
|
|
767
|
+
});
|
|
699
768
|
}
|
|
700
769
|
// `merge` is a deterministic executor action: dispatched BEFORE any worktree
|
|
701
770
|
// ensure/recreate or worker-spawn logic — it ensures no worktree and spawns no
|
|
@@ -1175,7 +1244,20 @@ async function superviseDriverPhase(params) {
|
|
|
1175
1244
|
collectTelemetry: params.collectTelemetry,
|
|
1176
1245
|
control: params.control,
|
|
1177
1246
|
});
|
|
1178
|
-
|
|
1247
|
+
let settleTimer;
|
|
1248
|
+
try {
|
|
1249
|
+
await Promise.race([
|
|
1250
|
+
flow.settled,
|
|
1251
|
+
new Promise((resolve) => {
|
|
1252
|
+
settleTimer = params.deps.setTimer(resolve, DRIVER_PHASE_SETTLE_MAX_MS);
|
|
1253
|
+
}),
|
|
1254
|
+
]);
|
|
1255
|
+
}
|
|
1256
|
+
finally {
|
|
1257
|
+
// A normal phase must not leave its four-minute bound alive after it settles.
|
|
1258
|
+
if (settleTimer !== undefined)
|
|
1259
|
+
params.deps.clearTimer(settleTimer);
|
|
1260
|
+
}
|
|
1179
1261
|
if (procResult.serverStopRequested)
|
|
1180
1262
|
return { kind: "server_stop" };
|
|
1181
1263
|
if (params.ownership.abandoned)
|
|
@@ -1323,10 +1405,56 @@ async function ensureJobWorktree(job, options, deps, seams, policy) {
|
|
|
1323
1405
|
}
|
|
1324
1406
|
}
|
|
1325
1407
|
async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
1408
|
+
// A continuation passes no local head here (`head === undefined`): its retained
|
|
1409
|
+
// worktree was already proven by `prepareRetainedContinuationWorktree` →
|
|
1410
|
+
// `verifyContinuationHeadLineage`, which refuses before spawn unless HEAD
|
|
1411
|
+
// descends from `continuation.bound_head_sha` — the same head as
|
|
1412
|
+
// `correction_intent.head_sha`, including one the server derived for a first
|
|
1413
|
+
// attempt with no bound ticket head (BAPI-1344). The spawn source is always
|
|
1414
|
+
// that retained worktree, never an origin checkout.
|
|
1415
|
+
const refuseCorrectionDrift = async (branch, head) => {
|
|
1416
|
+
if (!isRemainingWork(job.job_type, job.payload))
|
|
1417
|
+
return null;
|
|
1418
|
+
const intent = job.payload?.correction_intent;
|
|
1419
|
+
if (branch === intent.branch && (head === undefined || head === intent.head_sha))
|
|
1420
|
+
return null;
|
|
1421
|
+
const reason = "correction_context_missing";
|
|
1422
|
+
await httpClient.fail(job, {
|
|
1423
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
1424
|
+
error_message: reason,
|
|
1425
|
+
classification: "crashed",
|
|
1426
|
+
});
|
|
1427
|
+
return { ok: false, result: { status: "failed", reason } };
|
|
1428
|
+
};
|
|
1429
|
+
if (isRemainingWork(job.job_type, job.payload)) {
|
|
1430
|
+
const intent = validateCorrectionIntent(job.payload?.correction_intent, {
|
|
1431
|
+
repo_name: job.repo_name,
|
|
1432
|
+
epic_run_id: job.epic_run_id ?? null,
|
|
1433
|
+
ticket_key: job.ticket_key ?? null,
|
|
1434
|
+
expected_branch: job.expected_branch,
|
|
1435
|
+
expected_head_sha: job.expected_head_sha,
|
|
1436
|
+
});
|
|
1437
|
+
const reason = !intent.ok
|
|
1438
|
+
? intent.code
|
|
1439
|
+
: !job.required_capabilities?.includes(CORRECTION_INTENT_CAPABILITY)
|
|
1440
|
+
? "correction_contract_invalid"
|
|
1441
|
+
: null;
|
|
1442
|
+
if (reason !== null) {
|
|
1443
|
+
await httpClient.fail(job, {
|
|
1444
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
1445
|
+
error_message: reason,
|
|
1446
|
+
classification: "crashed",
|
|
1447
|
+
});
|
|
1448
|
+
return { ok: false, result: { status: "failed", reason } };
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1326
1451
|
if (job.job_type === "resume") {
|
|
1327
1452
|
const prepareResume = seams.prepareResumeSpawn ?? prepareResumeSpawn;
|
|
1328
1453
|
try {
|
|
1329
1454
|
const prepared = await prepareResume(job, options, buildResumeSeams(deps, options));
|
|
1455
|
+
const drift = await refuseCorrectionDrift(prepared.branch);
|
|
1456
|
+
if (drift)
|
|
1457
|
+
return drift;
|
|
1330
1458
|
return {
|
|
1331
1459
|
ok: true,
|
|
1332
1460
|
worktreePath: prepared.worktreePath,
|
|
@@ -1386,6 +1514,9 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1386
1514
|
await httpClient.fail(job, jobWorktreeFailure(wt));
|
|
1387
1515
|
return { ok: false, result: { status: "failed", reason: "worktree_create_failed" } };
|
|
1388
1516
|
}
|
|
1517
|
+
const drift = await refuseCorrectionDrift(wt.branch, readContinuationBinding(job) ? undefined : wt.expectedHeadSha);
|
|
1518
|
+
if (drift)
|
|
1519
|
+
return drift;
|
|
1389
1520
|
return {
|
|
1390
1521
|
ok: true,
|
|
1391
1522
|
worktreePath: wt.worktreePath,
|
|
@@ -1461,11 +1592,18 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1461
1592
|
// validated against a closed set — never inferred locally, and never taken
|
|
1462
1593
|
// from worker output (R14 rules 3 and 4).
|
|
1463
1594
|
staleBranchClassification: resolveStaleBranchClassification(job.payload),
|
|
1595
|
+
// BAPI-1367: the server-minted (and client-normalized) prior head. Only an
|
|
1596
|
+
// `implement` retry acts on it; `ensureExecutorWorktree` re-checks that.
|
|
1597
|
+
// Spread only when present so an ordinary job keeps its exact legacy policy.
|
|
1598
|
+
...(job.prior_attempt_head_sha ? { priorAttemptHeadSha: job.prior_attempt_head_sha } : {}),
|
|
1464
1599
|
});
|
|
1465
1600
|
if (!wt.ok) {
|
|
1466
1601
|
await httpClient.fail(job, jobWorktreeFailure(wt));
|
|
1467
1602
|
return { ok: false, result: { status: "failed", reason: "worktree_create_failed" } };
|
|
1468
1603
|
}
|
|
1604
|
+
const drift = await refuseCorrectionDrift(wt.branch, readContinuationBinding(job) ? undefined : wt.expectedHeadSha);
|
|
1605
|
+
if (drift)
|
|
1606
|
+
return drift;
|
|
1469
1607
|
// Explicit/synthesized prompt wins verbatim, preserving existing behavior.
|
|
1470
1608
|
// BAPI-1311: for a focused rebase that explicit prompt IS the validated
|
|
1471
1609
|
// server-rendered prompt (`validateFocusedRebaseContext` required it).
|
|
@@ -1476,6 +1614,7 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1476
1614
|
branch: wt.branch,
|
|
1477
1615
|
prompt: explicit.prompt,
|
|
1478
1616
|
expectedHeadSha: wt.expectedHeadSha,
|
|
1617
|
+
...(wt.priorWork ? { priorWork: wt.priorWork } : {}),
|
|
1479
1618
|
...(focusedRebase ? { focusedRebase } : {}),
|
|
1480
1619
|
};
|
|
1481
1620
|
}
|
|
@@ -1498,6 +1637,7 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1498
1637
|
branch: wt.branch,
|
|
1499
1638
|
prompt: rendered,
|
|
1500
1639
|
expectedHeadSha: wt.expectedHeadSha,
|
|
1640
|
+
...(wt.priorWork ? { priorWork: wt.priorWork } : {}),
|
|
1501
1641
|
};
|
|
1502
1642
|
}
|
|
1503
1643
|
catch (err) {
|
|
@@ -1512,6 +1652,93 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1512
1652
|
return { ok: false, result: { status: "failed", reason: "prompt_contract" } };
|
|
1513
1653
|
}
|
|
1514
1654
|
}
|
|
1655
|
+
/** The first twelve characters of a SHA — safe, supporting evidence only. */
|
|
1656
|
+
function shortSha(sha) {
|
|
1657
|
+
return sha.slice(0, 12);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* BAPI-1367 — the continuation wording prepended to a retried `implement` prompt.
|
|
1661
|
+
*
|
|
1662
|
+
* States the work state first (this is prior-attempt work, not a fresh start),
|
|
1663
|
+
* says whether uncommitted files survived as a WIP checkpoint or were unavailable
|
|
1664
|
+
* because the worktree was recreated from the pushed branch, and leaves the
|
|
1665
|
+
* resolved prompt untouched after it. Short SHAs are the only identifiers used.
|
|
1666
|
+
*/
|
|
1667
|
+
export function buildPriorAttemptContinuationNotice(params) {
|
|
1668
|
+
const { branch, priorWork, checkpointed, ticketKey } = params;
|
|
1669
|
+
const recreated = priorWork.kind === "recreated_from_pushed_branch";
|
|
1670
|
+
const command = ticketKey ? `/implement-ticket ${ticketKey} --auto` : "/implement-ticket";
|
|
1671
|
+
const files = recreated
|
|
1672
|
+
? "Prior uncommitted files were unavailable on this host: the worktree was recreated from the pushed branch, so only committed work is present."
|
|
1673
|
+
: checkpointed
|
|
1674
|
+
? "Prior uncommitted files were saved as a WIP checkpoint commit so nothing was lost."
|
|
1675
|
+
: "The retained worktree was clean, so no WIP checkpoint was needed.";
|
|
1676
|
+
return (`CONTINUATION: this is a retry. An earlier attempt of this job left work on branch '${branch}' ` +
|
|
1677
|
+
`(its recorded head was ${shortSha(priorWork.priorHeadSha)}). You are continuing that prior-attempt ` +
|
|
1678
|
+
`work, not starting ${command} from base: read the branch's existing commits first, keep what is ` +
|
|
1679
|
+
`correct, and finish only what is missing. ${files} The original instructions follow unchanged.\n\n`);
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* BAPI-1367 — finish a prior-attempt resume UNDER THE WORKTREE LOCK.
|
|
1683
|
+
*
|
|
1684
|
+
* Order is the contract: prove the retained HEAD descends from the recorded prior
|
|
1685
|
+
* head, WIP-checkpoint dirty files (a checkpoint commits onto whatever HEAD is, so
|
|
1686
|
+
* lineage must be proven first), then finalize — which pins `expectedHeadSha` to
|
|
1687
|
+
* the POST-checkpoint head and provisions command assets. No destructive Git
|
|
1688
|
+
* command is ever issued. Diagnostics carry the job id, attempt number, and short
|
|
1689
|
+
* SHAs only — never the prompt or a full hash.
|
|
1690
|
+
*/
|
|
1691
|
+
async function resumePriorAttemptUnderLock(params) {
|
|
1692
|
+
const { job, prep, options, deps, worktreePath, branch } = params;
|
|
1693
|
+
const priorWork = prep.priorWork;
|
|
1694
|
+
const resumeSeams = buildResumeSeams(deps, options);
|
|
1695
|
+
try {
|
|
1696
|
+
await verifyContinuationHeadLineage(worktreePath, priorWork.priorHeadSha, resumeSeams);
|
|
1697
|
+
const checkpoint = await autoCheckpointDirtyState(worktreePath, resumeSeams, PRIOR_ATTEMPT_WIP_COMMIT_MESSAGE);
|
|
1698
|
+
const finalized = await finalizeRetainedExecutorWorktree(worktreePath, branch, deps);
|
|
1699
|
+
if (!finalized.ok) {
|
|
1700
|
+
return {
|
|
1701
|
+
ok: false,
|
|
1702
|
+
failure: {
|
|
1703
|
+
error_kind: "WorktreeError",
|
|
1704
|
+
error_message: finalized.error,
|
|
1705
|
+
classification: "crashed",
|
|
1706
|
+
},
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
const notice = buildPriorAttemptContinuationNotice({
|
|
1710
|
+
branch,
|
|
1711
|
+
priorWork,
|
|
1712
|
+
checkpointed: checkpoint.committed,
|
|
1713
|
+
ticketKey: job.ticket_key,
|
|
1714
|
+
});
|
|
1715
|
+
deps.errorLog(`[executor] job ${job.id}: attempt ${job.attempt_count} resuming prior-attempt work ` +
|
|
1716
|
+
`(prior head ${shortSha(priorWork.priorHeadSha)}, resumed at ${shortSha(finalized.expectedHeadSha)}` +
|
|
1717
|
+
`${checkpoint.committed ? ", dirty files checkpointed" : ""})`);
|
|
1718
|
+
if (priorWork.kind === "recreated_from_pushed_branch") {
|
|
1719
|
+
deps.errorLog(`[executor] job ${job.id}: attempt ${job.attempt_count} worktree recreated from the pushed ` +
|
|
1720
|
+
`branch; prior uncommitted files were unavailable (prior head ${shortSha(priorWork.priorHeadSha)})`);
|
|
1721
|
+
}
|
|
1722
|
+
return {
|
|
1723
|
+
ok: true,
|
|
1724
|
+
prep: {
|
|
1725
|
+
...prep,
|
|
1726
|
+
prompt: `${notice}${prep.prompt}`,
|
|
1727
|
+
expectedHeadSha: finalized.expectedHeadSha,
|
|
1728
|
+
},
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
catch (err) {
|
|
1732
|
+
const failure = isExecutorNamedError(err)
|
|
1733
|
+
? toExecutorFailure(err)
|
|
1734
|
+
: {
|
|
1735
|
+
error_kind: "WorktreeError",
|
|
1736
|
+
error_message: secretFreeErrorMessage(err),
|
|
1737
|
+
classification: "crashed",
|
|
1738
|
+
};
|
|
1739
|
+
return { ok: false, failure };
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1515
1742
|
/**
|
|
1516
1743
|
* BAPI-1296 — verify a prepared `ci_fix` checkout against its dispatched CI
|
|
1517
1744
|
* context and render the focused prompt, UNDER THE WORKTREE LOCK.
|
|
@@ -1724,6 +1951,34 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
|
|
|
1724
1951
|
* adds no second worktree entry and no second worker launcher.
|
|
1725
1952
|
*/
|
|
1726
1953
|
driver) {
|
|
1954
|
+
// A server-owned read precedes every worktree reset/cut and worker launch.
|
|
1955
|
+
// Only this boundary can authorize a no-charge pre-work wait; a later plan
|
|
1956
|
+
// retrieval failure is ordinary bounded worker failure.
|
|
1957
|
+
if (job.epic_run_id && (httpClient.scopeReadiness ||
|
|
1958
|
+
job.required_capabilities?.includes("scope_wait.v1"))) {
|
|
1959
|
+
if (!httpClient.scopeReadiness) {
|
|
1960
|
+
await httpClient.fail(job, {
|
|
1961
|
+
error_kind: "ContractError.ScopeReadiness",
|
|
1962
|
+
error_message: "Required pre-work scope protocol is unavailable.",
|
|
1963
|
+
classification: "crashed",
|
|
1964
|
+
});
|
|
1965
|
+
return { status: "failed", reason: "scope_readiness_unsupported" };
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
const readiness = await httpClient.scopeReadiness(job);
|
|
1969
|
+
if (readiness.decision !== "ready") {
|
|
1970
|
+
return { status: "failed", reason: `scope_${readiness.decision}` };
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
catch {
|
|
1974
|
+
await httpClient.fail(job, {
|
|
1975
|
+
error_kind: "ScopeReadinessUnavailable",
|
|
1976
|
+
error_message: "Server scope readiness could not be confirmed.",
|
|
1977
|
+
classification: "crashed",
|
|
1978
|
+
});
|
|
1979
|
+
return { status: "failed", reason: "scope_readiness_unavailable" };
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1727
1982
|
// --- Per-job base branch (BAPI-586) ----------------------------------
|
|
1728
1983
|
// Resolve the effective logical base BEFORE any side effect: the persisted
|
|
1729
1984
|
// run base (`payload.base_branch`) is authoritative, falling back to the
|
|
@@ -1824,11 +2079,12 @@ driver) {
|
|
|
1824
2079
|
// --- Worktree mutual exclusion (FAIL-CLOSED, BAPI-793) ----------------
|
|
1825
2080
|
// The FIRST thing that happens once a canonical worktree path exists, and
|
|
1826
2081
|
// ahead of every executor-owned write to it: the deny layer, MCP
|
|
1827
|
-
// provisioning, the
|
|
1828
|
-
// itself all live inside the lock. `worker.log`
|
|
1829
|
-
//
|
|
1830
|
-
// erase the running worker's forensic log
|
|
1831
|
-
//
|
|
2082
|
+
// provisioning, the attempt log allocation, the viewer tab, and the spawn
|
|
2083
|
+
// itself all live inside the lock. Before BAPI-1330 the shared `worker.log`
|
|
2084
|
+
// was created with a truncating write, so a refused contender that got as far
|
|
2085
|
+
// as the tee could erase the running worker's forensic log; per-attempt logs
|
|
2086
|
+
// are now created exclusively, and refusal still returns from HERE rather
|
|
2087
|
+
// than anywhere downstream.
|
|
1832
2088
|
//
|
|
1833
2089
|
// Known residual window, stated rather than papered over: `prepareSpawn`
|
|
1834
2090
|
// above already ran `ensureWorktree`, which hard-resets and cleans the tree.
|
|
@@ -1869,6 +2125,20 @@ driver) {
|
|
|
1869
2125
|
// a refused ci_fix leaves the worktree exactly as preparation left it. The
|
|
1870
2126
|
// verified, rendered prompt replaces preparation's empty placeholder.
|
|
1871
2127
|
let readyPrep = prep;
|
|
2128
|
+
// --- Prior-attempt resume (BAPI-1367) --------------------------------
|
|
2129
|
+
// Inside the lock, and BEFORE artifact cleanup and every executor-owned write:
|
|
2130
|
+
// prove the retained head's lineage, WIP-checkpoint any dirty files, and only
|
|
2131
|
+
// then pin the pre-spawn expectation to the POST-checkpoint head. Pinning
|
|
2132
|
+
// earlier (to base or to the lineage anchor) would make the BAPI-731
|
|
2133
|
+
// assertion refuse the very checkpoint this step just made.
|
|
2134
|
+
if (prep.priorWork) {
|
|
2135
|
+
const resumed = await resumePriorAttemptUnderLock({ job, prep, options, deps, worktreePath, branch });
|
|
2136
|
+
if (!resumed.ok) {
|
|
2137
|
+
await httpClient.fail(job, resumed.failure);
|
|
2138
|
+
return { status: "failed", reason: "prior_attempt_resume_failed" };
|
|
2139
|
+
}
|
|
2140
|
+
readyPrep = resumed.prep;
|
|
2141
|
+
}
|
|
1872
2142
|
if (prep.ciFix) {
|
|
1873
2143
|
const rendered = await renderVerifiedCiFixPrompt({
|
|
1874
2144
|
job,
|
|
@@ -2114,6 +2384,12 @@ async function runPreparedSpawn(params) {
|
|
|
2114
2384
|
// safety margin but must not kill a job. Refusing to CLAIM on unenforced deny
|
|
2115
2385
|
// remains a separate, fatal preflight concern.
|
|
2116
2386
|
const denyCapability = adapter.denyEnforcement;
|
|
2387
|
+
const correctionWork = isRemainingWork(job.job_type, job.payload);
|
|
2388
|
+
let correctionDenyInstalled = false;
|
|
2389
|
+
const correctionHookBinPath = denyCapability?.supported === true
|
|
2390
|
+
? (seams.resolveWorkerGuardBinPath ??
|
|
2391
|
+
(() => defaultResolveBinPath(WORKER_GUARD_BIN_FILENAME)))()
|
|
2392
|
+
: "";
|
|
2117
2393
|
if (denyCapability?.supported === true) {
|
|
2118
2394
|
const deny = await denyCapability.value.provisionWorktreeDenyLayer({
|
|
2119
2395
|
worktreePath,
|
|
@@ -2125,8 +2401,8 @@ async function runPreparedSpawn(params) {
|
|
|
2125
2401
|
// "is this push destination mine?" by comparing against it, so a value the
|
|
2126
2402
|
// payload could influence would let a job nominate its own safe branch.
|
|
2127
2403
|
workerBranch: branch,
|
|
2128
|
-
|
|
2129
|
-
|
|
2404
|
+
...(correctionWork ? { correctionIntentVersion: 1 } : {}),
|
|
2405
|
+
workerGuardHookBinPath: correctionHookBinPath,
|
|
2130
2406
|
});
|
|
2131
2407
|
// Log on WARNING PRESENCE, not on `!ok` (BAPI-815/R2). The git-exclude
|
|
2132
2408
|
// hygiene step can degrade without failing the deny layer — present-but-
|
|
@@ -2138,6 +2414,40 @@ async function runPreparedSpawn(params) {
|
|
|
2138
2414
|
// fail-OPEN and the job continues to the spawn boundary.
|
|
2139
2415
|
if (deny.warning)
|
|
2140
2416
|
deps.errorLog(deny.warning);
|
|
2417
|
+
correctionDenyInstalled = deny.ok;
|
|
2418
|
+
}
|
|
2419
|
+
if (correctionWork && !correctionDenyInstalled) {
|
|
2420
|
+
await httpClient.fail(job, {
|
|
2421
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
2422
|
+
error_message: "correction_contract_invalid",
|
|
2423
|
+
classification: "crashed",
|
|
2424
|
+
});
|
|
2425
|
+
return { status: "failed", reason: "correction_hook_unavailable" };
|
|
2426
|
+
}
|
|
2427
|
+
if (correctionWork) {
|
|
2428
|
+
let hookQualified = false;
|
|
2429
|
+
try {
|
|
2430
|
+
if (seams.verifyInstalledCorrectionHook) {
|
|
2431
|
+
hookQualified = await seams.verifyInstalledCorrectionHook(worktreePath, correctionHookBinPath);
|
|
2432
|
+
}
|
|
2433
|
+
else {
|
|
2434
|
+
const settings = await deps.readFile(`${worktreePath}/.claude/settings.local.json`);
|
|
2435
|
+
hookQualified = settings.includes(correctionHookBinPath) &&
|
|
2436
|
+
settings.includes("--correction-intent-version 1") &&
|
|
2437
|
+
probeInstalledCorrectionHook(worktreePath, correctionHookBinPath);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
catch {
|
|
2441
|
+
hookQualified = false;
|
|
2442
|
+
}
|
|
2443
|
+
if (!hookQualified) {
|
|
2444
|
+
await httpClient.fail(job, {
|
|
2445
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
2446
|
+
error_message: "correction_contract_invalid",
|
|
2447
|
+
classification: "crashed",
|
|
2448
|
+
});
|
|
2449
|
+
return { status: "failed", reason: "correction_hook_unverified" };
|
|
2450
|
+
}
|
|
2141
2451
|
}
|
|
2142
2452
|
// --- MCP registration + trust pre-approval (fail-open, BAPI-724) -----
|
|
2143
2453
|
// Unconditional for every job type that reaches this shared spawn path
|
|
@@ -2187,28 +2497,45 @@ async function runPreparedSpawn(params) {
|
|
|
2187
2497
|
}
|
|
2188
2498
|
const mcpContract = registration.contract;
|
|
2189
2499
|
// --- Worker log tee + watch registry + viewer tab (BAPI-535 §8) ------
|
|
2500
|
+
// BAPI-1330: ONE attempt identity — the server's job id and attempt number
|
|
2501
|
+
// plus a claim id minted here — names this attempt's log file, its registry
|
|
2502
|
+
// entry, and its active-log key. It is allocated BEFORE the tee exists, the
|
|
2503
|
+
// file is created exclusively, and every terminal branch below finishes and
|
|
2504
|
+
// deactivates exactly this identity, so no later claim of the same job (a
|
|
2505
|
+
// retry, a reclaim, a reused worktree) can overwrite or be confused with it.
|
|
2190
2506
|
const registryDeps = buildJobLogRegistryDeps(deps);
|
|
2191
2507
|
const markFinished = seams.markExecutorJobLogFinished ?? markExecutorJobLogFinished;
|
|
2192
2508
|
const createTee = seams.createWorkerLogTee ?? createWorkerLogTee;
|
|
2193
|
-
let
|
|
2509
|
+
let logClaimId = null;
|
|
2194
2510
|
try {
|
|
2195
|
-
|
|
2196
|
-
mkdir: deps.mkdir,
|
|
2197
|
-
writeFile: deps.writeFile,
|
|
2198
|
-
appendFile: deps.appendFile,
|
|
2199
|
-
platform: deps.platform,
|
|
2200
|
-
// BAPI-1021 (AC-6): the archive seams. All optional on `ExecutorDeps`
|
|
2201
|
-
// already, so this is source-compatible with every existing deps
|
|
2202
|
-
// builder — archival simply does not run when either is absent.
|
|
2203
|
-
rename: deps.rename,
|
|
2204
|
-
stat: deps.stat,
|
|
2205
|
-
now: deps.now,
|
|
2206
|
-
errorLog: deps.errorLog,
|
|
2207
|
-
});
|
|
2511
|
+
logClaimId = (seams.createWorkerLogClaimId ?? createWorkerLogClaimId)();
|
|
2208
2512
|
}
|
|
2209
2513
|
catch {
|
|
2210
|
-
|
|
2514
|
+
logClaimId = null;
|
|
2211
2515
|
}
|
|
2516
|
+
let tee = null;
|
|
2517
|
+
if (logClaimId !== null) {
|
|
2518
|
+
try {
|
|
2519
|
+
tee = await createTee(worktreePath, {
|
|
2520
|
+
mkdir: deps.mkdir,
|
|
2521
|
+
writeFile: deps.writeFile,
|
|
2522
|
+
appendFile: deps.appendFile,
|
|
2523
|
+
writeFileExclusive: deps.writeFileExclusive,
|
|
2524
|
+
stat: deps.stat,
|
|
2525
|
+
platform: deps.platform,
|
|
2526
|
+
errorLog: deps.errorLog,
|
|
2527
|
+
}, { jobId: job.id, attempt: job.attempt_count, claimId: logClaimId });
|
|
2528
|
+
}
|
|
2529
|
+
catch {
|
|
2530
|
+
tee = null;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (tee === null) {
|
|
2534
|
+
// A logging failure never changes the worker's disposition; it only means
|
|
2535
|
+
// this attempt has no transcript, which is said once, without detail.
|
|
2536
|
+
deps.errorLog(`worker log unavailable for job ${job.id} (allocation_failed); the attempt continues.`);
|
|
2537
|
+
}
|
|
2538
|
+
const teeClaimId = tee?.identity.claimId;
|
|
2212
2539
|
// BAPI-828 — idempotent worker-log closure, defined the moment a tee could
|
|
2213
2540
|
// exist. Every terminal branch below calls this instead of `closeWorkerLogTee`
|
|
2214
2541
|
// directly, and `runSpawnJob`'s `finally` calls it once more as a backstop for
|
|
@@ -2221,8 +2548,10 @@ async function runPreparedSpawn(params) {
|
|
|
2221
2548
|
return;
|
|
2222
2549
|
workerLogClosed = true;
|
|
2223
2550
|
// Deactivate FIRST: once the tee is closing, an append from the suspend
|
|
2224
|
-
// diagnostic would be writing to a log nobody is reading any more.
|
|
2225
|
-
|
|
2551
|
+
// diagnostic would be writing to a log nobody is reading any more. Only
|
|
2552
|
+
// THIS attempt's entry is removed (BAPI-1330).
|
|
2553
|
+
if (teeClaimId !== undefined)
|
|
2554
|
+
deactivateJobLog(job.id, teeClaimId);
|
|
2226
2555
|
await closeWorkerLogTee(tee);
|
|
2227
2556
|
};
|
|
2228
2557
|
lifecycle.closeWorkerLog = closeWorkerLog;
|
|
@@ -2233,7 +2562,9 @@ async function runPreparedSpawn(params) {
|
|
|
2233
2562
|
// visibility nicety for a separate `executor watch` process), and chaining
|
|
2234
2563
|
// the active marker to it would mean a failed registry write silently
|
|
2235
2564
|
// disables suspend annotation for a worker whose tee is perfectly healthy.
|
|
2236
|
-
|
|
2565
|
+
// BAPI-1330: an attempt whose file could not be allocated is never active.
|
|
2566
|
+
if (tee.available)
|
|
2567
|
+
activateJobLog(job.id, tee.logPath, tee.identity.claimId);
|
|
2237
2568
|
const registerLog = seams.registerExecutorJobLog ?? registerExecutorJobLog;
|
|
2238
2569
|
try {
|
|
2239
2570
|
await registerLog({
|
|
@@ -2244,12 +2575,17 @@ async function runPreparedSpawn(params) {
|
|
|
2244
2575
|
worktree_path: worktreePath,
|
|
2245
2576
|
log_path: tee.logPath,
|
|
2246
2577
|
started_at: nowIso(deps),
|
|
2578
|
+
attempt: tee.identity.attempt,
|
|
2579
|
+
claim_id: tee.identity.claimId,
|
|
2580
|
+
availability: tee.available ? "available" : "unavailable",
|
|
2581
|
+
...(tee.unavailableReason ? { unavailable_reason: tee.unavailableReason } : {}),
|
|
2247
2582
|
}, registryDeps);
|
|
2248
2583
|
}
|
|
2249
2584
|
catch {
|
|
2250
2585
|
/* registry is a visibility nicety — never fail the job */
|
|
2586
|
+
deps.errorLog(`executor job-log registry: could not record job ${job.id}'s attempt log (non-fatal)`);
|
|
2251
2587
|
}
|
|
2252
|
-
if (executorViewerTabsEnabled(deps.platform, deps.env)) {
|
|
2588
|
+
if (tee.available && executorViewerTabsEnabled(deps.platform, deps.env)) {
|
|
2253
2589
|
const openViewer = seams.openExecutorViewerTab ?? openExecutorViewerTab;
|
|
2254
2590
|
try {
|
|
2255
2591
|
const viewer = await openViewer({
|
|
@@ -2273,11 +2609,21 @@ async function runPreparedSpawn(params) {
|
|
|
2273
2609
|
const finalizeRegistry = async () => {
|
|
2274
2610
|
if (!tee)
|
|
2275
2611
|
return;
|
|
2612
|
+
const finishing = tee;
|
|
2276
2613
|
try {
|
|
2277
|
-
|
|
2614
|
+
// BAPI-1330: stamps only THIS attempt's entry, and records a transcript
|
|
2615
|
+
// that became incomplete mid-run as unavailable rather than leaving a
|
|
2616
|
+
// reader to trust it.
|
|
2617
|
+
await markFinished(job.id, finishing.identity.claimId, nowIso(deps), registryDeps, finishing.available
|
|
2618
|
+
? {}
|
|
2619
|
+
: {
|
|
2620
|
+
availability: "unavailable",
|
|
2621
|
+
...(finishing.unavailableReason ? { unavailable_reason: finishing.unavailableReason } : {}),
|
|
2622
|
+
});
|
|
2278
2623
|
}
|
|
2279
2624
|
catch {
|
|
2280
2625
|
/* non-fatal */
|
|
2626
|
+
deps.errorLog(`executor job-log registry: could not finish job ${job.id}'s attempt log (non-fatal)`);
|
|
2281
2627
|
}
|
|
2282
2628
|
};
|
|
2283
2629
|
// --- Spawn -----------------------------------------------------------
|
|
@@ -2337,6 +2683,7 @@ async function runPreparedSpawn(params) {
|
|
|
2337
2683
|
// denies every `git push` the worker attempts — the worker has no remote
|
|
2338
2684
|
// write authority at all.
|
|
2339
2685
|
...(driver?.omitWorkerBranch ? {} : { workerBranch: branch }),
|
|
2686
|
+
...(correctionWork ? { correctionIntentVersion: 1 } : {}),
|
|
2340
2687
|
...(driverStep ? { declaredTouchedFiles: driverStep.declaredTouchedFiles } : {}),
|
|
2341
2688
|
parentEnv: deps.env,
|
|
2342
2689
|
});
|
|
@@ -2488,6 +2835,9 @@ async function runPreparedSpawn(params) {
|
|
|
2488
2835
|
})
|
|
2489
2836
|
: null;
|
|
2490
2837
|
const runtime = probed ?? unrecognizedRuntime(adapter);
|
|
2838
|
+
// Carry the probe identity into any later not-launched report for this job.
|
|
2839
|
+
if (probed !== null)
|
|
2840
|
+
jobTimeCompletionRuntimes.set(ownership, probed);
|
|
2491
2841
|
const refuse = async (reason, message) => {
|
|
2492
2842
|
await closeWorkerLog();
|
|
2493
2843
|
await finalizeRegistry();
|
|
@@ -2536,6 +2886,17 @@ async function runPreparedSpawn(params) {
|
|
|
2536
2886
|
stripEnvFiles: stripWorktreeEnvFiles,
|
|
2537
2887
|
});
|
|
2538
2888
|
}
|
|
2889
|
+
// BAPI-1364: an observe-only ledger for a legacy run of a covered job type. It
|
|
2890
|
+
// never enters `runCoveredAttempt()` and never enables the completion contract;
|
|
2891
|
+
// it only lets a clean exit be checked for required background work still live.
|
|
2892
|
+
const legacyLedger = isCoveredJobType(job.job_type)
|
|
2893
|
+
? createRequiredWorkLedger({
|
|
2894
|
+
binding: readCompletionBinding(job, null).binding,
|
|
2895
|
+
lifecycleSupported: true,
|
|
2896
|
+
optionalWatchers: readOptionalWatchers(job.payload),
|
|
2897
|
+
})
|
|
2898
|
+
: null;
|
|
2899
|
+
const legacyLineFeeder = legacyLedger ? createLedgerLineFeeder(legacyLedger) : null;
|
|
2539
2900
|
let proc;
|
|
2540
2901
|
try {
|
|
2541
2902
|
proc = spawnWorker({ env: spawnShape.env });
|
|
@@ -2600,12 +2961,15 @@ async function runPreparedSpawn(params) {
|
|
|
2600
2961
|
collectTelemetry,
|
|
2601
2962
|
collectRemoteMarker,
|
|
2602
2963
|
mcpSurface,
|
|
2964
|
+
...(legacyLineFeeder ? { onStdoutObserved: legacyLineFeeder.feed } : {}),
|
|
2603
2965
|
// The SAME controller the registry holds, so an executor shutdown, a timeout,
|
|
2604
2966
|
// and an MCP containment refusal all converge on one `SIGTERM` and one grace
|
|
2605
2967
|
// timer for this child (BAPI-828).
|
|
2606
2968
|
terminationController,
|
|
2607
2969
|
control,
|
|
2608
2970
|
});
|
|
2971
|
+
// Flush a trailing partial stdout line so the observe-only ledger sees it.
|
|
2972
|
+
legacyLineFeeder?.flush();
|
|
2609
2973
|
// --- Adapter lifecycle (EXPLICIT, BAPI-781) ---------------------------
|
|
2610
2974
|
// Consumed rather than assumed. Claude declares `{ kind: "none" }` and so
|
|
2611
2975
|
// nothing runs here — which is correct, because BAPI-790 deleted the per-job
|
|
@@ -2835,6 +3199,32 @@ async function runPreparedSpawn(params) {
|
|
|
2835
3199
|
await finalizeRegistry();
|
|
2836
3200
|
return terminalToRunResult(terminal, "completed");
|
|
2837
3201
|
}
|
|
3202
|
+
// BAPI-1364: a legacy worker that ended its (headless) session while the
|
|
3203
|
+
// structured stream still showed a required background command with no
|
|
3204
|
+
// terminal notification can never receive that command's result. Report it
|
|
3205
|
+
// before generic success, the ci_fix artifact merge, or finalization can hide
|
|
3206
|
+
// it. Only the fixed kind and message are sent: no event text, command, path,
|
|
3207
|
+
// or output. A matching terminal task_notification leaves nothing pending.
|
|
3208
|
+
if (legacyLedger?.observeLiveRequiredWork() === "live_required_work") {
|
|
3209
|
+
deps.errorLog(`[executor] job ${job.id}: worker exited cleanly with required background work still live`);
|
|
3210
|
+
await finalizeRegistry();
|
|
3211
|
+
const terminal = await sendTerminalMutationWithAuthReport({
|
|
3212
|
+
kind: "fail",
|
|
3213
|
+
send: () => httpClient.fail(job, {
|
|
3214
|
+
error_kind: WorkerEndedWithPendingBackgroundWork,
|
|
3215
|
+
error_message: WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE,
|
|
3216
|
+
classification: "crashed",
|
|
3217
|
+
...observedExitCode(procResult.exitCode),
|
|
3218
|
+
...commitResidue(git),
|
|
3219
|
+
telemetry: observation.snapshot(),
|
|
3220
|
+
}),
|
|
3221
|
+
deps,
|
|
3222
|
+
options,
|
|
3223
|
+
ownership,
|
|
3224
|
+
log: deps.log,
|
|
3225
|
+
}, job, control);
|
|
3226
|
+
return terminalToRunResult(terminal, "failed");
|
|
3227
|
+
}
|
|
2838
3228
|
// Non-verdict spawn jobs (`implement`, `resume`, and the recovery jobs
|
|
2839
3229
|
// `remediate`/`ci_fix`/`rebase`) use the generic success-result construction;
|
|
2840
3230
|
// the verdict-artifact path above stays limited to verdict jobs like
|
|
@@ -2902,6 +3292,9 @@ async function runPreparedSpawn(params) {
|
|
|
2902
3292
|
kind: "fail",
|
|
2903
3293
|
send: () => httpClient.fail(job, {
|
|
2904
3294
|
...finalization.failure,
|
|
3295
|
+
// BAPI-1330: the process's observed exit rides beside the
|
|
3296
|
+
// finalization failure; it is never rewritten to look like a crash.
|
|
3297
|
+
...observedExitCode(procResult.exitCode),
|
|
2905
3298
|
...commitResidue(git),
|
|
2906
3299
|
telemetry: observation.snapshot(),
|
|
2907
3300
|
}),
|
|
@@ -3016,6 +3409,7 @@ async function runPreparedSpawn(params) {
|
|
|
3016
3409
|
? CLAUDE_NOT_AUTHENTICATED_MESSAGE
|
|
3017
3410
|
: `worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,
|
|
3018
3411
|
classification: toWireClassification(procResult.classification),
|
|
3412
|
+
...observedExitCode(procResult.exitCode),
|
|
3019
3413
|
...commitResidue(git),
|
|
3020
3414
|
telemetry: observation.snapshot(),
|
|
3021
3415
|
...(capacityEvidence !== undefined ? { capacity_evidence: capacityEvidence } : {}),
|
|
@@ -3037,6 +3431,16 @@ const INITIAL_PROMPT_WRITE_BOUND_MS = 30_000;
|
|
|
3037
3431
|
/** Bound on closing the held stdin once the closure rule fires. */
|
|
3038
3432
|
const STDIN_CLOSE_BOUND_MS = 5_000;
|
|
3039
3433
|
const SUPPORTED_COMPLETION_MARKER = COMPLETION_CONTRACT_MARKER_VALUES[0];
|
|
3434
|
+
/**
|
|
3435
|
+
* BAPI-1330 — the worker's observed exit code as a `/fail` field. Omitted (never
|
|
3436
|
+
* null, never invented) when the process ended without a usable code, e.g. on a
|
|
3437
|
+
* signal. Only ever read from the process result, never from worker output.
|
|
3438
|
+
*/
|
|
3439
|
+
function observedExitCode(exitCode) {
|
|
3440
|
+
return typeof exitCode === "number" && Number.isInteger(exitCode) && exitCode >= 0
|
|
3441
|
+
? { exit_code: exitCode }
|
|
3442
|
+
: {};
|
|
3443
|
+
}
|
|
3040
3444
|
/**
|
|
3041
3445
|
* Whether this claim runs under the covered completion contract.
|
|
3042
3446
|
*
|
|
@@ -3048,22 +3452,25 @@ const SUPPORTED_COMPLETION_MARKER = COMPLETION_CONTRACT_MARKER_VALUES[0];
|
|
|
3048
3452
|
function claimIsCovered(job) {
|
|
3049
3453
|
return claimRequiresCompletionEvidence(job.required_capabilities);
|
|
3050
3454
|
}
|
|
3051
|
-
/**
|
|
3052
|
-
|
|
3053
|
-
* "not launched" envelope (BAPI-1301). Every other call passes straight through.
|
|
3054
|
-
*/
|
|
3055
|
-
function withNotLaunchedEnvelopeFallback(httpClient, job) {
|
|
3455
|
+
/** Exported for the covered-job runtime tests; not part of the executor's public surface. */
|
|
3456
|
+
export function withNotLaunchedEnvelopeFallback(httpClient, job, runtimeSources) {
|
|
3056
3457
|
const wrapped = Object.create(httpClient);
|
|
3057
3458
|
wrapped.fail = (claimed, failure) => {
|
|
3058
3459
|
if (failure.completion_evidence !== undefined)
|
|
3059
3460
|
return httpClient.fail(claimed, failure);
|
|
3060
3461
|
const { binding } = readCompletionBinding(job, typeof job.expected_head_sha === "string" ? job.expected_head_sha : null);
|
|
3061
3462
|
const unlaunched = unlaunchedVerification(readRequiredVerification(job.payload));
|
|
3463
|
+
// The runtime actually probed, never the measured baseline: the job-time probe
|
|
3464
|
+
// when it ran, then the qualified preflight runtime, then an unrecognized one.
|
|
3465
|
+
// A runtime that does not satisfy the predicate makes the report a truthful
|
|
3466
|
+
// refusal, so the server accepts it and still never reads it as success.
|
|
3467
|
+
const runtime = runtimeSources.jobTimeRuntime() ?? runtimeSources.preflightRuntime ?? unrecognizedRuntimeForAgent(runtimeSources.agentId);
|
|
3062
3468
|
const envelope = buildNotLaunchedEnvelope({
|
|
3063
3469
|
binding,
|
|
3064
|
-
runtime: { ...
|
|
3470
|
+
runtime: { ...runtime },
|
|
3065
3471
|
verification: unlaunched.verification,
|
|
3066
3472
|
residuals: CLEANUP_RESIDUALS_NOT_COVERED,
|
|
3473
|
+
unsupportedRuntime: !runtimeMatchesSupported(runtime),
|
|
3067
3474
|
...(unlaunched.facts ? { verificationFacts: unlaunched.facts } : {}),
|
|
3068
3475
|
});
|
|
3069
3476
|
return httpClient.fail(claimed, {
|
|
@@ -3074,6 +3481,12 @@ function withNotLaunchedEnvelopeFallback(httpClient, job) {
|
|
|
3074
3481
|
};
|
|
3075
3482
|
return wrapped;
|
|
3076
3483
|
}
|
|
3484
|
+
/**
|
|
3485
|
+
* The covered-job runtime probe's identity per job, recorded before launch so a
|
|
3486
|
+
* later not-launched report stamps what was actually probed (BAPI-1386). Keyed by
|
|
3487
|
+
* the job's ownership state, which both the wrapper and the spawn path share.
|
|
3488
|
+
*/
|
|
3489
|
+
const jobTimeCompletionRuntimes = new WeakMap();
|
|
3077
3490
|
/**
|
|
3078
3491
|
* The verification a covered report carries when no declared check ran
|
|
3079
3492
|
* (BAPI-1302): before launch, after a worker failure, or on a stop.
|
|
@@ -3120,8 +3533,11 @@ function declaredOrderFacts(checks, runFacts) {
|
|
|
3120
3533
|
}
|
|
3121
3534
|
/** An identity that cannot match the supported runtime, for truthful refusal envelopes. */
|
|
3122
3535
|
function unrecognizedRuntime(adapter) {
|
|
3536
|
+
return unrecognizedRuntimeForAgent(adapter.identity.agentId);
|
|
3537
|
+
}
|
|
3538
|
+
function unrecognizedRuntimeForAgent(agentId) {
|
|
3123
3539
|
return {
|
|
3124
|
-
adapter:
|
|
3540
|
+
adapter: agentId,
|
|
3125
3541
|
cli_version: "unrecognized",
|
|
3126
3542
|
platform: "unrecognized",
|
|
3127
3543
|
event_schema: "unrecognized",
|
|
@@ -3361,33 +3777,9 @@ async function runCoveredAttempt(params) {
|
|
|
3361
3777
|
buildNudge: params.nudge,
|
|
3362
3778
|
deadlineAtMs: plan.holdDeadlineAtMs,
|
|
3363
3779
|
});
|
|
3364
|
-
// The authoritative consumer: whole lines into the ledger, bounded.
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
let lineBuffer = "";
|
|
3368
|
-
let discardingOversize = false;
|
|
3369
|
-
const authoritativeStdout = (chunk) => {
|
|
3370
|
-
lineBuffer += chunk;
|
|
3371
|
-
let newline;
|
|
3372
|
-
while ((newline = lineBuffer.indexOf("\n")) >= 0) {
|
|
3373
|
-
const line = lineBuffer.slice(0, newline);
|
|
3374
|
-
lineBuffer = lineBuffer.slice(newline + 1);
|
|
3375
|
-
if (discardingOversize) {
|
|
3376
|
-
discardingOversize = false;
|
|
3377
|
-
continue;
|
|
3378
|
-
}
|
|
3379
|
-
ledger.ingestLine(line);
|
|
3380
|
-
supervisor.onActivity();
|
|
3381
|
-
}
|
|
3382
|
-
if (!discardingOversize && lineBuffer.length > MAX_EVENT_BYTES) {
|
|
3383
|
-
ledger.ingestLine(lineBuffer);
|
|
3384
|
-
lineBuffer = "";
|
|
3385
|
-
discardingOversize = true;
|
|
3386
|
-
}
|
|
3387
|
-
else if (discardingOversize) {
|
|
3388
|
-
lineBuffer = "";
|
|
3389
|
-
}
|
|
3390
|
-
};
|
|
3780
|
+
// The authoritative consumer: whole lines into the ledger, bounded.
|
|
3781
|
+
const lineFeeder = createLedgerLineFeeder(ledger, () => supervisor.onActivity());
|
|
3782
|
+
const authoritativeStdout = lineFeeder.feed;
|
|
3391
3783
|
const runPromise = runProcessWithTimeout(supervisedProc, Math.max(1, Math.ceil(plan.workerWindowMs / 1000)), deps, {
|
|
3392
3784
|
termGraceMs: jobOptions.termGraceMs,
|
|
3393
3785
|
terminationController,
|
|
@@ -3419,8 +3811,7 @@ async function runCoveredAttempt(params) {
|
|
|
3419
3811
|
if (sampleTimer !== undefined)
|
|
3420
3812
|
deps.clearTimer(sampleTimer);
|
|
3421
3813
|
supervisor.dispose();
|
|
3422
|
-
|
|
3423
|
-
ledger.ingestLine(lineBuffer);
|
|
3814
|
+
lineFeeder.flush();
|
|
3424
3815
|
ledger.recordStreamEnd(procResult.authoritativeStreamError || procResult.exitUnconfirmed
|
|
3425
3816
|
? "stream_error"
|
|
3426
3817
|
: (procResult.stdoutEnd ?? "eof"));
|
|
@@ -3604,6 +3995,7 @@ async function runCoveredAttempt(params) {
|
|
|
3604
3995
|
? CLAUDE_NOT_AUTHENTICATED_MESSAGE
|
|
3605
3996
|
: `worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,
|
|
3606
3997
|
classification: toWireClassification(procResult.classification),
|
|
3998
|
+
...observedExitCode(procResult.exitCode),
|
|
3607
3999
|
}, envelope, capacityEvidence !== undefined ? { capacity_evidence: capacityEvidence } : {}));
|
|
3608
4000
|
}
|
|
3609
4001
|
// --- Phase 3: independent declared checks (BAPI-1302, D4c) ---------------------
|
|
@@ -3734,6 +4126,9 @@ async function runCoveredAttempt(params) {
|
|
|
3734
4126
|
runCommand: deps.runCommand,
|
|
3735
4127
|
headSha: git.last_commit_sha,
|
|
3736
4128
|
expectedBaseBranch: effectiveBaseBranch,
|
|
4129
|
+
// BAPI-1330: this host only runs claims the SERVER stamped as covered, so
|
|
4130
|
+
// the existence-only legacy fallback is refused here.
|
|
4131
|
+
coveredAttempt: true,
|
|
3737
4132
|
});
|
|
3738
4133
|
const facts = finalizationFacts(finalization.ok ? { ok: true } : { ok: false, errorKind: finalization.failure.error_kind });
|
|
3739
4134
|
const envelope = envelopeWith({
|
|
@@ -3775,19 +4170,35 @@ async function runCoveredAttempt(params) {
|
|
|
3775
4170
|
// Not a success. A completion cause (required work, cleanup, verification,
|
|
3776
4171
|
// permission) outranks the finalization kind; with none, finalization keeps
|
|
3777
4172
|
// its existing error kind and recovery lane.
|
|
4173
|
+
//
|
|
4174
|
+
// BAPI-1330: every branch below is reached only after the worker exited
|
|
4175
|
+
// cleanly. A completion refusal reports that observed process fact —
|
|
4176
|
+
// `clean_exit` and the real exit code — beside the refusal (the server decides
|
|
4177
|
+
// it from the envelope's completion cause, never the classification). A
|
|
4178
|
+
// finalization failure adds the exit code to its own kind and classification.
|
|
4179
|
+
const observedExit = {
|
|
4180
|
+
classification: toWireClassification(procResult.classification),
|
|
4181
|
+
...observedExitCode(procResult.exitCode),
|
|
4182
|
+
};
|
|
3778
4183
|
const cause = envelope.cause;
|
|
3779
4184
|
if (cause) {
|
|
3780
4185
|
return finish(await failWith({
|
|
3781
4186
|
error_kind: COMPLETION_CAUSE_ERROR_KINDS[cause],
|
|
3782
4187
|
error_message: `covered attempt did not prove completion (${successRefusalReason(envelope) ?? cause})`,
|
|
3783
|
-
|
|
4188
|
+
...observedExit,
|
|
3784
4189
|
}, envelope));
|
|
3785
4190
|
}
|
|
3786
|
-
if (!finalization.ok)
|
|
3787
|
-
|
|
4191
|
+
if (!finalization.ok) {
|
|
4192
|
+
// The finalization failure keeps its OWN classification, exactly as the
|
|
4193
|
+
// legacy path does: the kinds that mean "the process ended cleanly"
|
|
4194
|
+
// (`PrNotAttached`, `PublicationUnverified`) already say `clean_exit`, and
|
|
4195
|
+
// the rest keep the classification their existing recovery routing reads.
|
|
4196
|
+
// Only the observed exit code is added.
|
|
4197
|
+
return finish(await failWith({ ...finalization.failure, ...observedExitCode(procResult.exitCode) }, envelope));
|
|
4198
|
+
}
|
|
3788
4199
|
return finish(await failWith({
|
|
3789
4200
|
error_kind: COMPLETION_CAUSE_ERROR_KINDS.required_work_incomplete,
|
|
3790
4201
|
error_message: `covered attempt did not prove completion (${successRefusalReason(envelope) ?? "unknown"})`,
|
|
3791
|
-
|
|
4202
|
+
...observedExit,
|
|
3792
4203
|
}, envelope));
|
|
3793
4204
|
}
|