@bridge_gpt/mcp-server 0.2.66 → 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 +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- 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 +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -3
- package/build/conductor/repair-contract.js +54 -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/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- 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 +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- 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/tool-wrapper.js +39 -43
- 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 +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* inside `build/pipelines.generated.js`. `mcp_server/src/` does not exist there,
|
|
7
7
|
* so an instruction that told the agent to "call `validateConductorBundleInputs`"
|
|
8
8
|
* would be referencing a path only this repository has. This subcommand is the
|
|
9
|
-
* reachable surface,
|
|
10
|
-
*
|
|
9
|
+
* reachable surface, the same way `setup-epic` is a command rather than
|
|
10
|
+
* hand-rolled HTTP calls.
|
|
11
11
|
*
|
|
12
12
|
* It holds no policy of its own: it parses argv, delegates to
|
|
13
13
|
* `conductor-bundle-artifacts.ts`, and prints a JSON envelope. All I/O is behind
|
package/build/executor/cli.js
CHANGED
|
@@ -37,9 +37,10 @@ const DEFAULT_JOB_TIMEOUT_SECONDS = 20 * 60;
|
|
|
37
37
|
export const DEFAULT_MAX_CONCURRENT = 1;
|
|
38
38
|
export function getExecutorUsage() {
|
|
39
39
|
return [
|
|
40
|
-
"Usage: mcp-server executor --repo <name> [--repo <name> ...]
|
|
40
|
+
"Usage: mcp-server executor --repo <name> [--repo <name> ...]",
|
|
41
|
+
" (--epic-run-id <id> | --repo-wide) [options]",
|
|
41
42
|
"",
|
|
42
|
-
"Runs the Epic
|
|
43
|
+
"Runs the Epic Implementer local executor: poll → claim → spawn → heartbeat.",
|
|
43
44
|
"",
|
|
44
45
|
"Options:",
|
|
45
46
|
" --repo <name> Repo to serve (repeatable).",
|
|
@@ -53,6 +53,37 @@ export const SUPPORTED_COMPLETION_RUNTIME = {
|
|
|
53
53
|
platform: "darwin-arm64",
|
|
54
54
|
event_schema: "claude-stream-json.lifecycle.v1",
|
|
55
55
|
};
|
|
56
|
+
/** Oldest Claude CLI version whose lifecycle events were measured. */
|
|
57
|
+
export const MIN_CLI_VERSION = "2.1.282";
|
|
58
|
+
/** The four platform ids `<process.platform>-<process.arch>` the executor can produce. */
|
|
59
|
+
export const SUPPORTED_PLATFORMS = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
|
|
60
|
+
/** Names the lifecycle parser version. A constant, never a probed value. */
|
|
61
|
+
export const EVENT_SCHEMA = "claude-stream-json.lifecycle.v1";
|
|
62
|
+
const CLI_VERSION_RE = /^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,5})(?: \(Claude Code\))?$/;
|
|
63
|
+
/**
|
|
64
|
+
* Parse `major.minor.patch` with an optional exact ` (Claude Code)` suffix into its
|
|
65
|
+
* numeric triple. Prereleases, whitespace, non-numeric segments, leading zeros, and
|
|
66
|
+
* non-strings are `null`. Mirror of `normalize_cli_version` in the Python contract.
|
|
67
|
+
*/
|
|
68
|
+
export function parseCliVersion(value) {
|
|
69
|
+
if (typeof value !== "string")
|
|
70
|
+
return null;
|
|
71
|
+
const match = CLI_VERSION_RE.exec(value);
|
|
72
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
73
|
+
}
|
|
74
|
+
/** The normalized `major.minor.patch` text, or `null` when the version is not strict. */
|
|
75
|
+
export function normalizeCliVersion(value) {
|
|
76
|
+
const parsed = parseCliVersion(value);
|
|
77
|
+
return parsed ? parsed.join(".") : null;
|
|
78
|
+
}
|
|
79
|
+
/** Is version triple `a` at or above `b`? */
|
|
80
|
+
export function cliVersionAtLeast(a, b) {
|
|
81
|
+
for (let i = 0; i < 3; i += 1) {
|
|
82
|
+
if (a[i] !== b[i])
|
|
83
|
+
return a[i] > b[i];
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
56
87
|
export const EVIDENCE_VALUES = ["sufficient", "insufficient"];
|
|
57
88
|
export const INSUFFICIENT_REASON_VALUES = [
|
|
58
89
|
"no_init_event",
|
|
@@ -239,13 +270,23 @@ export function isCoveredJobType(jobType) {
|
|
|
239
270
|
export function claimRequiresCompletionEvidence(requiredCapabilities) {
|
|
240
271
|
return Array.isArray(requiredCapabilities) && requiredCapabilities.includes(COMPLETION_EVIDENCE_CAPABILITY);
|
|
241
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* True for the measured baseline or a newer supported runtime: adapter `claude`, the
|
|
275
|
+
* lifecycle event schema, a closed platform id, and a strict numeric version at or
|
|
276
|
+
* above {@link MIN_CLI_VERSION}. Mirror of `runtime_matches_supported` in Python.
|
|
277
|
+
*/
|
|
242
278
|
export function runtimeMatchesSupported(runtime) {
|
|
243
279
|
if (!runtime)
|
|
244
280
|
return false;
|
|
281
|
+
const version = parseCliVersion(runtime.cli_version);
|
|
282
|
+
const minimum = parseCliVersion(MIN_CLI_VERSION);
|
|
245
283
|
return (runtime.adapter === SUPPORTED_COMPLETION_RUNTIME.adapter &&
|
|
246
|
-
runtime.
|
|
247
|
-
runtime.platform ===
|
|
248
|
-
runtime.
|
|
284
|
+
runtime.event_schema === EVENT_SCHEMA &&
|
|
285
|
+
typeof runtime.platform === "string" &&
|
|
286
|
+
SUPPORTED_PLATFORMS.includes(runtime.platform) &&
|
|
287
|
+
version !== null &&
|
|
288
|
+
minimum !== null &&
|
|
289
|
+
cliVersionAtLeast(version, minimum));
|
|
249
290
|
}
|
|
250
291
|
export function unresolvedRequiredTasks(envelope) {
|
|
251
292
|
return envelope.tasks.filter((t) => t.required && !TASK_RESOLVING_STATES.has(t.state));
|
|
@@ -366,7 +407,9 @@ export function buildNotLaunchedEnvelope(input) {
|
|
|
366
407
|
binding: { ...input.binding },
|
|
367
408
|
session_id: null,
|
|
368
409
|
evidence: "insufficient",
|
|
369
|
-
insufficient_reasons:
|
|
410
|
+
insufficient_reasons: input.unsupportedRuntime
|
|
411
|
+
? ["worker_not_launched", "unsupported_runtime"]
|
|
412
|
+
: ["worker_not_launched"],
|
|
370
413
|
execution: "unknown",
|
|
371
414
|
verification: input.verification,
|
|
372
415
|
finalization: "unknown",
|
|
@@ -381,7 +424,7 @@ export function buildNotLaunchedEnvelope(input) {
|
|
|
381
424
|
denials: [],
|
|
382
425
|
missing_capabilities: [],
|
|
383
426
|
results_seen: 0,
|
|
384
|
-
cause: null,
|
|
427
|
+
cause: input.unsupportedRuntime ? "unsupported_completion_evidence" : null,
|
|
385
428
|
...(input.verificationFacts ? { verification_facts: input.verificationFacts } : {}),
|
|
386
429
|
};
|
|
387
430
|
}
|
|
@@ -228,6 +228,10 @@ export function finalizationFacts(outcome) {
|
|
|
228
228
|
// and neither push nor PR is inferred satisfied.
|
|
229
229
|
case "WorkerFinalizationPublicationUnverified":
|
|
230
230
|
return { verdict: "unknown", pushSatisfied: false, prSatisfied: false };
|
|
231
|
+
// BAPI-1357: the origin lookup could not be trusted, so the branch was not
|
|
232
|
+
// observed missing either. Unknown, and neither obligation is inferred met.
|
|
233
|
+
case "WorkerFinalizationOriginUnverifiable":
|
|
234
|
+
return { verdict: "unknown", pushSatisfied: false, prSatisfied: false };
|
|
231
235
|
default:
|
|
232
236
|
return { verdict: "unknown", pushSatisfied: false, prSatisfied: false };
|
|
233
237
|
}
|
package/build/executor/env.js
CHANGED
|
@@ -329,3 +329,22 @@ export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
|
|
|
329
329
|
// per call site.
|
|
330
330
|
return env;
|
|
331
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* The environment for a headless agent run that is not a worker job, such as the
|
|
334
|
+
* executor's deny-enforcement probe (BAPI-1386).
|
|
335
|
+
*
|
|
336
|
+
* The probe must see what a worker sees. Spawned from an executor that is itself
|
|
337
|
+
* running inside a Claude session, it would otherwise inherit that session's
|
|
338
|
+
* nested-session variables, and the CLI can refuse to start a tool call there. So
|
|
339
|
+
* this reuses the worker allowlist and worker-injected defaults behind
|
|
340
|
+
* {@link buildExecutorBaseWorkerEnv} rather than keeping a deny-list of Claude
|
|
341
|
+
* variable names, which would go stale with every CLI release. No job-specific
|
|
342
|
+
* option is set: the result carries no branch, base, scope, or correction marker.
|
|
343
|
+
*
|
|
344
|
+
* Applied only where the executor builds the deny probe's context. The shared
|
|
345
|
+
* `runHeadless()` passes its supplied environment through unchanged, so standalone
|
|
346
|
+
* capability probes are not filtered.
|
|
347
|
+
*/
|
|
348
|
+
export function buildHeadlessAgentEnv(parentEnv) {
|
|
349
|
+
return buildExecutorBaseWorkerEnv(parentEnv);
|
|
350
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* interval. This is TELEMETRY ONLY (R14): the marker is never persisted, never
|
|
23
23
|
* reported as `last_commit_sha`, and never drives an executor decision — the
|
|
24
24
|
* reconciler remains the sole consumer and the sole transition authority
|
|
25
|
-
* (`docs/
|
|
25
|
+
* (`docs/epic-implementer/standing-rules.md`).
|
|
26
26
|
*
|
|
27
27
|
* - SERVER STOP (WS-B executor half). A heartbeat response carrying the
|
|
28
28
|
* server's stop request kills the owned worker like `stale_claim` does, but
|
|
@@ -76,6 +76,22 @@ export function normalizeRequiredCapabilities(value) {
|
|
|
76
76
|
return [];
|
|
77
77
|
return value.every((entry) => typeof entry === "string") ? [...value] : [];
|
|
78
78
|
}
|
|
79
|
+
const FULL_SHA_PATTERN = /^[0-9a-fA-F]{40}$/;
|
|
80
|
+
/**
|
|
81
|
+
* BAPI-1367 — normalize a claim response's `prior_attempt_head_sha`. Absent or
|
|
82
|
+
* explicit null reads as null silently (an older server, or no prior head). A
|
|
83
|
+
* present value that is not a 40-hex string also reads as null, and `onMalformed`
|
|
84
|
+
* is told once so the caller can warn. The raw value is never passed on, so it
|
|
85
|
+
* can never reach a log line.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizePriorAttemptHeadSha(value, onMalformed) {
|
|
88
|
+
if (value === undefined || value === null)
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof value === "string" && FULL_SHA_PATTERN.test(value))
|
|
91
|
+
return value;
|
|
92
|
+
onMalformed();
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
79
95
|
/** Bound a server-provided diagnostic snippet included in error strings. */
|
|
80
96
|
function boundedDetail(text) {
|
|
81
97
|
const trimmed = (text ?? "").trim();
|
|
@@ -271,6 +287,10 @@ export function createExecutorHttpClient(config) {
|
|
|
271
287
|
if ("required_capabilities" in job) {
|
|
272
288
|
job.required_capabilities = normalizeRequiredCapabilities(job.required_capabilities);
|
|
273
289
|
}
|
|
290
|
+
// BAPI-1367: a malformed prior head fails closed to null with one
|
|
291
|
+
// bounded warning naming only the job, never the raw value or a SHA.
|
|
292
|
+
job.prior_attempt_head_sha = normalizePriorAttemptHeadSha(job.prior_attempt_head_sha, () => (config.warn ?? ((message) => console.error(message)))(`executor: ignoring malformed prior attempt head in the claim for job ${String(job.id)}; ` +
|
|
293
|
+
`treating it as a first dispatch.`));
|
|
274
294
|
return { kind: "claimed", job, reconcilerLiveness };
|
|
275
295
|
}
|
|
276
296
|
catch {
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
* work reached origin") because this case has positive proof of recoverable
|
|
42
42
|
* state: an operator can resume/finalize from the existing branch rather than
|
|
43
43
|
* re-running from scratch; see `worker-finalization.ts`.
|
|
44
|
+
* - `WorkerFinalizationOriginUnverifiable` — an implementation-style job
|
|
45
|
+
* (BAPI-1357) exited cleanly, but the origin branch lookup could not be
|
|
46
|
+
* trusted (a throw, an expired bound, an unexpected exit, or unusable output).
|
|
47
|
+
* The branch was NOT observed absent, so this is distinct from
|
|
48
|
+
* `WorkerFinalizationMissingRemoteBranchAndPr`; see `worker-finalization.ts`.
|
|
44
49
|
* - `ContractError.WorkerConfigIsolation` — a per-job isolated Claude config
|
|
45
50
|
* directory could not be established (unsupported platform, unsupported auth
|
|
46
51
|
* layout, unseedable directory, or a missing required MCP registration), so
|
|
@@ -141,6 +146,24 @@ export const WorkerFinalizationSavedButUnfinalized = "WorkerFinalizationSavedBut
|
|
|
141
146
|
* verdict is `unknown`, never `unmet` and never `ok`.
|
|
142
147
|
*/
|
|
143
148
|
export const WorkerFinalizationPublicationUnverified = "WorkerFinalizationPublicationUnverified";
|
|
149
|
+
/**
|
|
150
|
+
* BAPI-1357: the origin branch lookup could not be TRUSTED — the command threw,
|
|
151
|
+
* its call bound expired, it exited with anything other than "found" (0) or "no
|
|
152
|
+
* matching ref" (2), or it exited 0 without a usable SHA for the exact ref. The
|
|
153
|
+
* branch was not observed absent, so this is distinct from
|
|
154
|
+
* `WorkerFinalizationMissingRemoteBranchAndPr`, which is reserved for an
|
|
155
|
+
* observed absence. The process itself exited clean. Nothing command-derived
|
|
156
|
+
* (stderr, error text, host, credentials) is carried by this kind.
|
|
157
|
+
*/
|
|
158
|
+
export const WorkerFinalizationOriginUnverifiable = "WorkerFinalizationOriginUnverifiable";
|
|
159
|
+
/**
|
|
160
|
+
* BAPI-1364: a LEGACY (non-covered) worker exited cleanly while the structured
|
|
161
|
+
* stream still showed a required background command with no terminal
|
|
162
|
+
* notification. The headless session had no later turn, so the command's result
|
|
163
|
+
* could never reach the worker. Carries only {@link WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE};
|
|
164
|
+
* no event text, command, path, or output.
|
|
165
|
+
*/
|
|
166
|
+
export const WorkerEndedWithPendingBackgroundWork = "WorkerEndedWithPendingBackgroundWork";
|
|
144
167
|
export const WorkerConfigIsolation = "ContractError.WorkerConfigIsolation";
|
|
145
168
|
export const PreSpawnVerification = "ContractError.PreSpawnVerification";
|
|
146
169
|
export const BridgeCredentialUnavailable = "ContractError.BridgeCredentialUnavailable";
|
|
@@ -282,6 +305,8 @@ export function formatMcpSurfaceMismatch(expected, observed) {
|
|
|
282
305
|
"Check that the installed Claude CLI supports --strict-mcp-config and that nothing adds MCP " +
|
|
283
306
|
"servers outside the worktree registration.");
|
|
284
307
|
}
|
|
308
|
+
/** The fixed message posted with {@link WorkerEndedWithPendingBackgroundWork}. */
|
|
309
|
+
export const WORKER_ENDED_WITH_PENDING_BACKGROUND_WORK_MESSAGE = "the worker ended its session while a background command was still running";
|
|
285
310
|
/**
|
|
286
311
|
* The fixed message posted with {@link WorkerStartupFatal}.
|
|
287
312
|
*
|
|
@@ -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). */
|
|
@@ -540,6 +540,46 @@ function createNoopProcess(deps, durationMs, message) {
|
|
|
540
540
|
},
|
|
541
541
|
};
|
|
542
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
|
+
}
|
|
543
583
|
/**
|
|
544
584
|
* Run the process and heartbeat loop concurrently; return the process outcome.
|
|
545
585
|
*
|
|
@@ -569,7 +609,10 @@ async function superviseProcess(params) {
|
|
|
569
609
|
...(params.terminationController
|
|
570
610
|
? { terminationController: params.terminationController }
|
|
571
611
|
: {}),
|
|
572
|
-
onStdout: (chunk) =>
|
|
612
|
+
onStdout: (chunk) => {
|
|
613
|
+
params.observation.recordStdout(chunk);
|
|
614
|
+
params.onStdoutObserved?.(chunk);
|
|
615
|
+
},
|
|
573
616
|
...(mcpSurface
|
|
574
617
|
? {
|
|
575
618
|
onStdoutTerminationCheck: (chunk) => {
|
|
@@ -717,7 +760,11 @@ control) {
|
|
|
717
760
|
// any /fail that has no envelope gets the truthful "not launched" envelope and
|
|
718
761
|
// keeps its own error kind; reports that already carry one are untouched.
|
|
719
762
|
if (claimIsCovered(job)) {
|
|
720
|
-
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
|
+
});
|
|
721
768
|
}
|
|
722
769
|
// `merge` is a deterministic executor action: dispatched BEFORE any worktree
|
|
723
770
|
// ensure/recreate or worker-spawn logic — it ensures no worktree and spawns no
|
|
@@ -1545,6 +1592,10 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1545
1592
|
// validated against a closed set — never inferred locally, and never taken
|
|
1546
1593
|
// from worker output (R14 rules 3 and 4).
|
|
1547
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 } : {}),
|
|
1548
1599
|
});
|
|
1549
1600
|
if (!wt.ok) {
|
|
1550
1601
|
await httpClient.fail(job, jobWorktreeFailure(wt));
|
|
@@ -1563,6 +1614,7 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1563
1614
|
branch: wt.branch,
|
|
1564
1615
|
prompt: explicit.prompt,
|
|
1565
1616
|
expectedHeadSha: wt.expectedHeadSha,
|
|
1617
|
+
...(wt.priorWork ? { priorWork: wt.priorWork } : {}),
|
|
1566
1618
|
...(focusedRebase ? { focusedRebase } : {}),
|
|
1567
1619
|
};
|
|
1568
1620
|
}
|
|
@@ -1585,6 +1637,7 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1585
1637
|
branch: wt.branch,
|
|
1586
1638
|
prompt: rendered,
|
|
1587
1639
|
expectedHeadSha: wt.expectedHeadSha,
|
|
1640
|
+
...(wt.priorWork ? { priorWork: wt.priorWork } : {}),
|
|
1588
1641
|
};
|
|
1589
1642
|
}
|
|
1590
1643
|
catch (err) {
|
|
@@ -1599,6 +1652,93 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1599
1652
|
return { ok: false, result: { status: "failed", reason: "prompt_contract" } };
|
|
1600
1653
|
}
|
|
1601
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
|
+
}
|
|
1602
1742
|
/**
|
|
1603
1743
|
* BAPI-1296 — verify a prepared `ci_fix` checkout against its dispatched CI
|
|
1604
1744
|
* context and render the focused prompt, UNDER THE WORKTREE LOCK.
|
|
@@ -1985,6 +2125,20 @@ driver) {
|
|
|
1985
2125
|
// a refused ci_fix leaves the worktree exactly as preparation left it. The
|
|
1986
2126
|
// verified, rendered prompt replaces preparation's empty placeholder.
|
|
1987
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
|
+
}
|
|
1988
2142
|
if (prep.ciFix) {
|
|
1989
2143
|
const rendered = await renderVerifiedCiFixPrompt({
|
|
1990
2144
|
job,
|
|
@@ -2681,6 +2835,9 @@ async function runPreparedSpawn(params) {
|
|
|
2681
2835
|
})
|
|
2682
2836
|
: null;
|
|
2683
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);
|
|
2684
2841
|
const refuse = async (reason, message) => {
|
|
2685
2842
|
await closeWorkerLog();
|
|
2686
2843
|
await finalizeRegistry();
|
|
@@ -2729,6 +2886,17 @@ async function runPreparedSpawn(params) {
|
|
|
2729
2886
|
stripEnvFiles: stripWorktreeEnvFiles,
|
|
2730
2887
|
});
|
|
2731
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;
|
|
2732
2900
|
let proc;
|
|
2733
2901
|
try {
|
|
2734
2902
|
proc = spawnWorker({ env: spawnShape.env });
|
|
@@ -2793,12 +2961,15 @@ async function runPreparedSpawn(params) {
|
|
|
2793
2961
|
collectTelemetry,
|
|
2794
2962
|
collectRemoteMarker,
|
|
2795
2963
|
mcpSurface,
|
|
2964
|
+
...(legacyLineFeeder ? { onStdoutObserved: legacyLineFeeder.feed } : {}),
|
|
2796
2965
|
// The SAME controller the registry holds, so an executor shutdown, a timeout,
|
|
2797
2966
|
// and an MCP containment refusal all converge on one `SIGTERM` and one grace
|
|
2798
2967
|
// timer for this child (BAPI-828).
|
|
2799
2968
|
terminationController,
|
|
2800
2969
|
control,
|
|
2801
2970
|
});
|
|
2971
|
+
// Flush a trailing partial stdout line so the observe-only ledger sees it.
|
|
2972
|
+
legacyLineFeeder?.flush();
|
|
2802
2973
|
// --- Adapter lifecycle (EXPLICIT, BAPI-781) ---------------------------
|
|
2803
2974
|
// Consumed rather than assumed. Claude declares `{ kind: "none" }` and so
|
|
2804
2975
|
// nothing runs here — which is correct, because BAPI-790 deleted the per-job
|
|
@@ -3028,6 +3199,32 @@ async function runPreparedSpawn(params) {
|
|
|
3028
3199
|
await finalizeRegistry();
|
|
3029
3200
|
return terminalToRunResult(terminal, "completed");
|
|
3030
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
|
+
}
|
|
3031
3228
|
// Non-verdict spawn jobs (`implement`, `resume`, and the recovery jobs
|
|
3032
3229
|
// `remediate`/`ci_fix`/`rebase`) use the generic success-result construction;
|
|
3033
3230
|
// the verdict-artifact path above stays limited to verdict jobs like
|
|
@@ -3255,22 +3452,25 @@ function observedExitCode(exitCode) {
|
|
|
3255
3452
|
function claimIsCovered(job) {
|
|
3256
3453
|
return claimRequiresCompletionEvidence(job.required_capabilities);
|
|
3257
3454
|
}
|
|
3258
|
-
/**
|
|
3259
|
-
|
|
3260
|
-
* "not launched" envelope (BAPI-1301). Every other call passes straight through.
|
|
3261
|
-
*/
|
|
3262
|
-
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) {
|
|
3263
3457
|
const wrapped = Object.create(httpClient);
|
|
3264
3458
|
wrapped.fail = (claimed, failure) => {
|
|
3265
3459
|
if (failure.completion_evidence !== undefined)
|
|
3266
3460
|
return httpClient.fail(claimed, failure);
|
|
3267
3461
|
const { binding } = readCompletionBinding(job, typeof job.expected_head_sha === "string" ? job.expected_head_sha : null);
|
|
3268
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);
|
|
3269
3468
|
const envelope = buildNotLaunchedEnvelope({
|
|
3270
3469
|
binding,
|
|
3271
|
-
runtime: { ...
|
|
3470
|
+
runtime: { ...runtime },
|
|
3272
3471
|
verification: unlaunched.verification,
|
|
3273
3472
|
residuals: CLEANUP_RESIDUALS_NOT_COVERED,
|
|
3473
|
+
unsupportedRuntime: !runtimeMatchesSupported(runtime),
|
|
3274
3474
|
...(unlaunched.facts ? { verificationFacts: unlaunched.facts } : {}),
|
|
3275
3475
|
});
|
|
3276
3476
|
return httpClient.fail(claimed, {
|
|
@@ -3281,6 +3481,12 @@ function withNotLaunchedEnvelopeFallback(httpClient, job) {
|
|
|
3281
3481
|
};
|
|
3282
3482
|
return wrapped;
|
|
3283
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();
|
|
3284
3490
|
/**
|
|
3285
3491
|
* The verification a covered report carries when no declared check ran
|
|
3286
3492
|
* (BAPI-1302): before launch, after a worker failure, or on a stop.
|
|
@@ -3327,8 +3533,11 @@ function declaredOrderFacts(checks, runFacts) {
|
|
|
3327
3533
|
}
|
|
3328
3534
|
/** An identity that cannot match the supported runtime, for truthful refusal envelopes. */
|
|
3329
3535
|
function unrecognizedRuntime(adapter) {
|
|
3536
|
+
return unrecognizedRuntimeForAgent(adapter.identity.agentId);
|
|
3537
|
+
}
|
|
3538
|
+
function unrecognizedRuntimeForAgent(agentId) {
|
|
3330
3539
|
return {
|
|
3331
|
-
adapter:
|
|
3540
|
+
adapter: agentId,
|
|
3332
3541
|
cli_version: "unrecognized",
|
|
3333
3542
|
platform: "unrecognized",
|
|
3334
3543
|
event_schema: "unrecognized",
|
|
@@ -3568,33 +3777,9 @@ async function runCoveredAttempt(params) {
|
|
|
3568
3777
|
buildNudge: params.nudge,
|
|
3569
3778
|
deadlineAtMs: plan.holdDeadlineAtMs,
|
|
3570
3779
|
});
|
|
3571
|
-
// The authoritative consumer: whole lines into the ledger, bounded.
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
let lineBuffer = "";
|
|
3575
|
-
let discardingOversize = false;
|
|
3576
|
-
const authoritativeStdout = (chunk) => {
|
|
3577
|
-
lineBuffer += chunk;
|
|
3578
|
-
let newline;
|
|
3579
|
-
while ((newline = lineBuffer.indexOf("\n")) >= 0) {
|
|
3580
|
-
const line = lineBuffer.slice(0, newline);
|
|
3581
|
-
lineBuffer = lineBuffer.slice(newline + 1);
|
|
3582
|
-
if (discardingOversize) {
|
|
3583
|
-
discardingOversize = false;
|
|
3584
|
-
continue;
|
|
3585
|
-
}
|
|
3586
|
-
ledger.ingestLine(line);
|
|
3587
|
-
supervisor.onActivity();
|
|
3588
|
-
}
|
|
3589
|
-
if (!discardingOversize && lineBuffer.length > MAX_EVENT_BYTES) {
|
|
3590
|
-
ledger.ingestLine(lineBuffer);
|
|
3591
|
-
lineBuffer = "";
|
|
3592
|
-
discardingOversize = true;
|
|
3593
|
-
}
|
|
3594
|
-
else if (discardingOversize) {
|
|
3595
|
-
lineBuffer = "";
|
|
3596
|
-
}
|
|
3597
|
-
};
|
|
3780
|
+
// The authoritative consumer: whole lines into the ledger, bounded.
|
|
3781
|
+
const lineFeeder = createLedgerLineFeeder(ledger, () => supervisor.onActivity());
|
|
3782
|
+
const authoritativeStdout = lineFeeder.feed;
|
|
3598
3783
|
const runPromise = runProcessWithTimeout(supervisedProc, Math.max(1, Math.ceil(plan.workerWindowMs / 1000)), deps, {
|
|
3599
3784
|
termGraceMs: jobOptions.termGraceMs,
|
|
3600
3785
|
terminationController,
|
|
@@ -3626,8 +3811,7 @@ async function runCoveredAttempt(params) {
|
|
|
3626
3811
|
if (sampleTimer !== undefined)
|
|
3627
3812
|
deps.clearTimer(sampleTimer);
|
|
3628
3813
|
supervisor.dispose();
|
|
3629
|
-
|
|
3630
|
-
ledger.ingestLine(lineBuffer);
|
|
3814
|
+
lineFeeder.flush();
|
|
3631
3815
|
ledger.recordStreamEnd(procResult.authoritativeStreamError || procResult.exitUnconfirmed
|
|
3632
3816
|
? "stream_error"
|
|
3633
3817
|
: (procResult.stdoutEnd ?? "eof"));
|