@bridge_gpt/mcp-server 0.2.62 → 0.2.66

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 (113) hide show
  1. package/README.md +66 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +145 -20
  3. package/build/agent-launchers/executor-adapter.js +81 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/bridge-config.js +104 -0
  7. package/build/codex-bridge-profile.js +10 -1
  8. package/build/commands.generated.js +7 -6
  9. package/build/conductor/bridge-api-client.js +565 -32
  10. package/build/conductor/bring-up-facts.js +3 -1
  11. package/build/conductor/doctor.js +23 -14
  12. package/build/conductor/epic-child-continuation.js +351 -0
  13. package/build/conductor/epic-implementer-cli.js +154 -17
  14. package/build/conductor/git-hooks.js +529 -118
  15. package/build/conductor/guided-repair.js +323 -0
  16. package/build/conductor/hook-runtime-qualification.js +340 -0
  17. package/build/conductor/readiness-cli.js +30 -9
  18. package/build/conductor/readiness-sections.js +5 -4
  19. package/build/conductor/readiness.js +45 -12
  20. package/build/conductor/recovery-cli.js +859 -27
  21. package/build/conductor/recovery-operations.js +574 -5
  22. package/build/conductor/repair-contract.js +69 -3
  23. package/build/conductor-bundle-artifacts.js +1 -0
  24. package/build/credential-store.js +52 -13
  25. package/build/diagnostic-text.js +36 -0
  26. package/build/docs.generated.js +3 -2
  27. package/build/doctor.js +147 -21
  28. package/build/drive-epic-key.js +12 -0
  29. package/build/drive-epic.js +1537 -125
  30. package/build/epic-coherence-input-cli.js +137 -0
  31. package/build/epic-coherence-input.js +920 -0
  32. package/build/epic-coherence-input.test-fixtures.js +175 -0
  33. package/build/epic-implementer-bin.js +41 -33
  34. package/build/epic-implementer-bundle-cli.js +14 -1
  35. package/build/epic-launch-diagnostic.js +31 -0
  36. package/build/epic-launch-profile.js +58 -19
  37. package/build/epic-run-status.js +5 -0
  38. package/build/epic-shared/bridge-client.js +46 -4
  39. package/build/executor/cli.js +39 -0
  40. package/build/executor/completion-contract.js +423 -0
  41. package/build/executor/completion-supervision.js +291 -0
  42. package/build/executor/correction-dispatch.js +60 -0
  43. package/build/executor/credentials.js +3 -1
  44. package/build/executor/deps.js +65 -1
  45. package/build/executor/env.js +12 -0
  46. package/build/executor/heartbeat.js +13 -3
  47. package/build/executor/http-client.js +91 -4
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +61 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +1550 -55
  52. package/build/executor/job-types.js +9 -6
  53. package/build/executor/preflight.js +189 -1
  54. package/build/executor/process-containment.js +756 -0
  55. package/build/executor/process.js +235 -5
  56. package/build/executor/prompt-spec.js +366 -1
  57. package/build/executor/provider-checks.js +69 -0
  58. package/build/executor/rebase-finalization.js +351 -0
  59. package/build/executor/reconcile-overlap-job.js +77 -84
  60. package/build/executor/required-verification.js +313 -0
  61. package/build/executor/required-work-ledger.js +1121 -0
  62. package/build/executor/results.js +142 -0
  63. package/build/executor/resume-pre-spawn.js +87 -4
  64. package/build/executor/runner.js +168 -0
  65. package/build/executor/stale-artifacts.js +5 -4
  66. package/build/executor/test-declaration.js +20 -0
  67. package/build/executor/types.js +13 -0
  68. package/build/executor/watch-cli.js +35 -8
  69. package/build/executor/worker-command.js +70 -34
  70. package/build/executor/worker-finalization.js +168 -26
  71. package/build/executor/worker-guard-hook.js +75 -14
  72. package/build/executor/worker-log.js +137 -70
  73. package/build/executor/worktree-gc.js +30 -6
  74. package/build/executor/worktree.js +13 -0
  75. package/build/executor-launch.js +658 -0
  76. package/build/index.js +594 -1152
  77. package/build/init.js +107 -4
  78. package/build/install-bridge-conductor.js +231 -41
  79. package/build/install-bridge.js +284 -20
  80. package/build/install-doctor.js +172 -30
  81. package/build/install-reexec.js +69 -7
  82. package/build/macos-terminal-permission-guidance.js +16 -0
  83. package/build/merge-pull-request.js +1 -1
  84. package/build/model-thresholds-tool.js +185 -0
  85. package/build/pipelines.generated.js +9 -8
  86. package/build/plan-discovery.js +137 -0
  87. package/build/plan-epic-implementer-eligibility.js +2 -2
  88. package/build/plane/cli.js +37 -3
  89. package/build/plane/member-logs.js +3 -0
  90. package/build/plane/member-roster.js +7 -1
  91. package/build/plane/shutdown.js +59 -1
  92. package/build/plane/status.js +4 -0
  93. package/build/plane/supervisor.js +146 -0
  94. package/build/plane/worker-exit-code.js +2 -0
  95. package/build/project-root.js +99 -0
  96. package/build/readme.generated.js +1 -1
  97. package/build/review-tickets.js +156 -13
  98. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  99. package/build/setup-epic.js +208 -125
  100. package/build/sfcc/setup-status.js +2 -0
  101. package/build/sfcc/tool-wrapper.js +2 -0
  102. package/build/sfcc/tools-configured.js +96 -0
  103. package/build/start-tickets.js +163 -58
  104. package/build/upgrade-cli.js +109 -24
  105. package/build/version.generated.js +3 -3
  106. package/build/windows-npm-entry.js +103 -0
  107. package/build/worker-guard-hook-bin.js +5 -4
  108. package/build/worktree-core.js +6 -5
  109. package/docs/CONDUCTOR.md +116 -2
  110. package/docs/install/mcp-tool-integrations.md +1 -0
  111. package/docs/install/sfcc-integration.md +12 -7
  112. package/package.json +2 -1
  113. package/pipelines/implement-ticket.json +1 -1
package/README.md CHANGED
@@ -252,9 +252,13 @@ workflows. Bridge-spawned Codex workers then select it automatically, including
252
252
  the fresh implementation session after review. Pass `--codex-profile=off` to
253
253
  decline; the installer leaves any existing profile content intact. On package
254
254
  upgrade, the next Bridge-spawned worker refreshes an older, unmodified managed
255
- profile before launch. If a profile
256
- already contains user-authored or conflicting content, the installer leaves it
257
- untouched and reports that workers cannot use it. Run `doctor --agent codex` to
255
+ profile before launch. The profile preapproves review-decision capture, the
256
+ fixed reviewed-document publisher, and the implementation phase-result record,
257
+ so those required Bridge writes do not enter Codex Auto-review. The general
258
+ `attachment` tool stays on automatic review because it also supports deletion
259
+ and replacement. If a profile already contains user-authored or conflicting
260
+ content, the installer leaves it untouched and reports that workers cannot use
261
+ it. Run `doctor --agent codex` to
258
262
  inspect the current status. To use the profile in a Codex session you open
259
263
  yourself, run `codex --profile bridge`. Bridge `--auto` controls recipe gates;
260
264
  Codex MCP tool approvals are a separate setting. The profile does not change
@@ -369,7 +373,7 @@ later boot.
369
373
  "mcpServers": {
370
374
  "bridge": {
371
375
  "command": "npx",
372
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"],
376
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"],
373
377
  "env": {
374
378
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
375
379
  "BAPI_REPO_NAME": "your-repo",
@@ -391,7 +395,7 @@ later boot.
391
395
  "bridge": {
392
396
  "type": "stdio",
393
397
  "command": "npx",
394
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"],
398
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"],
395
399
  "env": {
396
400
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
397
401
  "BAPI_REPO_NAME": "your-repo",
@@ -413,7 +417,7 @@ later boot.
413
417
  "bridge": {
414
418
  "type": "stdio",
415
419
  "command": "npx",
416
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"],
420
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"],
417
421
  "env": {
418
422
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
419
423
  "BAPI_REPO_NAME": "your-repo",
@@ -436,7 +440,7 @@ Windsurf only supports global MCP configuration.
436
440
  "mcpServers": {
437
441
  "bridge": {
438
442
  "command": "npx",
439
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"],
443
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"],
440
444
  "env": {
441
445
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
442
446
  "BAPI_REPO_NAME": "your-repo",
@@ -455,7 +459,7 @@ Windsurf only supports global MCP configuration.
455
459
  ```toml
456
460
  [mcp_servers.bridge]
457
461
  command = "npx"
458
- args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"]
462
+ args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"]
459
463
 
460
464
  [mcp_servers.bridge.env]
461
465
  BAPI_BASE_URL = "https://bridgegpt-api.com"
@@ -893,7 +897,7 @@ Because tier 2 outranks tier 3, a stale `SFCC_HOSTNAME` left in the environment
893
897
  "env": { "BRIDGE_MCP_PROFILE": "sfcc" }
894
898
  ```
895
899
 
896
- `sfcc` gives the eight read tools plus `sfcc_log_query`. For the nine destructive write tools as well, use `"sfcc,sfcc-write"`; `full` expands to every group and is therefore write-capable. Without any of these, only the diagnostic tools are registered.
900
+ `sfcc` gives the two diagnostics, the eight read tools, and `sfcc_log_query`. For the nine destructive write tools as well, use `"sfcc,sfcc-write"`; `full` expands to every group and is therefore write-capable. Without any of these, no SFCC tool is registered — not even the diagnostics.
897
901
 
898
902
  **4. Verify and grant OCAPI access.** With `sfcc` selected in step 3 (both diagnostics live in that profile), ask your agent to run `sfcc_setup_status` (expect all checks ✓), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration → Site Development → Open Commerce API Settings → Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change — a running session does not pick them up.
899
903
 
@@ -1049,7 +1053,7 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
1049
1053
  npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
1050
1054
  ```
1051
1055
 
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.
1056
+ **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
1057
 
1054
1058
  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
1059
 
@@ -1128,6 +1132,51 @@ including every error path, ever offers you two paths.
1128
1132
  the strategy and the posture (see below). With none of them, a multi-ticket epic
1129
1133
  runs on `epic/<KEY>`, unattended.
1130
1134
 
1135
+ **Finding the plan from the epic key (BAPI-1285).** `--plan-file` is optional.
1136
+ Without it, `drive-epic` searches `{docs_dir}/epic-plans/*/epic-plan.meta.json` for a
1137
+ file whose `epic_key` is exactly this epic (`docs_dir` is `BAPI_DOCS_DIR`, else
1138
+ `docs/tmp`, resolved against the Git top level; an absolute `BAPI_DOCS_DIR` is used
1139
+ as given). Exactly one match with a sibling `epic-plan.dag.json` is used as if you
1140
+ had passed `--plan-file`, and its path is printed before anything is created. Zero
1141
+ matches, two or more, or an unusable meta file **stop** with the paths found and
1142
+ create nothing, and a folder name, a child ticket key, or
1143
+ `sibling-ticket-manifest.json` is never used to guess. A supplied `--plan-file`
1144
+ always wins and discovery does not run.
1145
+
1146
+ The meta file is `{"epic_key": "<KEY>"}`, written by `emit-epic-implementer-bundle
1147
+ finalize` beside the DAG (its own atomic write, reported as `meta_path` in
1148
+ `--json`). It is separate from `epic-plan.dag.json` on purpose: the DAG is sent to
1149
+ the server, which rejects unknown fields. A plan finalized before this change has no
1150
+ meta file, so it is reported as not found — pass its DAG with `--plan-file`.
1151
+
1152
+ **A live run with no plan.** If the epic already has a live run and no plan was
1153
+ supplied or found, `drive-epic` creates nothing. It makes sure that run has an
1154
+ executor: it reuses a verified live one (never starting a second), starts one when
1155
+ the old one is gone, starts none for a durable launch profile with a confirmed
1156
+ repo-wide executor, and starts none when this checkout's plane is bound to that
1157
+ run. An epic with several active runs (`live-run-ambiguous`), or a run that cannot
1158
+ be read (`live-run-unknown`), is refused by name.
1159
+
1160
+ **Preview with `--dry-run`.** `drive-epic <EPIC> --dry-run` is read-only. It reads
1161
+ the target, readiness, the live run, and the plan, runs the bootstrap's own dry-run
1162
+ validation when a plan is available, and reports the decision a real invocation
1163
+ would make — `create`, `attach`, `refuse`, or `stop` (exit `0` for the first two,
1164
+ `1` for the others). It never creates a run, cuts a branch, starts a plane,
1165
+ requests a lane, or starts or signals an executor. With `--json` (valid only with
1166
+ `--dry-run`) stdout carries exactly one JSON document with `epic_key`, `target`,
1167
+ `readiness`, `live_run`, `plan`, `summary`, `decision`, and `refusal`. The
1168
+ readiness `reason_id` and `remedy` are the strings the real refusal prints. The
1169
+ webhook URL, credentials, and policy contents are never included. The preview does
1170
+ probes only a selected loopback plane after a network readiness failure and an
1171
+ available plan. A refused port plus passing local preflight yields a conditional
1172
+ `create` with optional `cold_start` (`plane_endpoint`, `will_start_plane: true`,
1173
+ `live_run_checked: false`), `live_run: null`, and partial offline setup validation.
1174
+ The real launch attempts guarded startup, then rechecks run existence and server
1175
+ validation; the preview does not guarantee creation. Remote targets, non-refused
1176
+ port probes, and ordinary production previews keep their existing refusal or
1177
+ create behavior. `target.source` is optional and appears only for selected
1178
+ profiles (`profile` or `profile-env`).
1179
+
1131
1180
  **The composed bring-up is two-phase (BAPI-1102).** With `--plan-file` on a
1132
1181
  repository whose readiness is missing only the runtime facts, one invocation runs,
1133
1182
  in order: the **control plane** (Bridge API server + reconciler, each gated on its
@@ -1209,8 +1258,10 @@ default, or, when it declares none, as an explicit recorded `no_ci_gate` mode.
1209
1258
  > combining `--attended` with a file that declares `posture: "unattended"` is a named
1210
1259
  > refusal rather than a silent winner.
1211
1260
 
1212
- An unattended run has two one-time **repository** prerequisites — consent
1213
- (`unattended_epic_implementer_allowed`) and a verified notify webhook default — plus
1261
+ An unattended run has two **repository** prerequisites — consent
1262
+ (`unattended_epic_implementer_allowed`, ON by default; the repository owner can switch
1263
+ "Allow unattended Epic Implementer runs" off and on in the Bridge admin panel's Setup
1264
+ page) and a verified notify webhook default — plus
1214
1265
  confirmed default-branch review/CI workflows. Each is refused by name **before any
1215
1266
  run row is created** (`target_not_allowed`, `webhook_unverified`,
1216
1267
  `repository_readiness_unconfirmed`), and `drive-epic` readiness and `doctor` both
@@ -1417,7 +1468,7 @@ If a custom pipeline has the same key as a built-in pipeline, the custom version
1417
1468
  | `BAPI_BASE_URL` (`executor` subcommand) | **Yes** | _(none)_ | The `executor` deliberately has **no** production fallback — it refuses to start rather than guess a target |
1418
1469
  | `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |
1419
1470
  | `BAPI_API_KEY` | No | _(none)_ | A Bridge credential **is** required; this environment variable is only the first place the server looks for it. When it is unset the server resolves the credential from the user-scoped store (`~/.config/bridge/credentials.json`, target `bapi:<repo>`), which is why generated MCP registrations are secret-free |
1420
- | `BAPI_PROJECT_ROOT` | No | _(see fallback order)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution. Resolved once, in order: `BAPI_PROJECT_ROOT` → the connected client's MCP `roots/list` → `CLAUDE_PROJECT_DIR` → `process.cwd()`. Several paths *write* it into a generated registration (`--init`, host-config provisioning, the worktree `mcp-invoke` shim) — that is provenance, not a runtime default |
1471
+ | `BAPI_PROJECT_ROOT` | No | _(see fallback order)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution. Resolved once, in order: `BAPI_PROJECT_ROOT` → the connected client's MCP `roots/list` → `CLAUDE_PROJECT_DIR` → `process.cwd()`. The explicit value is validated: it is used only when it exists, is a folder, and contains `.git` (a folder, or a worktree's `.git` file) or `.bridge/config`. Otherwise it is ignored and the next source is used, so a path committed from another machine is harmless. The later sources are not validated. The server logs the chosen root and its source to stderr once (for example `Bridge MCP: project root /home/b/app (from roots/list); BAPI_PROJECT_ROOT /Users/a/app ignored: does not exist`), and `doctor` reports whether each configured value is valid. The check proves the path looks like a project, not that it is *this* project. Several paths *write* it into a generated registration (`--init`, `install`, `upgrade`, host-config provisioning, the worktree `mcp-invoke` shim) — that is provenance, not a runtime default; a git-tracked config keeps its existing value. What each host passes at start: see the per-host table in `docs/claude/cli-install-architecture.md` (§5a) |
1421
1472
  | `SFCC_HOSTNAME` | No | _(none)_ | SFCC sandbox hostname. Part of the environment credential tier — `SFCC_HOSTNAME`, `SFCC_CLIENT_ID`, and `SFCC_CLIENT_SECRET` must **all three** be set for that tier to apply, and a complete tier takes precedence over `dw.json` |
1422
1473
  | `SFCC_CLIENT_ID` | No | _(none)_ | Account Manager API client id. See `SFCC_HOSTNAME` — all three are needed together. Also required on its own when a tool is called with an explicit dotted `instance` |
1423
1474
  | `SFCC_CLIENT_SECRET` | No | _(none)_ | Account Manager API client secret. See `SFCC_HOSTNAME` — all three are needed together. Never sent to Bridge; it goes only to the Account Manager token endpoint |
@@ -1691,7 +1742,7 @@ The full surface, for when you need the complete enumeration. Day-to-day, use [U
1691
1742
 
1692
1743
  ### MCP tools
1693
1744
 
1694
- The authoritative tool catalog covers **77 tools** (enumerated below). What's actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).
1745
+ The authoritative tool catalog covers **78 tools** (enumerated below). What's actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).
1695
1746
 
1696
1747
  - **Connectivity & identity** — `ping` (its JSON also carries `docs_dir`, `role`, and `customer_type`)
1697
1748
  - **Team & access** — `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project — the plaintext key is shown exactly once)
@@ -1704,7 +1755,7 @@ The authoritative tool catalog covers **77 tools** (enumerated below). What's ac
1704
1755
  - **Jira status** — `update_jira_status` (pass `status: "auto"` to resolve the configured post-PR status server-side)
1705
1756
  - **Repository & CI** — `parse_repository` (`action`: `start`, `status`), `regenerate_directory_map`, `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `wait_for_ci_checks` (the last two are hidden until `ci_check_config` resolves, and share one visibility gate)
1706
1757
  - **Pipelines & automation** — `get_pipeline_recipe` (returns a fully resolved recipe the agent executes step-by-step)
1707
- - **Config** — `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)
1758
+ - **Config** — `get_project_standards`, `config_field` (operations: `get`, `update`, `list`), `model_thresholds` (actions: `get`, `set`, `clear`; the only way to set personal thresholds)
1708
1759
 
1709
1760
  ### Bundled pipelines
1710
1761
 
@@ -1,4 +1,3 @@
1
- import { isModelTier, isValidModelAlias, resolveModelAlias } from "../agent-registry.js";
2
1
  import { evaluateClaudeMcpShadowingPolicy, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "../claude-user-config-doctor.js";
3
2
  import { runDenyEnforcementPreflight } from "../conductor/deny-enforcement-preflight.js";
4
3
  import { buildExecutorBaseWorkerEnv } from "../executor/env.js";
@@ -7,7 +6,8 @@ import { provisionExecutorDenyLayer } from "../executor/permissions.js";
7
6
  import { WORKER_GUARD_ROOT_FLAG } from "../executor/worker-guard-hook.js";
8
7
  import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
9
8
  import { normalizeMcpServerNames } from "../executor/observation.js";
10
- import { supported } from "./executor-adapter.js";
9
+ import { COMPLETION_EVIDENCE_TRANSPORT, resolveExecutorModelAlias, supported, } from "./executor-adapter.js";
10
+ import { SUPPORTED_COMPLETION_RUNTIME, boundedIdentifier, } from "../executor/completion-contract.js";
11
11
  import { MCP_SERVER_NAME } from "../mcp-identity.js";
12
12
  /** Stable identity of this reference implementation. */
13
13
  export const CLAUDE_ADAPTER_ID = "claude-reference";
@@ -158,21 +158,12 @@ export function classifyClaudeAuthFailure(stdoutExcerpt) {
158
158
  * when it passes both registry alias validation AND the spec's static allowlist;
159
159
  * anything else falls through to tier resolution, and an unusable result is null
160
160
  * (meaning "omit `--model`") rather than an unvalidated passthrough.
161
+ *
162
+ * BAPI-1343: the rule now lives in the agent-neutral
163
+ * {@link resolveExecutorModelAlias}; this export delegates with no behavior change.
161
164
  */
162
165
  export function resolveClaudeModelAlias(spec, payload) {
163
- const p = payload && typeof payload === "object" ? payload : {};
164
- const directRaw = p.model_alias;
165
- if (typeof directRaw === "string" && directRaw.trim().length > 0) {
166
- const direct = directRaw.trim();
167
- const allowed = !spec.staticModelAliasAllowlist || spec.staticModelAliasAllowlist.includes(direct);
168
- if (isValidModelAlias(direct) && allowed) {
169
- return direct;
170
- }
171
- // An invalid/disallowed direct alias falls through to the tier path — never
172
- // returned as-is.
173
- }
174
- const tier = isModelTier(p.model_tier) ? p.model_tier : null;
175
- return resolveModelAlias(spec, tier, null);
166
+ return resolveExecutorModelAlias(spec, payload);
176
167
  }
177
168
  // ---------------------------------------------------------------------------
178
169
  // Spawn shape
@@ -210,8 +201,17 @@ export function resolveClaudeModelAlias(spec, payload) {
210
201
  * Every entry is pushed SEPARATELY and never joined into a command string, so a
211
202
  * prompt or path containing a space or a shell metacharacter is just a value.
212
203
  */
213
- export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
214
- const argv = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
204
+ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture, options = {}) {
205
+ // BAPI-1301 (D4a): the held-stdin form keeps `-p` (headless print mode) but
206
+ // puts NO prompt on argv — it arrives as the first stream-json user message on
207
+ // a held-open stdin, which is what lets the executor keep the worker alive
208
+ // until its required background work resolves. Everything after the input
209
+ // flags — output format, model, exactly one permission posture, strict MCP
210
+ // loading — is identical to the argv form. The default is byte-identical to the
211
+ // pre-BAPI-1301 golden shape.
212
+ const argv = options.promptTransport === "stdin-stream-json"
213
+ ? ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"]
214
+ : ["-p", prompt, "--output-format", "stream-json", "--verbose"];
215
215
  if (alias) {
216
216
  argv.push("--model", alias);
217
217
  }
@@ -219,6 +219,101 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
219
219
  argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
220
220
  return argv;
221
221
  }
222
+ // ---------------------------------------------------------------------------
223
+ // Held-stdin stream-json messages (BAPI-1301, D4a)
224
+ // ---------------------------------------------------------------------------
225
+ /**
226
+ * One stream-json user message as the exact NDJSON line written to the held
227
+ * stdin: `{"type":"user","message":{"role":"user","content":<text>}}\n`.
228
+ * `JSON.stringify` escapes every newline inside `text`, so one message is always
229
+ * exactly one line.
230
+ */
231
+ function claudeStdinUserLine(text) {
232
+ return `${JSON.stringify({ type: "user", message: { role: "user", content: text } })}\n`;
233
+ }
234
+ /** The prompt as the first held-stdin user message. */
235
+ export function buildClaudeStdinUserMessage(prompt) {
236
+ return claudeStdinUserLine(prompt);
237
+ }
238
+ /** At most this many task ids are named in a nudge. */
239
+ const NUDGE_MAX_TASK_IDS = 16;
240
+ /**
241
+ * The ONE optional-watcher nudge (BAPI-1301, D4a), sent when the worker produced
242
+ * its result while background tasks are still live.
243
+ *
244
+ * The wording is the safety property. It asks the worker to WAIT for tasks it
245
+ * needs and to stop ONLY an optional watcher it no longer needs — it never asks
246
+ * the worker to cancel, kill, or abandon required work to finish sooner, because
247
+ * a nudge that could talk a worker out of required work would turn the
248
+ * supervision meant to guarantee completion into a way to skip it. Task ids are
249
+ * passed through {@link boundedIdentifier}; anything that is not a safe opaque
250
+ * identifier is dropped rather than echoed.
251
+ */
252
+ export function buildClaudeWatcherNudgeMessage(liveTaskIds) {
253
+ const ids = liveTaskIds
254
+ .map((id) => boundedIdentifier(id))
255
+ .filter((id) => id !== null)
256
+ .slice(0, NUDGE_MAX_TASK_IDS);
257
+ const named = ids.length > 0 ? ` (task ids: ${ids.join(", ")})` : "";
258
+ return claudeStdinUserLine(`Your result was received, but background work you started is still running${named}. ` +
259
+ "If that work is part of this task, wait for it to finish and then report its outcome. " +
260
+ "If a task is only an optional watcher you no longer need (for example a monitor or a " +
261
+ "background log tail), you may stop that watcher. Do not cancel, kill, or abandon any " +
262
+ "required work in order to finish sooner.");
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Runtime identity probe (BAPI-1301, D4a)
266
+ // ---------------------------------------------------------------------------
267
+ /** Bound on the `--version` identity probe. */
268
+ export const CLAUDE_RUNTIME_PROBE_TIMEOUT_MS = 15_000;
269
+ /** Placeholder for a version the probe could not recognize — never matches. */
270
+ export const UNRECOGNIZED_CLI_VERSION = "unrecognized";
271
+ /**
272
+ * Parse `claude --version` output of the exact measured form
273
+ * `2.1.282 (Claude Code)` into `2.1.282`. Anything else is null — the raw text
274
+ * is never carried, because it would travel into logs and evidence.
275
+ */
276
+ export function parseClaudeCliVersion(stdout) {
277
+ const firstLine = (typeof stdout === "string" ? stdout : "").trim().split("\n")[0]?.trim() ?? "";
278
+ const match = /^(\d{1,4}\.\d{1,4}\.\d{1,6}) \(Claude Code\)$/.exec(firstLine);
279
+ return match ? match[1] : null;
280
+ }
281
+ /**
282
+ * The completion platform id for a host: `darwin` + `arm64` is `darwin-arm64`.
283
+ * Every other pair is rendered as `<os>-<arch>` over a closed character set, so
284
+ * it is bounded and can never equal the approved id by accident.
285
+ */
286
+ export function completionPlatformId(platform, arch) {
287
+ const clean = (value) => (typeof value === "string" ? value : "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 16) ||
288
+ "unknown";
289
+ return `${clean(platform)}-${clean(arch)}`;
290
+ }
291
+ /**
292
+ * Probe the installed Claude runtime's completion identity through the INJECTED
293
+ * command runner (the adapter never spawns on its own authority). Resolves null
294
+ * when the probe could not run or exited non-zero; otherwise an identity whose
295
+ * `cli_version` is the parsed version or {@link UNRECOGNIZED_CLI_VERSION}. The
296
+ * event schema is the one this adapter's lifecycle parser speaks. Never throws.
297
+ */
298
+ export async function probeClaudeCompletionRuntime(command, deps) {
299
+ let output;
300
+ try {
301
+ output = await deps.runCommand(command, ["--version"], {
302
+ timeoutMs: deps.timeoutMs ?? CLAUDE_RUNTIME_PROBE_TIMEOUT_MS,
303
+ });
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ if (!output || output.exitCode !== 0)
309
+ return null;
310
+ return {
311
+ adapter: SUPPORTED_COMPLETION_RUNTIME.adapter,
312
+ cli_version: parseClaudeCliVersion(output.stdout) ?? UNRECOGNIZED_CLI_VERSION,
313
+ platform: completionPlatformId(deps.platform ?? process.platform, deps.arch ?? process.arch),
314
+ event_schema: SUPPORTED_COMPLETION_RUNTIME.event_schema,
315
+ };
316
+ }
222
317
  /**
223
318
  * The single permission form for one posture — the whole policy→argv mapping,
224
319
  * in one exhaustive place.
@@ -306,9 +401,10 @@ function shellQuoteHookPath(value) {
306
401
  * where the executor's env deny-list can guarantee an ambient value never
307
402
  * substitutes for the job's own.
308
403
  */
309
- export function buildWorkerGuardHookCommand(binPath, execPath, worktreePath) {
404
+ export function buildWorkerGuardHookCommand(binPath, execPath, worktreePath, correctionIntentVersion) {
310
405
  return (`${shellQuoteHookPath(execPath)} ${shellQuoteHookPath(binPath)} ` +
311
- `${WORKER_GUARD_ROOT_FLAG} ${shellQuoteHookPath(worktreePath)}`);
406
+ `${WORKER_GUARD_ROOT_FLAG} ${shellQuoteHookPath(worktreePath)}` +
407
+ (correctionIntentVersion === 1 ? " --correction-intent-version 1" : ""));
312
408
  }
313
409
  // ---------------------------------------------------------------------------
314
410
  // The adapter factory
@@ -345,9 +441,24 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
345
441
  if (input.workerBranch !== undefined) {
346
442
  envOptions.workerBranch = input.workerBranch;
347
443
  }
444
+ if (input.correctionIntentVersion !== undefined) {
445
+ envOptions.correctionIntentVersion = input.correctionIntentVersion;
446
+ }
348
447
  if (input.declaredTouchedFiles !== undefined) {
349
448
  envOptions.declaredTouchedFiles = input.declaredTouchedFiles;
350
449
  }
450
+ // BAPI-1301 (D4a): the held-stdin transport is opted into per spawn. The
451
+ // argv form returns exactly the pre-BAPI-1301 three-key shape.
452
+ if (input.promptTransport === "stdin-stream-json") {
453
+ return {
454
+ executable: spec.command,
455
+ argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture, {
456
+ promptTransport: "stdin-stream-json",
457
+ }),
458
+ env: buildClaudeWorkerEnv(input.parentEnv, envOptions),
459
+ stdinMessages: { initial: buildClaudeStdinUserMessage(input.prompt) },
460
+ };
461
+ }
351
462
  return {
352
463
  executable: spec.command,
353
464
  argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
@@ -405,7 +516,7 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
405
516
  }
406
517
  const options = {
407
518
  baseBranch: input.baseBranch,
408
- workerGuardHookCommand: buildWorkerGuardHookCommand(input.workerGuardHookBinPath, deps.execPath ?? process.execPath, input.worktreePath),
519
+ workerGuardHookCommand: buildWorkerGuardHookCommand(input.workerGuardHookBinPath, deps.execPath ?? process.execPath, input.worktreePath, input.correctionIntentVersion),
409
520
  };
410
521
  if (input.workerBranch !== undefined) {
411
522
  options.workerBranch = input.workerBranch;
@@ -470,6 +581,19 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
470
581
  }
471
582
  },
472
583
  };
584
+ /**
585
+ * BAPI-1301 (D4a) — declared for exactly the measured identity (Claude Code
586
+ * 2.1.282, macOS arm64, the lifecycle event schema this adapter parses).
587
+ * Declaring it is not qualifying: preflight probes the installed runtime and
588
+ * advertises `completion_evidence.v1` only on an exact match, so a future CLI
589
+ * version or another platform stays unsupported until it is measured.
590
+ */
591
+ const completionEvidence = {
592
+ runtime: { ...SUPPORTED_COMPLETION_RUNTIME },
593
+ transport: COMPLETION_EVIDENCE_TRANSPORT,
594
+ probeRuntime: (probeDeps) => probeClaudeCompletionRuntime(spec.command, probeDeps),
595
+ buildWatcherNudge: buildClaudeWatcherNudgeMessage,
596
+ };
473
597
  return {
474
598
  identity: {
475
599
  agentId: spec.name,
@@ -488,5 +612,6 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
488
612
  redaction: supported(redaction),
489
613
  lifecycle: supported(lifecycle),
490
614
  advisoryMcpInspection: supported(advisoryMcpInspection),
615
+ completionEvidence: supported(completionEvidence),
491
616
  };
492
617
  }
@@ -1,3 +1,4 @@
1
+ import { isModelTier, isValidModelAlias, resolveModelAlias } from "../agent-registry.js";
1
2
  /** Construct a supported capability. */
2
3
  export function supported(value) {
3
4
  return { supported: true, value };
@@ -8,6 +9,68 @@ export function unsupported(reason, strategyId) {
8
9
  ? { supported: false, reason }
9
10
  : { supported: false, reason, strategyId };
10
11
  }
12
+ /**
13
+ * Agent-neutral payload -> model alias rule shared by every executor adapter
14
+ * (BAPI-1343).
15
+ *
16
+ * A direct `model_alias` is accepted only when it passes registry alias
17
+ * validation AND the spec's static allowlist. Otherwise a stamped `model_tier`
18
+ * maps through {@link resolveModelAlias} with no repository override. Anything
19
+ * unusable returns null, meaning "omit the model flag". There is deliberately no
20
+ * job-type branch: the server decides whether to stamp a tier, and every stamped
21
+ * payload takes the same path.
22
+ */
23
+ export function resolveExecutorModelAlias(spec, payload) {
24
+ const p = payload && typeof payload === "object" ? payload : {};
25
+ const directRaw = p.model_alias;
26
+ if (typeof directRaw === "string" && directRaw.trim().length > 0) {
27
+ const direct = directRaw.trim();
28
+ const allowed = !spec.staticModelAliasAllowlist || spec.staticModelAliasAllowlist.includes(direct);
29
+ if (isValidModelAlias(direct) && allowed) {
30
+ return direct;
31
+ }
32
+ // An invalid/disallowed direct alias falls through to the tier path — never
33
+ // returned as-is.
34
+ }
35
+ const tier = isModelTier(p.model_tier) ? p.model_tier : null;
36
+ return resolveModelAlias(spec, tier, null);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Completion evidence (job-specific, non-required) — BAPI-1301 (D4a)
40
+ // ---------------------------------------------------------------------------
41
+ /** The one held-stdin transport a completion-evidence adapter can declare. */
42
+ export const COMPLETION_EVIDENCE_TRANSPORT = "held-stdin-stream-json";
43
+ /**
44
+ * Validate an OPTIONAL completion-evidence declaration (BAPI-1301).
45
+ *
46
+ * Returns the usable capability or a bounded reason it cannot be used. Never a
47
+ * refusal of the adapter: an absent, unsupported, or malformed declaration only
48
+ * means the executor does not advertise `completion_evidence.v1`.
49
+ */
50
+ export function resolveCompletionEvidenceCapability(adapter) {
51
+ const declared = adapter.completionEvidence;
52
+ if (declared === undefined || declared === null) {
53
+ return { supported: false, reason: "adapter declares no completion-evidence capability" };
54
+ }
55
+ if (declared.supported !== true) {
56
+ const reason = typeof declared.reason === "string" && declared.reason.length > 0
57
+ ? declared.reason.slice(0, 200)
58
+ : "completion-evidence capability is declared unsupported";
59
+ return { supported: false, reason };
60
+ }
61
+ const value = declared.value;
62
+ const runtime = value?.runtime;
63
+ const wellFormedRuntime = !!runtime &&
64
+ ["adapter", "cli_version", "platform", "event_schema"].every((key) => typeof runtime[key] === "string" && runtime[key].length > 0);
65
+ if (!value ||
66
+ !wellFormedRuntime ||
67
+ value.transport !== COMPLETION_EVIDENCE_TRANSPORT ||
68
+ typeof value.probeRuntime !== "function" ||
69
+ typeof value.buildWatcherNudge !== "function") {
70
+ return { supported: false, reason: "completion-evidence capability declaration is malformed" };
71
+ }
72
+ return { supported: true, value: value };
73
+ }
11
74
  /**
12
75
  * The capability keys an adapter MUST supply as supported.
13
76
  *
@@ -93,10 +156,26 @@ export function validateExecutorAdapterCapabilities(adapter) {
93
156
  reason: null,
94
157
  });
95
158
  }
159
+ // BAPI-1301 (D4a): the job-specific completion-evidence capability is
160
+ // validated ONLY when declared, and its outcome never joins `refusals` — an
161
+ // adapter that cannot supervise required work is still a complete adapter for
162
+ // every other job. The key is added only when declared, so an adapter without
163
+ // it validates to exactly the pre-BAPI-1301 shape.
164
+ const optional = {};
165
+ if (adapter.completionEvidence !== undefined && adapter.completionEvidence !== null) {
166
+ const resolved = resolveCompletionEvidenceCapability(adapter);
167
+ optional.optionalCapabilities = [
168
+ {
169
+ capability: "completionEvidence",
170
+ supported: resolved.supported,
171
+ reason: resolved.supported ? null : resolved.reason,
172
+ },
173
+ ];
174
+ }
96
175
  if (refusals.length > 0) {
97
- return { ok: false, identity, refusals, capabilities };
176
+ return { ok: false, identity, refusals, capabilities, ...optional };
98
177
  }
99
- return { ok: true, identity, capabilities };
178
+ return { ok: true, identity, capabilities, ...optional };
100
179
  }
101
180
  /** Render a bounded, secret-free summary of why an adapter was refused. */
102
181
  export function formatAdapterRefusal(identity, refusals) {