@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.
Files changed (132) hide show
  1. package/README.md +269 -82
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +55 -34
  4. package/build/agent-launchers/executor-adapter.js +34 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/bridge-config.js +104 -0
  8. package/build/codex-bridge-profile.js +10 -1
  9. package/build/commands.generated.js +12 -12
  10. package/build/conductor/bridge-api-client.js +372 -10
  11. package/build/conductor/bring-up-facts.js +1 -1
  12. package/build/conductor/doctor.js +3 -3
  13. package/build/conductor/epic-child-continuation.js +61 -34
  14. package/build/conductor/epic-implementer-cli.js +34 -6
  15. package/build/conductor/errors.js +4 -3
  16. package/build/conductor/guided-repair.js +327 -0
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/merge-execution-contract.js +27 -8
  19. package/build/conductor/merge-execution-eligibility.js +3 -2
  20. package/build/conductor/pr-ci-producer.js +1 -1
  21. package/build/conductor/readiness-cli.js +46 -9
  22. package/build/conductor/readiness-sections.js +41 -6
  23. package/build/conductor/readiness.js +204 -30
  24. package/build/conductor/recovery-cli.js +703 -20
  25. package/build/conductor/recovery-operations.js +392 -3
  26. package/build/conductor/repair-contract.js +112 -3
  27. package/build/conductor/repository-workflow-remedy.js +125 -0
  28. package/build/conductor/tools.js +2 -2
  29. package/build/conductor-bundle-artifacts.js +8 -0
  30. package/build/credential-store.js +5 -4
  31. package/build/diagnostic-text.js +36 -0
  32. package/build/docs.generated.js +8 -3
  33. package/build/doctor.js +70 -10
  34. package/build/drive-epic-key.js +6 -5
  35. package/build/drive-epic.js +479 -131
  36. package/build/epic-coherence-input.js +2 -1
  37. package/build/epic-implementer-bin.js +30 -26
  38. package/build/epic-implementer-bundle-cli.js +2 -2
  39. package/build/epic-launch-diagnostic.js +31 -0
  40. package/build/epic-launch-profile.js +44 -14
  41. package/build/executor/cli.js +3 -2
  42. package/build/executor/completion-contract.js +48 -5
  43. package/build/executor/completion-supervision.js +9 -0
  44. package/build/executor/correction-dispatch.js +60 -0
  45. package/build/executor/env.js +25 -0
  46. package/build/executor/heartbeat.js +1 -1
  47. package/build/executor/http-client.js +35 -0
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +38 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +485 -74
  52. package/build/executor/job-types.js +8 -13
  53. package/build/executor/observation.js +6 -1
  54. package/build/executor/preflight.js +136 -10
  55. package/build/executor/prompt-spec.js +113 -9
  56. package/build/executor/required-work-ledger.js +6 -1
  57. package/build/executor/resume-pre-spawn.js +2 -0
  58. package/build/executor/runner.js +31 -2
  59. package/build/executor/service-unit.js +7 -1
  60. package/build/executor/stale-artifacts.js +5 -4
  61. package/build/executor/types.js +1 -0
  62. package/build/executor/watch-cli.js +35 -8
  63. package/build/executor/worker-command.js +43 -43
  64. package/build/executor/worker-finalization.js +161 -43
  65. package/build/executor/worker-guard-hook.js +152 -12
  66. package/build/executor/worker-log.js +137 -70
  67. package/build/executor/worktree-gc.js +30 -6
  68. package/build/executor/worktree.js +82 -1
  69. package/build/index.js +482 -660
  70. package/build/init.js +107 -4
  71. package/build/install-bridge-conductor.js +8 -6
  72. package/build/install-bridge.js +287 -23
  73. package/build/install-doctor.js +172 -30
  74. package/build/install-reexec.js +69 -7
  75. package/build/macos-terminal-permission-guidance.js +16 -0
  76. package/build/model-thresholds-tool.js +185 -0
  77. package/build/phase-context-git-probe.js +68 -0
  78. package/build/phase-result-artifacts.js +173 -5
  79. package/build/pipelines.generated.js +19 -25
  80. package/build/plane/alembic-head.js +12 -3
  81. package/build/plane/base-worktree-guard.js +147 -0
  82. package/build/plane/cli.js +217 -17
  83. package/build/plane/code-snapshot.js +468 -0
  84. package/build/plane/defaults.js +48 -3
  85. package/build/plane/endpoint.js +41 -0
  86. package/build/plane/manifest.js +120 -1
  87. package/build/plane/member-roster.js +94 -19
  88. package/build/plane/preflight.js +30 -2
  89. package/build/plane/process-cwd.js +67 -0
  90. package/build/plane/restart.js +542 -0
  91. package/build/plane/shutdown.js +31 -0
  92. package/build/plane/status.js +145 -6
  93. package/build/plane/supervisor.js +588 -177
  94. package/build/plane/types.js +54 -2
  95. package/build/plane/worker-exit-code.js +2 -0
  96. package/build/printed-remedy-commands.js +37 -0
  97. package/build/project-root.js +99 -0
  98. package/build/readiness-check.js +1 -1
  99. package/build/readme.generated.js +1 -1
  100. package/build/review-generation.js +22 -0
  101. package/build/review-tickets.js +156 -13
  102. package/build/setup-epic.js +224 -127
  103. package/build/sfcc/config.js +9 -0
  104. package/build/sfcc/credentials.js +100 -38
  105. package/build/sfcc/hostname.js +67 -0
  106. package/build/sfcc/permissions.js +17 -13
  107. package/build/sfcc/register.js +1 -1
  108. package/build/sfcc/setup-status.js +2 -0
  109. package/build/sfcc/tool-wrapper.js +41 -43
  110. package/build/sfcc/tools-configured.js +96 -0
  111. package/build/sfcc/write-grants.js +17 -11
  112. package/build/sfcc/write-result.js +4 -3
  113. package/build/sfcc/writes-site-preference.js +20 -8
  114. package/build/start-tickets.js +332 -79
  115. package/build/tool-error-envelope.js +61 -0
  116. package/build/upgrade-cli.js +109 -24
  117. package/build/version.generated.js +3 -3
  118. package/build/visual-diff.js +22 -1
  119. package/build/windows-npm-entry.js +103 -0
  120. package/build/worker-guard-hook-bin.js +3 -5
  121. package/build/worker-guard-hook-main.js +47 -0
  122. package/build/worktree-core.js +136 -9
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  124. package/docs/EPIC-IMPLEMENTER.md +662 -0
  125. package/docs/install/mcp-tool-integrations.md +25 -15
  126. package/docs/install/sfcc-integration.md +21 -8
  127. package/package.json +1 -1
  128. package/pipelines/implement-ticket.json +1 -1
  129. package/pipelines/plan-epic.json +0 -5
  130. package/pipelines/review-ticket.json +1 -1
  131. package/docs/CONDUCTOR.md +0 -605
  132. 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
@@ -0,0 +1,31 @@
1
+ /** Keep operator-controlled components out of trusted epic-launch refusal text. */
2
+ export function diagnosticComponent(value) {
3
+ // A short identifier here is a schema-field label, not its stored value.
4
+ if (/^field '[A-Za-z][A-Za-z0-9]{0,39}'$/.test(value))
5
+ return value;
6
+ if (value.length > 512 ||
7
+ /[\x00-\x1f\x7f]/.test(value) ||
8
+ /(?:sk[-_][A-Za-z0-9]+|api[-_]?key[-_:=][A-Za-z0-9]+|secret|token|password|bearer|authorization)/i.test(value) ||
9
+ /%[0-9a-f]{2}/i.test(value) && decodedLooksSensitive(value))
10
+ return "<redacted>";
11
+ return value;
12
+ }
13
+ function decodedLooksSensitive(value) {
14
+ try {
15
+ const decoded = decodeURIComponent(value);
16
+ return decoded !== value && diagnosticComponent(decoded) === "<redacted>";
17
+ }
18
+ catch {
19
+ return true;
20
+ }
21
+ }
22
+ /** URL validation alone permits paths and queries that look like credentials. */
23
+ export function safeEpicLaunchTargetUrl(baseUrl) {
24
+ try {
25
+ const url = new URL(baseUrl);
26
+ return !url.username && !url.password && diagnosticComponent(baseUrl) === baseUrl;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
@@ -26,11 +26,13 @@
26
26
  */
27
27
  import os from "node:os";
28
28
  import path from "node:path";
29
+ import { createHash } from "node:crypto";
29
30
  import { chmod, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
30
31
  import { durablyReplaceCredentialStoreJson, getPrimaryCredentialStorePath, withCredentialStoreLock, } from "./credential-store.js";
31
32
  import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
32
33
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
33
34
  import { isTransientNpxEntryPath } from "./mcp-server-invocation.js";
35
+ import { diagnosticComponent } from "./epic-launch-diagnostic.js";
34
36
  // ---------------------------------------------------------------------------
35
37
  // Types
36
38
  // ---------------------------------------------------------------------------
@@ -45,7 +47,7 @@ export const EPIC_LAUNCH_EXECUTION_MODES = ["local", "durable"];
45
47
  /** One-time migration guidance for a profile this build cannot read. */
46
48
  export function epicLaunchProfileSetupGuidance(name) {
47
49
  return (`Re-run guided setup to (re)write it: npx -y ${MCP_PACKAGE_NAME} install conductor ` +
48
- `--launch-profile ${EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(name) ? name : "<name>"} --base-url <url> ...`);
50
+ `--launch-profile ${EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(name) ? diagnosticComponent(name) : "<name>"} --base-url <url> ...`);
49
51
  }
50
52
  // ---------------------------------------------------------------------------
51
53
  // Validation (pure)
@@ -73,7 +75,7 @@ function malformed(name, field, why) {
73
75
  return {
74
76
  ok: false,
75
77
  kind: "profile-malformed",
76
- message: `Epic launch profile '${name}' is unusable: ${field} ${why}. ${epicLaunchProfileSetupGuidance(name)}`,
78
+ message: `Epic launch profile '${diagnosticComponent(name)}' is unusable: ${diagnosticComponent(field)} ${why}. ${epicLaunchProfileSetupGuidance(name)}`,
77
79
  };
78
80
  }
79
81
  function validateRuntime(name, raw) {
@@ -118,7 +120,7 @@ export function validateEpicLaunchProfile(raw, selector = "<unnamed>") {
118
120
  return {
119
121
  ok: false,
120
122
  kind: "profile-unsupported-version",
121
- message: `Epic launch profile '${selector}' uses an unsupported schema version; this build reads ` +
123
+ message: `Epic launch profile '${diagnosticComponent(selector)}' uses an unsupported schema version; this build reads ` +
122
124
  `version ${EPIC_LAUNCH_PROFILE_VERSION} only. ${epicLaunchProfileSetupGuidance(selector)}`,
123
125
  };
124
126
  }
@@ -204,23 +206,23 @@ async function readStoreDocument(deps) {
204
206
  if (code === "ENOENT") {
205
207
  return { ok: true, present: false, document: { version: EPIC_LAUNCH_PROFILE_VERSION, profiles: {} } };
206
208
  }
207
- return { ok: false, kind: "store-unreadable", message: `Unable to read ${storePath}.` };
209
+ return { ok: false, kind: "store-unreadable", message: `Unable to read ${diagnosticComponent(storePath)}.` };
208
210
  }
209
211
  let parsed;
210
212
  try {
211
213
  parsed = JSON.parse(raw);
212
214
  }
213
215
  catch {
214
- return { ok: false, kind: "store-unreadable", message: `${storePath} is not valid JSON.` };
216
+ return { ok: false, kind: "store-unreadable", message: `${diagnosticComponent(storePath)} is not valid JSON.` };
215
217
  }
216
218
  if (!isPlainObject(parsed) || !isPlainObject(parsed.profiles)) {
217
- return { ok: false, kind: "store-unreadable", message: `${storePath} has no profiles object.` };
219
+ return { ok: false, kind: "store-unreadable", message: `${diagnosticComponent(storePath)} has no profiles object.` };
218
220
  }
219
221
  if (parsed.version !== EPIC_LAUNCH_PROFILE_VERSION) {
220
222
  return {
221
223
  ok: false,
222
224
  kind: "profile-unsupported-version",
223
- message: `${storePath} uses an unsupported schema version; this build reads version ` +
225
+ message: `${diagnosticComponent(storePath)} uses an unsupported schema version; this build reads version ` +
224
226
  `${EPIC_LAUNCH_PROFILE_VERSION} only. ${epicLaunchProfileSetupGuidance("<name>")}`,
225
227
  };
226
228
  }
@@ -249,7 +251,7 @@ export async function readEpicLaunchProfile(name, deps) {
249
251
  return {
250
252
  ok: false,
251
253
  kind: "profile-missing",
252
- message: `No epic launch profile named '${name}' exists on this host. ${epicLaunchProfileSetupGuidance(name)}`,
254
+ message: `No epic launch profile named '${diagnosticComponent(name)}' exists on this host. ${epicLaunchProfileSetupGuidance(name)}`,
253
255
  };
254
256
  }
255
257
  const validated = validateEpicLaunchProfile(store.document.profiles[name], name);
@@ -259,7 +261,7 @@ export async function readEpicLaunchProfile(name, deps) {
259
261
  return {
260
262
  ok: false,
261
263
  kind: "profile-host-mismatch",
262
- message: `Epic launch profile '${name}' was approved on a different host. Profiles are host-local; ` +
264
+ message: `Epic launch profile '${diagnosticComponent(name)}' was approved on a different host. Profiles are host-local; ` +
263
265
  epicLaunchProfileSetupGuidance(name),
264
266
  };
265
267
  }
@@ -309,6 +311,34 @@ export async function writeEpicLaunchProfile(profile, deps) {
309
311
  return { ok: true, path: storePath, action: existing === undefined ? "created" : "updated" };
310
312
  }, (error) => ({ ok: false, kind: "store-write-failed", message: error }));
311
313
  }
314
+ /** Safe for text and JSON; no policy recipient or webhook value is retained. */
315
+ export function formatEpicLaunchChannelIdentity(identity) {
316
+ return identity;
317
+ }
318
+ const pinnedPolicyContents = new WeakMap();
319
+ /** Store selected content once, without making it serializable with the selection. */
320
+ export function pinEpicLaunchPolicy(selection, policy) {
321
+ pinnedPolicyContents.set(selection, policy);
322
+ return selection;
323
+ }
324
+ export function getPinnedEpicLaunchPolicy(selection) {
325
+ return pinnedPolicyContents.get(selection);
326
+ }
327
+ export function epicLaunchPolicyDigest(raw) {
328
+ return createHash("sha256").update(raw).digest("hex");
329
+ }
330
+ /** Finalize the already selected target after the run's existence is known. */
331
+ export function resolveEpicLaunchSelection(args) {
332
+ return Object.freeze({
333
+ baseUrl: args.baseUrl,
334
+ repoName: args.repoName,
335
+ targetSource: args.targetSource,
336
+ profileName: args.profile?.name ?? null,
337
+ topology: args.topology,
338
+ policy: Object.freeze(args.policy),
339
+ escalationChannel: args.escalationChannel,
340
+ });
341
+ }
312
342
  /**
313
343
  * Normalize every applicable target source and decide ONE target, before any
314
344
  * credential is resolved and before any request is made.
@@ -338,7 +368,7 @@ export function resolveEpicLaunchTarget(inputs) {
338
368
  const flag = normalize(inputs.flagBaseUrl, "--base-url");
339
369
  if (!flag.ok)
340
370
  return { ok: false, kind: "target-invalid", message: flag.message };
341
- const env = normalize(inputs.env.BAPI_BASE_URL, "BAPI_BASE_URL");
371
+ const env = normalize(inputs.profileSource === "profile-env" ? undefined : inputs.env.BAPI_BASE_URL, "BAPI_BASE_URL");
342
372
  if (!env.ok)
343
373
  return { ok: false, kind: "target-invalid", message: env.message };
344
374
  const profile = inputs.profile;
@@ -347,7 +377,7 @@ export function resolveEpicLaunchTarget(inputs) {
347
377
  return {
348
378
  ok: false,
349
379
  kind: "target-conflict",
350
- message: `--base-url names a different endpoint than launch profile '${profile.name}'. An override ` +
380
+ message: `--base-url names a different endpoint than launch profile '${diagnosticComponent(profile.name)}'. An override ` +
351
381
  "does not rebind a profile; re-run guided setup to change its target.",
352
382
  };
353
383
  }
@@ -355,7 +385,7 @@ export function resolveEpicLaunchTarget(inputs) {
355
385
  return {
356
386
  ok: false,
357
387
  kind: "target-conflict",
358
- message: `BAPI_BASE_URL names a different endpoint than launch profile '${profile.name}'. Unset it, ` +
388
+ message: `BAPI_BASE_URL names a different endpoint than launch profile '${diagnosticComponent(profile.name)}'. Unset it, ` +
359
389
  "or re-run guided setup to change the profile's target.",
360
390
  };
361
391
  }
@@ -364,10 +394,10 @@ export function resolveEpicLaunchTarget(inputs) {
364
394
  return {
365
395
  ok: false,
366
396
  kind: "repository-conflict",
367
- message: `--repo names a different repository than launch profile '${profile.name}'.`,
397
+ message: `--repo names a different repository than launch profile '${diagnosticComponent(profile.name)}'.`,
368
398
  };
369
399
  }
370
- return { ok: true, baseUrl: profile.baseUrl, source: "profile", repoName: profile.repoName };
400
+ return { ok: true, baseUrl: profile.baseUrl, source: inputs.profileSource ?? "profile", repoName: profile.repoName };
371
401
  }
372
402
  if (flag.value !== null && env.value !== null && flag.value !== env.value) {
373
403
  return {
@@ -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
  }
@@ -223,6 +223,15 @@ export function finalizationFacts(outcome) {
223
223
  case "WorkerFinalizationMissingRemoteBranchAndPr":
224
224
  case "WorkerFinalizationSavedButUnfinalized":
225
225
  return { verdict: "unmet", pushSatisfied: false, prSatisfied: false };
226
+ // BAPI-1330: the worker's full HEAD, or the PR's head, could not be
227
+ // established. Nothing was observed MISSING, so the verdict is `unknown`
228
+ // and neither push nor PR is inferred satisfied.
229
+ case "WorkerFinalizationPublicationUnverified":
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 };
226
235
  default:
227
236
  return { verdict: "unknown", pushSatisfied: false, prSatisfied: false };
228
237
  }
@@ -0,0 +1,60 @@
1
+ /** BAPI-1328: persisted remaining-work authority, independent of prompt text. */
2
+ export const CORRECTION_INTENT_CAPABILITY = "correction_intent.v1";
3
+ export const CORRECTION_INTENT_VERSION = 1;
4
+ export const CORRECTION_KINDS = [
5
+ "review_fix", "operator_remediation", "ci_fix", "focused_rebase",
6
+ "overlap_reconciliation", "approved_spec_delta", "continuation",
7
+ "required_checks", "finalization", "integration_correction",
8
+ ];
9
+ function nonblank(value) {
10
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 4096;
11
+ }
12
+ function reference(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value) &&
14
+ Object.keys(value).length > 0 && Object.entries(value).every(([key, item]) => nonblank(key) && ((typeof item === "string" && nonblank(item)) ||
15
+ (typeof item === "number" && Number.isInteger(item))));
16
+ }
17
+ /** A prompt or job type never grants correction authority. */
18
+ export function validateCorrectionIntent(value, row) {
19
+ const refuse = (code, detail) => ({ ok: false, code, detail });
20
+ if (!value || typeof value !== "object" || Array.isArray(value))
21
+ return refuse("correction_context_missing", "correction intent is absent");
22
+ const intent = value;
23
+ if (intent.version !== CORRECTION_INTENT_VERSION)
24
+ return refuse("correction_contract_version_unsupported", "unsupported correction intent version");
25
+ if (!CORRECTION_KINDS.includes(intent.kind))
26
+ return refuse("correction_contract_invalid", "unknown correction kind");
27
+ for (const key of ["repo_name", "epic_run_id", "ticket_key"]) {
28
+ if (!nonblank(row[key]) || intent[key] !== row[key])
29
+ return refuse("correction_contract_invalid", `${key} differs from row`);
30
+ }
31
+ if (!nonblank(intent.branch) || (row.expected_branch && intent.branch !== row.expected_branch))
32
+ return refuse("correction_context_missing", "retained branch is unavailable or changed");
33
+ if (!nonblank(intent.head_sha) || (row.expected_head_sha && intent.head_sha !== row.expected_head_sha))
34
+ return refuse("correction_context_missing", "retained head is unavailable or changed");
35
+ if (!nonblank(intent.obligation) || !reference(intent.context_ref))
36
+ return refuse("correction_context_missing", "remaining obligation or context reference is absent");
37
+ if (intent.resume !== undefined && intent.resume !== null && intent.resume !== false)
38
+ return refuse("correction_contract_invalid", "unsupported resume metadata");
39
+ if (intent.kind === "approved_spec_delta") {
40
+ const delta = intent.approved_delta;
41
+ if (!delta || typeof delta !== "object" || Array.isArray(delta))
42
+ return refuse("renewed_scope_approval_required", "approved same-ticket delta is absent");
43
+ const d = delta;
44
+ if (!["old_identity", "new_identity", "obligation"].every(key => nonblank(d[key])) ||
45
+ !["file_boundary", "dependency_boundary"].every(key => Array.isArray(d[key]) && (key !== "file_boundary" || d[key].length > 0) &&
46
+ d[key].every(nonblank)))
47
+ return refuse("renewed_scope_approval_required", "approved delta identity or boundary is absent");
48
+ }
49
+ return { ok: true, intent };
50
+ }
51
+ export function isRemainingWork(jobType, payload) {
52
+ if (["remediate", "rebase", "ci_fix", "reconcile_overlap", "resume"].includes(jobType))
53
+ return true;
54
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
55
+ return false;
56
+ const p = payload;
57
+ return p.correction_intent !== undefined || p.continuation !== undefined ||
58
+ p.continuation_of_job_id !== undefined || p.finalization !== undefined ||
59
+ p.remaining_checks !== undefined || p.required_checks_remaining !== undefined;
60
+ }
@@ -118,6 +118,7 @@ const SECRET_NAME_FRAGMENTS = ["TOKEN", "SECRET", "PASSWORD", "API_KEY"];
118
118
  * without anything in the executor choosing that.
119
119
  */
120
120
  export const EXPLICIT_DENY_KEYS = [
121
+ "BAPI_CORRECTION_INTENT_VERSION",
121
122
  "BRIDGE_MCP_PROFILE",
122
123
  // BAPI-1301: the per-job ownership tag. The executor injects a fresh,
123
124
  // unpredictable value AFTER this builder and the adapter have run
@@ -287,6 +288,11 @@ export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
287
288
  env.POSTGRES_DATABASE = WORKER_SENTINEL_DATABASE_NAME;
288
289
  env.POSTGRES_DB = WORKER_SENTINEL_DATABASE_NAME;
289
290
  env.PGDATABASE = WORKER_SENTINEL_DATABASE_NAME;
291
+ if (options.correctionIntentVersion !== undefined) {
292
+ if (options.correctionIntentVersion !== 1)
293
+ throw new Error("unsupported correction intent version");
294
+ env.BAPI_CORRECTION_INTENT_VERSION = "1";
295
+ }
290
296
  if (typeof options.effectiveBaseBranch === "string" &&
291
297
  options.effectiveBaseBranch.length > 0) {
292
298
  env[PR_BASE_BRANCH_ENV_VAR] = options.effectiveBaseBranch;
@@ -323,3 +329,22 @@ export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
323
329
  // per call site.
324
330
  return env;
325
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 {
@@ -294,6 +314,18 @@ export function createExecutorHttpClient(config) {
294
314
  // 5xx and any other unexpected status are retryable.
295
315
  return { kind: "retryable", error: `claim failed (HTTP ${status}): ${boundedDetail(text)}` };
296
316
  },
317
+ async scopeReadiness(job) {
318
+ const { status, text } = await post(`/${job.id}/scope-readiness`, { repo_name: job.repo_name, claim_token: job.claim_token }, job.repo_name);
319
+ if (status !== 200)
320
+ throw new Error(`scope readiness HTTP ${status}`);
321
+ const parsed = JSON.parse(text);
322
+ if (!parsed || typeof parsed !== "object" ||
323
+ !("decision" in parsed) ||
324
+ !["ready", "waiting", "blocked"].includes(String(parsed.decision))) {
325
+ throw new Error("scope readiness response malformed");
326
+ }
327
+ return parsed;
328
+ },
297
329
  // Heartbeat does NOT go through `mutate()`: it is the one mutation whose
298
330
  // response BODY carries information beyond the outcome (the BAPI-731 stop
299
331
  // request), so it maps the response itself instead of discarding the body.
@@ -371,6 +403,9 @@ export function createExecutorHttpClient(config) {
371
403
  error_kind: failure.error_kind,
372
404
  error_message: failure.error_message,
373
405
  classification: failure.classification,
406
+ // BAPI-1330: the observed process exit, only when the runner has one —
407
+ // a failure without it keeps its exact pre-BAPI-1330 body.
408
+ ...(failure.exit_code !== undefined ? { exit_code: failure.exit_code } : {}),
374
409
  local_commit_count: failure.local_commit_count,
375
410
  last_commit_sha: failure.last_commit_sha,
376
411
  telemetry: failure.telemetry,
@@ -703,13 +703,18 @@ export function buildIntegrationCorrectionJob(action, timeoutSeconds) {
703
703
  job_type: "rebase",
704
704
  ticket_key: action.epic_key,
705
705
  epic_run_id: action.epic_run_id,
706
- payload: { prompt: action.correction_prompt ?? "", base_branch: action.base_branch },
706
+ payload: {
707
+ prompt: action.correction_prompt ?? "", base_branch: action.base_branch,
708
+ correction_intent: action.correction_intent ?? null,
709
+ integration_action_id: action.action_id,
710
+ },
707
711
  expected_branch: action.epic_branch,
708
712
  expected_head_sha: action.expected_epic_head_sha,
709
713
  lease_expires_at: action.lease_expires_at,
710
714
  attempt_count: 1,
711
715
  max_attempts: 1,
712
716
  timeout_seconds: timeoutSeconds,
717
+ required_capabilities: ["correction_intent.v1"],
713
718
  };
714
719
  }
715
720
  /**
@@ -28,10 +28,11 @@
28
28
  * exited cleanly and its exact HEAD is durable on origin, but no usable pull
29
29
  * request exists for that branch: none is visible, its URL is blank, its
30
30
  * description is blank (BAPI-824 folded in), or it does not yet carry the
31
- * pushed head. The work is COMPLETE and PUSHED — only the attachment is
32
- * missing — so this kind is deliberately distinct from every other
33
- * finalization failure, and its guidance names attachment and warns against
34
- * deleting or rebasing the branch; see `worker-finalization.ts`.
31
+ * pushed head. The commits are PUSHED and the attachment is missing; whether
32
+ * the required work is finished or its checks passed is NOT established by
33
+ * this kind (BAPI-1330). It stays distinct from every other finalization
34
+ * failure, and its guidance names attachment and warns against deleting or
35
+ * rebasing the branch; see `worker-finalization.ts`.
35
36
  * - `WorkerFinalizationSavedButUnfinalized` — an implementation-style job
36
37
  * (BAPI-762) exited cleanly, and an origin branch exists at an authoritative
37
38
  * tip that does NOT match the worker's own HEAD — durable work exists on
@@ -40,6 +41,11 @@
40
41
  * work reached origin") because this case has positive proof of recoverable
41
42
  * state: an operator can resume/finalize from the existing branch rather than
42
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`.
43
49
  * - `ContractError.WorkerConfigIsolation` — a per-job isolated Claude config
44
50
  * directory could not be established (unsupported platform, unsupported auth
45
51
  * layout, unseedable directory, or a missing required MCP registration), so
@@ -132,6 +138,32 @@ export const WorkerFinalizationMissingRemoteBranchAndPr = "WorkerFinalizationMis
132
138
  export const WorkerFinalizationPrBaseMismatch = "WorkerFinalizationPrBaseMismatch";
133
139
  export const WorkerFinalizationPrNotAttached = "WorkerFinalizationPrNotAttached";
134
140
  export const WorkerFinalizationSavedButUnfinalized = "WorkerFinalizationSavedButUnfinalized";
141
+ /**
142
+ * BAPI-1330: a COVERED attempt's publication could not be proven — the worker's
143
+ * full HEAD was unknown, or the pull request reported no head commit. Distinct
144
+ * from `WorkerFinalizationPrNotAttached` because nothing here was observed to be
145
+ * missing: the facts needed to decide were unavailable, so the finalization
146
+ * verdict is `unknown`, never `unmet` and never `ok`.
147
+ */
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";
135
167
  export const WorkerConfigIsolation = "ContractError.WorkerConfigIsolation";
136
168
  export const PreSpawnVerification = "ContractError.PreSpawnVerification";
137
169
  export const BridgeCredentialUnavailable = "ContractError.BridgeCredentialUnavailable";
@@ -273,6 +305,8 @@ export function formatMcpSurfaceMismatch(expected, observed) {
273
305
  "Check that the installed Claude CLI supports --strict-mcp-config and that nothing adds MCP " +
274
306
  "servers outside the worktree registration.");
275
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";
276
310
  /**
277
311
  * The fixed message posted with {@link WorkerStartupFatal}.
278
312
  *