@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.
Files changed (100) hide show
  1. package/README.md +252 -77
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +43 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/commands.generated.js +11 -11
  8. package/build/conductor/bridge-api-client.js +143 -1
  9. package/build/conductor/bring-up-facts.js +1 -1
  10. package/build/conductor/doctor.js +3 -3
  11. package/build/conductor/epic-implementer-cli.js +22 -6
  12. package/build/conductor/errors.js +4 -3
  13. package/build/conductor/guided-repair.js +6 -2
  14. package/build/conductor/hook-runtime-qualification.js +1 -1
  15. package/build/conductor/merge-execution-contract.js +27 -8
  16. package/build/conductor/merge-execution-eligibility.js +3 -2
  17. package/build/conductor/pr-ci-producer.js +1 -1
  18. package/build/conductor/readiness-cli.js +18 -2
  19. package/build/conductor/readiness-sections.js +41 -6
  20. package/build/conductor/readiness.js +181 -29
  21. package/build/conductor/recovery-cli.js +200 -8
  22. package/build/conductor/recovery-operations.js +69 -3
  23. package/build/conductor/repair-contract.js +54 -3
  24. package/build/conductor/repository-workflow-remedy.js +125 -0
  25. package/build/conductor/tools.js +2 -2
  26. package/build/conductor-bundle-artifacts.js +8 -0
  27. package/build/docs.generated.js +8 -3
  28. package/build/drive-epic-key.js +6 -5
  29. package/build/drive-epic.js +85 -54
  30. package/build/epic-coherence-input.js +2 -1
  31. package/build/epic-implementer-bin.js +22 -20
  32. package/build/epic-implementer-bundle-cli.js +2 -2
  33. package/build/executor/cli.js +3 -2
  34. package/build/executor/completion-contract.js +48 -5
  35. package/build/executor/completion-supervision.js +4 -0
  36. package/build/executor/env.js +19 -0
  37. package/build/executor/heartbeat.js +1 -1
  38. package/build/executor/http-client.js +20 -0
  39. package/build/executor/job-errors.js +25 -0
  40. package/build/executor/job-runner.js +225 -41
  41. package/build/executor/observation.js +6 -1
  42. package/build/executor/preflight.js +51 -14
  43. package/build/executor/required-work-ledger.js +6 -1
  44. package/build/executor/resume-pre-spawn.js +2 -0
  45. package/build/executor/runner.js +8 -2
  46. package/build/executor/service-unit.js +7 -1
  47. package/build/executor/worker-finalization.js +82 -29
  48. package/build/executor/worker-guard-hook.js +137 -33
  49. package/build/executor/worktree.js +82 -1
  50. package/build/index.js +403 -429
  51. package/build/install-bridge-conductor.js +8 -6
  52. package/build/install-bridge.js +3 -3
  53. package/build/phase-context-git-probe.js +68 -0
  54. package/build/phase-result-artifacts.js +173 -5
  55. package/build/pipelines.generated.js +18 -24
  56. package/build/plane/alembic-head.js +12 -3
  57. package/build/plane/base-worktree-guard.js +147 -0
  58. package/build/plane/cli.js +217 -17
  59. package/build/plane/code-snapshot.js +468 -0
  60. package/build/plane/defaults.js +48 -3
  61. package/build/plane/endpoint.js +41 -0
  62. package/build/plane/manifest.js +120 -1
  63. package/build/plane/member-roster.js +94 -19
  64. package/build/plane/preflight.js +30 -2
  65. package/build/plane/process-cwd.js +67 -0
  66. package/build/plane/restart.js +542 -0
  67. package/build/plane/shutdown.js +31 -0
  68. package/build/plane/status.js +143 -8
  69. package/build/plane/supervisor.js +586 -179
  70. package/build/plane/types.js +54 -2
  71. package/build/printed-remedy-commands.js +37 -0
  72. package/build/readiness-check.js +1 -1
  73. package/build/readme.generated.js +1 -1
  74. package/build/review-generation.js +22 -0
  75. package/build/setup-epic.js +44 -19
  76. package/build/sfcc/config.js +9 -0
  77. package/build/sfcc/credentials.js +100 -38
  78. package/build/sfcc/hostname.js +67 -0
  79. package/build/sfcc/permissions.js +17 -13
  80. package/build/sfcc/register.js +1 -1
  81. package/build/sfcc/tool-wrapper.js +39 -43
  82. package/build/sfcc/write-grants.js +17 -11
  83. package/build/sfcc/write-result.js +4 -3
  84. package/build/sfcc/writes-site-preference.js +20 -8
  85. package/build/start-tickets.js +179 -31
  86. package/build/tool-error-envelope.js +61 -0
  87. package/build/version.generated.js +3 -3
  88. package/build/visual-diff.js +22 -1
  89. package/build/worker-guard-hook-bin.js +3 -6
  90. package/build/worker-guard-hook-main.js +47 -0
  91. package/build/worktree-core.js +130 -4
  92. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  93. package/docs/EPIC-IMPLEMENTER.md +662 -0
  94. package/docs/install/mcp-tool-integrations.md +24 -15
  95. package/docs/install/sfcc-integration.md +12 -4
  96. package/package.json +1 -1
  97. package/pipelines/plan-epic.json +0 -5
  98. package/pipelines/review-ticket.json +1 -1
  99. package/docs/CONDUCTOR.md +0 -605
  100. 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, mirroring how `store-and-approve-epic-plan.md` invokes
10
- * `setup-epic` rather than hand-rolling HTTP calls.
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
@@ -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> ...] [options]",
40
+ "Usage: mcp-server executor --repo <name> [--repo <name> ...]",
41
+ " (--epic-run-id <id> | --repo-wide) [options]",
41
42
  "",
42
- "Runs the Epic Conductor v2 local executor: poll → claim → spawn → heartbeat.",
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.cli_version === SUPPORTED_COMPLETION_RUNTIME.cli_version &&
247
- runtime.platform === SUPPORTED_COMPLETION_RUNTIME.platform &&
248
- runtime.event_schema === SUPPORTED_COMPLETION_RUNTIME.event_schema);
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: ["worker_not_launched"],
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
  }
@@ -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/claude/epic-implementer-standing-rules.md`).
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, SUPPORTED_COMPLETION_RUNTIME, COMPLETION_CONTRACT_MARKER_VALUES, isCoveredJobType, MAX_EVENT_BYTES, runtimeMatchesSupported, successRefusalReason, verificationVerdictFromFacts, } from "./completion-contract.js";
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) => params.observation.recordStdout(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
- * Wrap the client so a covered claim's /fail without an envelope carries the
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: { ...SUPPORTED_COMPLETION_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: adapter.identity.agentId,
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. A line
3572
- // that outgrows the event bound is handed over once (the ledger marks it
3573
- // `stream_oversize`) and the rest of it is discarded up to its newline.
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
- if (!discardingOversize && lineBuffer.length > 0)
3630
- ledger.ingestLine(lineBuffer);
3814
+ lineFeeder.flush();
3631
3815
  ledger.recordStreamEnd(procResult.authoritativeStreamError || procResult.exitUnconfirmed
3632
3816
  ? "stream_error"
3633
3817
  : (procResult.stdoutEnd ?? "eof"));