@bridge_gpt/mcp-server 0.2.60 → 0.2.65

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 (89) hide show
  1. package/README.md +58 -11
  2. package/build/agent-launchers/claude-executor-adapter.js +134 -3
  3. package/build/agent-launchers/executor-adapter.js +54 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/commands.generated.js +8 -7
  7. package/build/conductor/bridge-api-client.js +678 -14
  8. package/build/conductor/bring-up-facts.js +3 -1
  9. package/build/conductor/doctor.js +23 -14
  10. package/build/conductor/epic-child-continuation.js +324 -0
  11. package/build/conductor/epic-implementer-cli.js +142 -17
  12. package/build/conductor/git-hooks.js +529 -118
  13. package/build/conductor/hook-runtime-qualification.js +340 -0
  14. package/build/conductor/merge-execution-contract.js +30 -5
  15. package/build/conductor/merge-execution-eligibility.js +10 -9
  16. package/build/conductor/readiness-sections.js +104 -20
  17. package/build/conductor/readiness.js +129 -25
  18. package/build/conductor/recovery-cli.js +354 -13
  19. package/build/conductor/recovery-operations.js +251 -5
  20. package/build/conductor/repair-contract.js +8 -0
  21. package/build/conductor-bundle-artifacts.js +1 -0
  22. package/build/credential-store.js +194 -0
  23. package/build/docs.generated.js +2 -1
  24. package/build/doctor.js +161 -7
  25. package/build/drive-epic-key.js +12 -0
  26. package/build/drive-epic.js +1318 -111
  27. package/build/epic-coherence-input-cli.js +137 -0
  28. package/build/epic-coherence-input.js +920 -0
  29. package/build/epic-coherence-input.test-fixtures.js +175 -0
  30. package/build/epic-implementer-bin.js +35 -29
  31. package/build/epic-implementer-bundle-cli.js +14 -1
  32. package/build/epic-integration-pr.js +61 -15
  33. package/build/epic-launch-profile.js +464 -0
  34. package/build/epic-run-status.js +5 -0
  35. package/build/epic-shared/bridge-client.js +46 -4
  36. package/build/executor/cli.js +48 -0
  37. package/build/executor/completion-contract.js +423 -0
  38. package/build/executor/completion-supervision.js +286 -0
  39. package/build/executor/credentials.js +19 -1
  40. package/build/executor/deps.js +65 -1
  41. package/build/executor/env.js +6 -0
  42. package/build/executor/heartbeat.js +13 -3
  43. package/build/executor/http-client.js +117 -1
  44. package/build/executor/integration-job.js +759 -0
  45. package/build/executor/job-errors.js +48 -0
  46. package/build/executor/job-runner.js +1386 -35
  47. package/build/executor/job-types.js +10 -2
  48. package/build/executor/observation.js +114 -7
  49. package/build/executor/preflight.js +99 -0
  50. package/build/executor/process-containment.js +756 -0
  51. package/build/executor/process.js +235 -5
  52. package/build/executor/prompt-spec.js +262 -1
  53. package/build/executor/provider-checks.js +69 -0
  54. package/build/executor/rebase-finalization.js +351 -0
  55. package/build/executor/reconcile-overlap-job.js +77 -84
  56. package/build/executor/required-verification.js +313 -0
  57. package/build/executor/required-work-ledger.js +1121 -0
  58. package/build/executor/results.js +142 -0
  59. package/build/executor/resume-pre-spawn.js +87 -4
  60. package/build/executor/runner.js +246 -1
  61. package/build/executor/test-declaration.js +20 -0
  62. package/build/executor/types.js +55 -0
  63. package/build/executor/worker-command.js +49 -13
  64. package/build/executor/worker-finalization.js +88 -11
  65. package/build/executor/worker-guard-hook.js +29 -4
  66. package/build/executor/worktree.js +13 -0
  67. package/build/executor-launch.js +658 -0
  68. package/build/index.js +772 -954
  69. package/build/install-bridge-conductor.js +622 -40
  70. package/build/install-bridge.js +92 -16
  71. package/build/merge-pull-request.js +1 -1
  72. package/build/merge-workflow-config.js +54 -0
  73. package/build/merge-workflow-drift.js +35 -4
  74. package/build/merge-workflow.js +386 -2
  75. package/build/pipelines.generated.js +9 -8
  76. package/build/plan-discovery.js +137 -0
  77. package/build/plan-epic-implementer-eligibility.js +2 -2
  78. package/build/plane/cli.js +37 -3
  79. package/build/plane/member-logs.js +3 -0
  80. package/build/plane/member-roster.js +7 -1
  81. package/build/plane/shutdown.js +59 -1
  82. package/build/plane/supervisor.js +148 -0
  83. package/build/readme.generated.js +1 -1
  84. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  85. package/build/setup-epic.js +130 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +2 -2
  88. package/docs/CONDUCTOR.md +116 -2
  89. package/package.json +2 -1
package/README.md CHANGED
@@ -369,7 +369,7 @@ later boot.
369
369
  "mcpServers": {
370
370
  "bridge": {
371
371
  "command": "npx",
372
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.60", "serve"],
372
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
373
373
  "env": {
374
374
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
375
375
  "BAPI_REPO_NAME": "your-repo",
@@ -391,7 +391,7 @@ later boot.
391
391
  "bridge": {
392
392
  "type": "stdio",
393
393
  "command": "npx",
394
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.60", "serve"],
394
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
395
395
  "env": {
396
396
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
397
397
  "BAPI_REPO_NAME": "your-repo",
@@ -413,7 +413,7 @@ later boot.
413
413
  "bridge": {
414
414
  "type": "stdio",
415
415
  "command": "npx",
416
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.60", "serve"],
416
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
417
417
  "env": {
418
418
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
419
419
  "BAPI_REPO_NAME": "your-repo",
@@ -436,7 +436,7 @@ Windsurf only supports global MCP configuration.
436
436
  "mcpServers": {
437
437
  "bridge": {
438
438
  "command": "npx",
439
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.60", "serve"],
439
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
440
440
  "env": {
441
441
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
442
442
  "BAPI_REPO_NAME": "your-repo",
@@ -455,7 +455,7 @@ Windsurf only supports global MCP configuration.
455
455
  ```toml
456
456
  [mcp_servers.bridge]
457
457
  command = "npx"
458
- args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.60", "serve"]
458
+ args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"]
459
459
 
460
460
  [mcp_servers.bridge.env]
461
461
  BAPI_BASE_URL = "https://bridgegpt-api.com"
@@ -1049,7 +1049,7 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
1049
1049
  npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
1050
1050
  ```
1051
1051
 
1052
- **Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and may inject `--model` at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand). Claude and Cursor map that tier through their agent registry aliases; Cursor validates against `cursor-agent --list-models`. Routing is gated per repo by `difficulty_model_routing_enabled` (default **ON**). Claude/Cursor may use `difficulty_model_tier_overrides` (tier → alias) and retain their premium fallback behavior on an unknown tier or transient failure. When an alias cannot be validated, the CLI omits `--model` and launches the agent default. `--dry-run` resolves routing read-only to preview the command without creating worktrees or opening tabs.
1052
+ **Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and may inject `--model` at the spawn boundary. Under the repo's `quality` optimization posture (also the fail-safe default), premium starts one step earlier, at difficulty 6. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand). Claude and Cursor map that tier through their agent registry aliases; Cursor validates against `cursor-agent --list-models`. Routing is gated per repo by `difficulty_model_routing_enabled` (default **ON**). Claude/Cursor may use `difficulty_model_tier_overrides` (tier → alias) and retain their premium fallback behavior on an unknown tier or transient failure. When an alias cannot be validated, the CLI omits `--model` and launches the agent default. `--dry-run` resolves routing read-only to preview the command without creating worktrees or opening tabs.
1053
1053
 
1054
1054
  For Codex, `GET /jira/codex-model-catalog?repo_name=...` is the sole tier-to-model authority. Bridge derives its three bare CLI IDs and SHA-256 mapping revision at request time from `MODEL_REGISTRY`; the launcher fetches and validates that catalog once per invocation. Repository `difficulty_model_tier_overrides` do not apply to Codex. A failed access, config, tier, catalog, revision, or alias check omits `--model` and launches the configured Codex default. The catalog expresses **Bridge provider policy only**: it cannot confirm that the local Codex login or subscription can use a model. The CLI has no supported non-interactive entitlement probe, so routing emits that limitation as a diagnostic and never uses Bridge's OpenAI API key as proof of local access.
1055
1055
 
@@ -1128,6 +1128,43 @@ including every error path, ever offers you two paths.
1128
1128
  the strategy and the posture (see below). With none of them, a multi-ticket epic
1129
1129
  runs on `epic/<KEY>`, unattended.
1130
1130
 
1131
+ **Finding the plan from the epic key (BAPI-1285).** `--plan-file` is optional.
1132
+ Without it, `drive-epic` searches `{docs_dir}/epic-plans/*/epic-plan.meta.json` for a
1133
+ file whose `epic_key` is exactly this epic (`docs_dir` is `BAPI_DOCS_DIR`, else
1134
+ `docs/tmp`, resolved against the Git top level; an absolute `BAPI_DOCS_DIR` is used
1135
+ as given). Exactly one match with a sibling `epic-plan.dag.json` is used as if you
1136
+ had passed `--plan-file`, and its path is printed before anything is created. Zero
1137
+ matches, two or more, or an unusable meta file **stop** with the paths found and
1138
+ create nothing, and a folder name, a child ticket key, or
1139
+ `sibling-ticket-manifest.json` is never used to guess. A supplied `--plan-file`
1140
+ always wins and discovery does not run.
1141
+
1142
+ The meta file is `{"epic_key": "<KEY>"}`, written by `emit-epic-implementer-bundle
1143
+ finalize` beside the DAG (its own atomic write, reported as `meta_path` in
1144
+ `--json`). It is separate from `epic-plan.dag.json` on purpose: the DAG is sent to
1145
+ the server, which rejects unknown fields. A plan finalized before this change has no
1146
+ meta file, so it is reported as not found — pass its DAG with `--plan-file`.
1147
+
1148
+ **A live run with no plan.** If the epic already has a live run and no plan was
1149
+ supplied or found, `drive-epic` creates nothing. It makes sure that run has an
1150
+ executor: it reuses a verified live one (never starting a second), starts one when
1151
+ the old one is gone, starts none for a durable launch profile with a confirmed
1152
+ repo-wide executor, and starts none when this checkout's plane is bound to that
1153
+ run. An epic with several active runs (`live-run-ambiguous`), or a run that cannot
1154
+ be read (`live-run-unknown`), is refused by name.
1155
+
1156
+ **Preview with `--dry-run`.** `drive-epic <EPIC> --dry-run` is read-only. It reads
1157
+ the target, readiness, the live run, and the plan, runs the bootstrap's own dry-run
1158
+ validation when a plan is available, and reports the decision a real invocation
1159
+ would make — `create`, `attach`, `refuse`, or `stop` (exit `0` for the first two,
1160
+ `1` for the others). It never creates a run, cuts a branch, starts a plane,
1161
+ requests a lane, or starts or signals an executor. With `--json` (valid only with
1162
+ `--dry-run`) stdout carries exactly one JSON document with `epic_key`, `target`,
1163
+ `readiness`, `live_run`, `plan`, `summary`, `decision`, and `refusal`. The
1164
+ readiness `reason_id` and `remedy` are the strings the real refusal prints. The
1165
+ webhook URL, credentials, and policy contents are never included. The preview does
1166
+ not probe ports, so a local plane that is not listening reads as unknown readiness.
1167
+
1131
1168
  **The composed bring-up is two-phase (BAPI-1102).** With `--plan-file` on a
1132
1169
  repository whose readiness is missing only the runtime facts, one invocation runs,
1133
1170
  in order: the **control plane** (Bridge API server + reconciler, each gated on its
@@ -1209,8 +1246,10 @@ default, or, when it declares none, as an explicit recorded `no_ci_gate` mode.
1209
1246
  > combining `--attended` with a file that declares `posture: "unattended"` is a named
1210
1247
  > refusal rather than a silent winner.
1211
1248
 
1212
- An unattended run has two one-time **repository** prerequisites — consent
1213
- (`unattended_epic_implementer_allowed`) and a verified notify webhook default — plus
1249
+ An unattended run has two **repository** prerequisites — consent
1250
+ (`unattended_epic_implementer_allowed`, ON by default; the repository owner can switch
1251
+ "Allow unattended Epic Implementer runs" off and on in the Bridge admin panel's Setup
1252
+ page) and a verified notify webhook default — plus
1214
1253
  confirmed default-branch review/CI workflows. Each is refused by name **before any
1215
1254
  run row is created** (`target_not_allowed`, `webhook_unverified`,
1216
1255
  `repository_readiness_unconfirmed`), and `drive-epic` readiness and `doctor` both
@@ -1263,7 +1302,7 @@ one file in your repository that can perform a privileged merge.
1263
1302
 
1264
1303
  | Path | What it is |
1265
1304
  |---|---|
1266
- | `.github/workflows/epic-implementer-merge.yml` | The merge workflow, contract version 1. |
1305
+ | `.github/workflows/epic-implementer-merge.yml` | The merge workflow, contract version 2. |
1267
1306
  | `.bridge/merge-workflow/CODEOWNERS.fragment` | A CODEOWNERS rule for that path. Not in force until you merge it into your active CODEOWNERS file. |
1268
1307
  | `.bridge/merge-workflow/REPOSITORY-RULES.md` | The setup steps, including the repository rule that actually enforces review. |
1269
1308
 
@@ -1326,13 +1365,13 @@ against it.
1326
1365
 
1327
1366
  #### Canonical template
1328
1367
 
1329
- Git blob SHA of the rendered workflow: `3d4956c9447223c37075069352cdd9aae2d561dc`
1368
+ Git blob SHA of the rendered workflow: `83089be700759e2e4d423ecedb7373829676ef39`
1330
1369
 
1331
1370
  ```yaml
1332
1371
  name: Epic Implementer merge
1333
1372
 
1334
1373
  env:
1335
- x-bridge-merge-contract-version: 1
1374
+ x-bridge-merge-contract-version: 2
1336
1375
 
1337
1376
  on:
1338
1377
  workflow_dispatch:
@@ -1349,6 +1388,14 @@ on:
1349
1388
  printed, never echoed, never written to an output.
1350
1389
  required: true
1351
1390
  type: string
1391
+ operation:
1392
+ description: >-
1393
+ Which job runs. `child_merge` (the default) merges an approved
1394
+ child pull request; `integration_action` performs one integration
1395
+ action. Selects a job only; the work itself is fetched from Bridge.
1396
+ required: false
1397
+ default: child_merge
1398
+ type: string
1352
1399
 
1353
1400
  permissions:
1354
1401
  contents: write
@@ -7,7 +7,8 @@ import { provisionExecutorDenyLayer } from "../executor/permissions.js";
7
7
  import { WORKER_GUARD_ROOT_FLAG } from "../executor/worker-guard-hook.js";
8
8
  import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
9
9
  import { normalizeMcpServerNames } from "../executor/observation.js";
10
- import { supported } from "./executor-adapter.js";
10
+ import { COMPLETION_EVIDENCE_TRANSPORT, supported } from "./executor-adapter.js";
11
+ import { SUPPORTED_COMPLETION_RUNTIME, boundedIdentifier, } from "../executor/completion-contract.js";
11
12
  import { MCP_SERVER_NAME } from "../mcp-identity.js";
12
13
  /** Stable identity of this reference implementation. */
13
14
  export const CLAUDE_ADAPTER_ID = "claude-reference";
@@ -210,8 +211,17 @@ export function resolveClaudeModelAlias(spec, payload) {
210
211
  * Every entry is pushed SEPARATELY and never joined into a command string, so a
211
212
  * prompt or path containing a space or a shell metacharacter is just a value.
212
213
  */
213
- export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
214
- const argv = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
214
+ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture, options = {}) {
215
+ // BAPI-1301 (D4a): the held-stdin form keeps `-p` (headless print mode) but
216
+ // puts NO prompt on argv — it arrives as the first stream-json user message on
217
+ // a held-open stdin, which is what lets the executor keep the worker alive
218
+ // until its required background work resolves. Everything after the input
219
+ // flags — output format, model, exactly one permission posture, strict MCP
220
+ // loading — is identical to the argv form. The default is byte-identical to the
221
+ // pre-BAPI-1301 golden shape.
222
+ const argv = options.promptTransport === "stdin-stream-json"
223
+ ? ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"]
224
+ : ["-p", prompt, "--output-format", "stream-json", "--verbose"];
215
225
  if (alias) {
216
226
  argv.push("--model", alias);
217
227
  }
@@ -219,6 +229,101 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
219
229
  argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
220
230
  return argv;
221
231
  }
232
+ // ---------------------------------------------------------------------------
233
+ // Held-stdin stream-json messages (BAPI-1301, D4a)
234
+ // ---------------------------------------------------------------------------
235
+ /**
236
+ * One stream-json user message as the exact NDJSON line written to the held
237
+ * stdin: `{"type":"user","message":{"role":"user","content":<text>}}\n`.
238
+ * `JSON.stringify` escapes every newline inside `text`, so one message is always
239
+ * exactly one line.
240
+ */
241
+ function claudeStdinUserLine(text) {
242
+ return `${JSON.stringify({ type: "user", message: { role: "user", content: text } })}\n`;
243
+ }
244
+ /** The prompt as the first held-stdin user message. */
245
+ export function buildClaudeStdinUserMessage(prompt) {
246
+ return claudeStdinUserLine(prompt);
247
+ }
248
+ /** At most this many task ids are named in a nudge. */
249
+ const NUDGE_MAX_TASK_IDS = 16;
250
+ /**
251
+ * The ONE optional-watcher nudge (BAPI-1301, D4a), sent when the worker produced
252
+ * its result while background tasks are still live.
253
+ *
254
+ * The wording is the safety property. It asks the worker to WAIT for tasks it
255
+ * needs and to stop ONLY an optional watcher it no longer needs — it never asks
256
+ * the worker to cancel, kill, or abandon required work to finish sooner, because
257
+ * a nudge that could talk a worker out of required work would turn the
258
+ * supervision meant to guarantee completion into a way to skip it. Task ids are
259
+ * passed through {@link boundedIdentifier}; anything that is not a safe opaque
260
+ * identifier is dropped rather than echoed.
261
+ */
262
+ export function buildClaudeWatcherNudgeMessage(liveTaskIds) {
263
+ const ids = liveTaskIds
264
+ .map((id) => boundedIdentifier(id))
265
+ .filter((id) => id !== null)
266
+ .slice(0, NUDGE_MAX_TASK_IDS);
267
+ const named = ids.length > 0 ? ` (task ids: ${ids.join(", ")})` : "";
268
+ return claudeStdinUserLine(`Your result was received, but background work you started is still running${named}. ` +
269
+ "If that work is part of this task, wait for it to finish and then report its outcome. " +
270
+ "If a task is only an optional watcher you no longer need (for example a monitor or a " +
271
+ "background log tail), you may stop that watcher. Do not cancel, kill, or abandon any " +
272
+ "required work in order to finish sooner.");
273
+ }
274
+ // ---------------------------------------------------------------------------
275
+ // Runtime identity probe (BAPI-1301, D4a)
276
+ // ---------------------------------------------------------------------------
277
+ /** Bound on the `--version` identity probe. */
278
+ export const CLAUDE_RUNTIME_PROBE_TIMEOUT_MS = 15_000;
279
+ /** Placeholder for a version the probe could not recognize — never matches. */
280
+ export const UNRECOGNIZED_CLI_VERSION = "unrecognized";
281
+ /**
282
+ * Parse `claude --version` output of the exact measured form
283
+ * `2.1.282 (Claude Code)` into `2.1.282`. Anything else is null — the raw text
284
+ * is never carried, because it would travel into logs and evidence.
285
+ */
286
+ export function parseClaudeCliVersion(stdout) {
287
+ const firstLine = (typeof stdout === "string" ? stdout : "").trim().split("\n")[0]?.trim() ?? "";
288
+ const match = /^(\d{1,4}\.\d{1,4}\.\d{1,6}) \(Claude Code\)$/.exec(firstLine);
289
+ return match ? match[1] : null;
290
+ }
291
+ /**
292
+ * The completion platform id for a host: `darwin` + `arm64` is `darwin-arm64`.
293
+ * Every other pair is rendered as `<os>-<arch>` over a closed character set, so
294
+ * it is bounded and can never equal the approved id by accident.
295
+ */
296
+ export function completionPlatformId(platform, arch) {
297
+ const clean = (value) => (typeof value === "string" ? value : "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 16) ||
298
+ "unknown";
299
+ return `${clean(platform)}-${clean(arch)}`;
300
+ }
301
+ /**
302
+ * Probe the installed Claude runtime's completion identity through the INJECTED
303
+ * command runner (the adapter never spawns on its own authority). Resolves null
304
+ * when the probe could not run or exited non-zero; otherwise an identity whose
305
+ * `cli_version` is the parsed version or {@link UNRECOGNIZED_CLI_VERSION}. The
306
+ * event schema is the one this adapter's lifecycle parser speaks. Never throws.
307
+ */
308
+ export async function probeClaudeCompletionRuntime(command, deps) {
309
+ let output;
310
+ try {
311
+ output = await deps.runCommand(command, ["--version"], {
312
+ timeoutMs: deps.timeoutMs ?? CLAUDE_RUNTIME_PROBE_TIMEOUT_MS,
313
+ });
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ if (!output || output.exitCode !== 0)
319
+ return null;
320
+ return {
321
+ adapter: SUPPORTED_COMPLETION_RUNTIME.adapter,
322
+ cli_version: parseClaudeCliVersion(output.stdout) ?? UNRECOGNIZED_CLI_VERSION,
323
+ platform: completionPlatformId(deps.platform ?? process.platform, deps.arch ?? process.arch),
324
+ event_schema: SUPPORTED_COMPLETION_RUNTIME.event_schema,
325
+ };
326
+ }
222
327
  /**
223
328
  * The single permission form for one posture — the whole policy→argv mapping,
224
329
  * in one exhaustive place.
@@ -348,6 +453,18 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
348
453
  if (input.declaredTouchedFiles !== undefined) {
349
454
  envOptions.declaredTouchedFiles = input.declaredTouchedFiles;
350
455
  }
456
+ // BAPI-1301 (D4a): the held-stdin transport is opted into per spawn. The
457
+ // argv form returns exactly the pre-BAPI-1301 three-key shape.
458
+ if (input.promptTransport === "stdin-stream-json") {
459
+ return {
460
+ executable: spec.command,
461
+ argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture, {
462
+ promptTransport: "stdin-stream-json",
463
+ }),
464
+ env: buildClaudeWorkerEnv(input.parentEnv, envOptions),
465
+ stdinMessages: { initial: buildClaudeStdinUserMessage(input.prompt) },
466
+ };
467
+ }
351
468
  return {
352
469
  executable: spec.command,
353
470
  argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
@@ -470,6 +587,19 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
470
587
  }
471
588
  },
472
589
  };
590
+ /**
591
+ * BAPI-1301 (D4a) — declared for exactly the measured identity (Claude Code
592
+ * 2.1.282, macOS arm64, the lifecycle event schema this adapter parses).
593
+ * Declaring it is not qualifying: preflight probes the installed runtime and
594
+ * advertises `completion_evidence.v1` only on an exact match, so a future CLI
595
+ * version or another platform stays unsupported until it is measured.
596
+ */
597
+ const completionEvidence = {
598
+ runtime: { ...SUPPORTED_COMPLETION_RUNTIME },
599
+ transport: COMPLETION_EVIDENCE_TRANSPORT,
600
+ probeRuntime: (probeDeps) => probeClaudeCompletionRuntime(spec.command, probeDeps),
601
+ buildWatcherNudge: buildClaudeWatcherNudgeMessage,
602
+ };
473
603
  return {
474
604
  identity: {
475
605
  agentId: spec.name,
@@ -488,5 +618,6 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
488
618
  redaction: supported(redaction),
489
619
  lifecycle: supported(lifecycle),
490
620
  advisoryMcpInspection: supported(advisoryMcpInspection),
621
+ completionEvidence: supported(completionEvidence),
491
622
  };
492
623
  }
@@ -8,6 +8,42 @@ export function unsupported(reason, strategyId) {
8
8
  ? { supported: false, reason }
9
9
  : { supported: false, reason, strategyId };
10
10
  }
11
+ // ---------------------------------------------------------------------------
12
+ // Completion evidence (job-specific, non-required) — BAPI-1301 (D4a)
13
+ // ---------------------------------------------------------------------------
14
+ /** The one held-stdin transport a completion-evidence adapter can declare. */
15
+ export const COMPLETION_EVIDENCE_TRANSPORT = "held-stdin-stream-json";
16
+ /**
17
+ * Validate an OPTIONAL completion-evidence declaration (BAPI-1301).
18
+ *
19
+ * Returns the usable capability or a bounded reason it cannot be used. Never a
20
+ * refusal of the adapter: an absent, unsupported, or malformed declaration only
21
+ * means the executor does not advertise `completion_evidence.v1`.
22
+ */
23
+ export function resolveCompletionEvidenceCapability(adapter) {
24
+ const declared = adapter.completionEvidence;
25
+ if (declared === undefined || declared === null) {
26
+ return { supported: false, reason: "adapter declares no completion-evidence capability" };
27
+ }
28
+ if (declared.supported !== true) {
29
+ const reason = typeof declared.reason === "string" && declared.reason.length > 0
30
+ ? declared.reason.slice(0, 200)
31
+ : "completion-evidence capability is declared unsupported";
32
+ return { supported: false, reason };
33
+ }
34
+ const value = declared.value;
35
+ const runtime = value?.runtime;
36
+ const wellFormedRuntime = !!runtime &&
37
+ ["adapter", "cli_version", "platform", "event_schema"].every((key) => typeof runtime[key] === "string" && runtime[key].length > 0);
38
+ if (!value ||
39
+ !wellFormedRuntime ||
40
+ value.transport !== COMPLETION_EVIDENCE_TRANSPORT ||
41
+ typeof value.probeRuntime !== "function" ||
42
+ typeof value.buildWatcherNudge !== "function") {
43
+ return { supported: false, reason: "completion-evidence capability declaration is malformed" };
44
+ }
45
+ return { supported: true, value: value };
46
+ }
11
47
  /**
12
48
  * The capability keys an adapter MUST supply as supported.
13
49
  *
@@ -93,10 +129,26 @@ export function validateExecutorAdapterCapabilities(adapter) {
93
129
  reason: null,
94
130
  });
95
131
  }
132
+ // BAPI-1301 (D4a): the job-specific completion-evidence capability is
133
+ // validated ONLY when declared, and its outcome never joins `refusals` — an
134
+ // adapter that cannot supervise required work is still a complete adapter for
135
+ // every other job. The key is added only when declared, so an adapter without
136
+ // it validates to exactly the pre-BAPI-1301 shape.
137
+ const optional = {};
138
+ if (adapter.completionEvidence !== undefined && adapter.completionEvidence !== null) {
139
+ const resolved = resolveCompletionEvidenceCapability(adapter);
140
+ optional.optionalCapabilities = [
141
+ {
142
+ capability: "completionEvidence",
143
+ supported: resolved.supported,
144
+ reason: resolved.supported ? null : resolved.reason,
145
+ },
146
+ ];
147
+ }
96
148
  if (refusals.length > 0) {
97
- return { ok: false, identity, refusals, capabilities };
149
+ return { ok: false, identity, refusals, capabilities, ...optional };
98
150
  }
99
- return { ok: true, identity, capabilities };
151
+ return { ok: true, identity, capabilities, ...optional };
100
152
  }
101
153
  /** Render a bounded, secret-free summary of why an adapter was refused. */
102
154
  export function formatAdapterRefusal(identity, refusals) {