@bridge_gpt/mcp-server 0.2.51 → 0.2.53

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 (79) hide show
  1. package/README.md +59 -13
  2. package/build/agent-capabilities/probe-context.js +15 -7
  3. package/build/agent-capabilities/probes.js +42 -6
  4. package/build/agent-launchers/claude-executor-adapter.js +98 -14
  5. package/build/commands.generated.js +7 -5
  6. package/build/conduct-epic/cut-protocol.js +17 -3
  7. package/build/conductor/bridge-api-client.js +232 -5
  8. package/build/conductor/cli.js +23 -0
  9. package/build/conductor/deny-enforcement-preflight.js +107 -10
  10. package/build/conductor/doctor.js +428 -5
  11. package/build/conductor/install-doctor.js +65 -656
  12. package/build/conductor/local-merge.js +170 -11
  13. package/build/conductor/readiness-cli.js +152 -0
  14. package/build/conductor/readiness-sections.js +666 -0
  15. package/build/conductor/readiness.js +710 -0
  16. package/build/conductor/tools.js +56 -3
  17. package/build/conductor-bin.js +21 -17
  18. package/build/connect-bitbucket-api.js +370 -0
  19. package/build/connect-bitbucket.js +437 -0
  20. package/build/docs.generated.js +1 -1
  21. package/build/doctor.js +40 -1
  22. package/build/drive-epic.js +423 -11
  23. package/build/env-file-link.js +164 -0
  24. package/build/epic-integration-pr.js +10 -0
  25. package/build/executor/cli.js +41 -6
  26. package/build/executor/deps.js +5 -1
  27. package/build/executor/env-file-guard.js +113 -0
  28. package/build/executor/env.js +78 -1
  29. package/build/executor/heartbeat.js +9 -0
  30. package/build/executor/http-client.js +90 -22
  31. package/build/executor/job-errors.js +43 -2
  32. package/build/executor/job-runner.js +130 -28
  33. package/build/executor/merge-job.js +67 -16
  34. package/build/executor/permissions.js +106 -0
  35. package/build/executor/preflight.js +38 -13
  36. package/build/executor/resume-pre-spawn.js +2 -1
  37. package/build/executor/runner.js +175 -4
  38. package/build/executor/service-unit.js +15 -0
  39. package/build/executor/terminal-mutation.js +22 -1
  40. package/build/executor/types.js +86 -0
  41. package/build/executor/worker-command.js +21 -5
  42. package/build/executor/worker-guard-hook.js +939 -0
  43. package/build/executor/worker-log.js +56 -0
  44. package/build/executor/worktree.js +11 -0
  45. package/build/git-reachability.js +147 -0
  46. package/build/index.js +4734 -4270
  47. package/build/install-bridge.js +95 -0
  48. package/build/install-doctor.js +154 -2
  49. package/build/pipelines.generated.js +6 -4
  50. package/build/plan-epic-conductor-eligibility.js +37 -7
  51. package/build/plane/alembic-head.js +40 -11
  52. package/build/plane/build-freshness.js +22 -11
  53. package/build/plane/cli.js +78 -15
  54. package/build/plane/defaults.js +165 -0
  55. package/build/plane/manifest.js +63 -8
  56. package/build/plane/member-logs.js +6 -0
  57. package/build/plane/member-roster.js +195 -11
  58. package/build/plane/preflight.js +402 -44
  59. package/build/plane/shutdown.js +25 -3
  60. package/build/plane/status.js +11 -0
  61. package/build/plane/supervisor.js +343 -14
  62. package/build/plane/test-fakes.js +43 -0
  63. package/build/plane/types.js +118 -11
  64. package/build/pr-base-contract.js +20 -0
  65. package/build/readiness-check.js +412 -0
  66. package/build/readme.generated.js +1 -1
  67. package/build/review-synthesis-config.js +60 -0
  68. package/build/scripts/executor-protocol-contract-driver.js +311 -0
  69. package/build/setup-epic.js +560 -139
  70. package/build/sfcc/log-query.js +2 -1
  71. package/build/start-tickets-conductor.js +11 -2
  72. package/build/start-tickets.js +69 -2
  73. package/build/version.generated.js +3 -3
  74. package/build/worker-containment-diagnostic.js +97 -0
  75. package/build/worker-guard-hook-bin.js +6 -0
  76. package/docs/CONDUCTOR.md +27 -0
  77. package/docs/install/mcp-tool-integrations.md +3 -2
  78. package/package.json +4 -3
  79. package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
package/README.md CHANGED
@@ -352,7 +352,7 @@ later boot.
352
352
  "mcpServers": {
353
353
  "bridge": {
354
354
  "command": "npx",
355
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],
355
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
356
356
  "env": {
357
357
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
358
358
  "BAPI_REPO_NAME": "your-repo",
@@ -374,7 +374,7 @@ later boot.
374
374
  "bridge": {
375
375
  "type": "stdio",
376
376
  "command": "npx",
377
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],
377
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
378
378
  "env": {
379
379
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
380
380
  "BAPI_REPO_NAME": "your-repo",
@@ -396,7 +396,7 @@ later boot.
396
396
  "bridge": {
397
397
  "type": "stdio",
398
398
  "command": "npx",
399
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],
399
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
400
400
  "env": {
401
401
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
402
402
  "BAPI_REPO_NAME": "your-repo",
@@ -421,7 +421,7 @@ you select `copilot-cli`; the shape below is what it produces.
421
421
  "bridge": {
422
422
  "type": "local",
423
423
  "command": "npx",
424
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],
424
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
425
425
  "tools": ["*"],
426
426
  "env": {
427
427
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
@@ -445,7 +445,7 @@ Windsurf only supports global MCP configuration.
445
445
  "mcpServers": {
446
446
  "bridge": {
447
447
  "command": "npx",
448
- "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],
448
+ "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
449
449
  "env": {
450
450
  "BAPI_BASE_URL": "https://bridgegpt-api.com",
451
451
  "BAPI_REPO_NAME": "your-repo",
@@ -464,7 +464,7 @@ Windsurf only supports global MCP configuration.
464
464
  ```toml
465
465
  [mcp_servers.bridge]
466
466
  command = "npx"
467
- args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"]
467
+ args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"]
468
468
 
469
469
  [mcp_servers.bridge.env]
470
470
  BAPI_BASE_URL = "https://bridgegpt-api.com"
@@ -570,7 +570,7 @@ The tools worth knowing for most tickets.
570
570
  **1. Review Ticket**
571
571
  - **What it does:** Runs a full quality review of a ticket: clarifying questions and a critique plus an alternate-model second opinion, then evaluates the findings and produces a decision page for accepting or rejecting them.
572
572
  - **When it's useful:** (Refinement) Right after a ticket is drafted, before anyone starts building — to surface gaps and tighten it.
573
- - **How to use it:** `/review-ticket BAPI-123` (command only — "review" as free text is easily mistaken for a freehand agent review). For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every `/review-ticket` flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.
573
+ - **How to use it:** `/review-tickets BAPI-123` (command only — "review" as free text is easily mistaken for a freehand agent review). A single key reviews inline in the current session — no terminal tab, no worktree. For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every review flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.
574
574
  - **Flags:** `--auto` auto-accept findings and skip the approval gates · `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions · `--rounds=2` force the full second-opinion review · omit `--rounds` to let the difficulty-adaptive review policy decide.
575
575
 
576
576
  **2. Start Tickets**
@@ -581,7 +581,7 @@ The tools worth knowing for most tickets.
581
581
 
582
582
  **3. Review and Start**
583
583
  - **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree — review and implementation run in two separate agent contexts, not one shared session.
584
- - **When it's useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/full-automation`, which only accepts an idea).
584
+ - **When it's useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/idea-to-pr`, which only accepts an idea).
585
585
  - **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).
586
586
  - **Flags:** `--auto` auto-approves both the review and the implementation phase of every spawned session · `--rounds=1|2` forwarded to the review phase · `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.
587
587
 
@@ -637,6 +637,12 @@ Good to know, but not needed every day.
637
637
  - **How to use it:** Ask your agent — "Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."
638
638
  - **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) · `quality` low (default, cheapest) / medium / high · `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.
639
639
 
640
+ **6. Create Decision Page**
641
+ - **What it does:** Turns open decisions from the current conversation into an interactive HTML decision page, then folds the submitted answers back into the conversation.
642
+ - **When it's useful:** (Refinement) Use it whenever a conversation has accumulated more open choices than are comfortable to settle in prose — not for a single simple question, which is faster to just ask directly.
643
+ - **How to use it:** `/create-decision-page <topic or open questions>` — e.g. *"how we handle rate limiting"* — or leave it empty to frame the open choices already raised earlier in the conversation.
644
+ - **Flags:** None.
645
+
640
646
  ### Now and then
641
647
 
642
648
  Useful once in a while.
@@ -688,11 +694,35 @@ Useful once in a while.
688
694
  - **When it's useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.
689
695
  - **How to use it:** `/estimate-epic BAPI-123`
690
696
  - **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.
697
+
698
+ **9. Regression Check**
699
+ - **What it does:** Runs the deterministic regression-reviewer (lightweight mode) against a proposed code change and reports its blast radius — which real call-sites, tests, mocks, or config the change does and doesn't account for.
700
+ - **When it's useful:** (Correctness) Before merging a change, to see which real callers, tests, or config it leaves unaccounted for.
701
+ - **How to use it:** `/regression-check` reviews the working tree against `HEAD` by default; `/regression-check --diff main...HEAD` reviews an explicit range.
702
+ - **Flags:** `--diff <range>` review a specific diff/PR range instead of the working tree vs. `HEAD` · `--symbols a,b,c` review a not-yet-diffed planned change by naming its symbols directly.
703
+
704
+ **10. Scan Test Coverage**
705
+ - **What it does:** Scans recently shipped tickets from git history and reports which features have or could gain integration tests, and which can only be smoke tested.
706
+ - **When it's useful:** (Testing) Periodically, to find recently shipped features still missing integration-test coverage and how each could be smoke tested instead.
707
+ - **How to use it:** `/scan-test-coverage` runs incrementally from the last recorded marker in `docs/test-coverage/STATE.md`.
708
+ - **Flags:** `--since=YYYY-MM-DD` override the window start date · `--full` ignore the marker and use a 6-month lookback · `--limit=N` cap the number of features investigated.
709
+
710
+ **11. Estimate Ticket**
711
+ - **What it does:** One-ticket shortcut over the existing epic estimation capability: estimates a single Jira ticket via the same `estimate_epic` tool.
712
+ - **When it's useful:** (Estimation) When you want one ticket's estimate without typing out an epic key or an explicit ticket list.
713
+ - **How to use it:** `/estimate-ticket BAPI-123`
714
+ - **Flags:** `--allow-partial` the sole optional flag.
715
+
716
+ **12. Draft Ticket**
717
+ - **What it does:** Delegates to the Jira Ticket Writer to create only a local Markdown draft — it does not create or update a Jira issue.
718
+ - **When it's useful:** (Refinement) When you want a reviewable local ticket draft before committing to an upload workflow.
719
+ - **How to use it:** `/draft-ticket <description>`
720
+ - **Flags:** None.
691
721
  <!-- END GENERATED: mcp-tool-documentation -->
692
722
 
693
723
  ### Workflow commands
694
724
 
695
- Slash commands that drive several tools at once. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful) — the rest live here.
725
+ Slash commands that drive several tools at once. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful); Create Decision Page is documented above under [Occasionally useful](#occasionally-useful); Regression Check and Scan Test Coverage are documented above under [Now and then](#now-and-then) — the rest live here.
696
726
 
697
727
  **1. Implement Ticket**
698
728
  - **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.
@@ -991,7 +1021,7 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
991
1021
  npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
992
1022
  ```
993
1023
 
994
- **Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-5 → basic, 6+ → premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier → alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model — and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) — each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.
1024
+ **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 injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier → alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model — and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) — each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.
995
1025
 
996
1026
  **Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.
997
1027
 
@@ -1059,6 +1089,10 @@ unauthorized, or malformed — it escalates and prints no conductor invocation a
1059
1089
  all, because an unknown owner is not the same as a not-ready one. No branch,
1060
1090
  including every error path, ever offers you two paths.
1061
1091
 
1092
+ `drive-epic` owns no branch behavior of its own — it forwards `--feature-branch
1093
+ <name>` and `--into-base` verbatim to `setup-epic`, which decides the strategy (see
1094
+ below). With neither flag, a multi-ticket epic runs on `epic/<KEY>`.
1095
+
1062
1096
  Two conductors is a transitional state. When one is eliminated, `drive-epic` is
1063
1097
  the only thing that changes.
1064
1098
 
@@ -1072,6 +1106,18 @@ npx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-e
1072
1106
 
1073
1107
  The plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.
1074
1108
 
1109
+ **The command above uses an epic branch.** For a plan with **two or more** nodes and
1110
+ no branch flag, `setup-epic` derives `epic/<KEY>`: every child-ticket PR targets that
1111
+ branch, and one draft integration PR targets the repository base branch. This holds on
1112
+ every path, CI and `--json` included. A single-node plan keeps the repository base
1113
+ branch, matching plain `start-tickets`.
1114
+
1115
+ > **Behavior change (BAPI-1009).** The same no-flag command previously ran children
1116
+ > directly into the repository base branch. **Pass `--into-base` to restore that**, if
1117
+ > your automation depends on it. Pass `--feature-branch <name>` to override the derived
1118
+ > name, or to force a branch for a single-node plan. Passing both is a parse error,
1119
+ > refused before any file read, credential resolution, or network call.
1120
+
1075
1121
  Once the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:
1076
1122
 
1077
1123
  ```
@@ -1441,7 +1487,7 @@ The full surface, for when you need the complete enumeration. Day-to-day, use [U
1441
1487
 
1442
1488
  ### MCP tools
1443
1489
 
1444
- The authoritative tool catalog covers **70 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).
1490
+ The authoritative tool catalog covers **73 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).
1445
1491
 
1446
1492
  - **Connectivity & identity** — `ping` (its JSON also carries `docs_dir`, `role`, and `customer_type`)
1447
1493
  - **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)
@@ -1462,10 +1508,10 @@ Pipelines are declarative, multi-step workflows your AI agent executes step-by-s
1462
1508
  | Pipeline | Description | Invoke with |
1463
1509
  |---|---|---|
1464
1510
  | `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |
1465
- | `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |
1511
+ | `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. Internal atomic pipeline invoked by `/review-tickets` (inline for one key, spawned per tab for several) and by `/review-and-implement`, `/review-and-start`, `/start-tickets`, and `/conduct-epic`. | `/review-tickets PROJ-123` |
1466
1512
  | `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |
1467
1513
  | `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |
1468
- | `full-automation` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/full-automation "<idea>"` |
1514
+ | `full-automation` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/idea-to-pr "<idea>"` |
1469
1515
  | `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |
1470
1516
 
1471
1517
  ### Pipeline response envelope
@@ -20,8 +20,10 @@ import { DEFAULT_PROBE_TIMEOUT_MS, } from "./types.js";
20
20
  * `--trust` is MANDATORY headless or the workspace-trust prompt hangs; cursor has
21
21
  * its own `--workspace` cwd flag. All `OutputFormat` values (including `stream-json`)
22
22
  * pass through the generic `--output-format <fmt>` slot.
23
- * - claude: `-p [--dangerously-skip-permissions] [--model <m>] [--output-format json|
24
- * stream-json --verbose] <prompt>`. Claude has NO working-dir flag — cwd is set via
23
+ * - claude: `-p [--permission-mode auto|acceptEdits | --dangerously-skip-permissions]
24
+ * [--model <m>] [--output-format json|stream-json --verbose] <prompt>`. The
25
+ * worker default is `--permission-mode auto` (BAPI-1020); bypass is emitted only
26
+ * for the explicit revert posture and for the probe that must still measure it. Claude has NO working-dir flag — cwd is set via
25
27
  * the spawn options, never an argument. `stream-json` REQUIRES `--verbose`. The
26
28
  * prompt is ALWAYS the final argv element so mocked prompt extraction stays valid.
27
29
  */
@@ -32,11 +34,17 @@ export function buildHeadlessArgs(agentName, opts) {
32
34
  }
33
35
  // claude (and any positional-prompt agent without a cwd flag)
34
36
  const args = ["-p"];
35
- // BAPI-725: exactly ONE permission form, matching the Claude executor
36
- // adapter's argv builder (`agent-launchers/claude-executor-adapter.ts`).
37
- // `permissionMode` wins when both are supplied — emitting both would let the
38
- // skip flag take effect and the probe would report on a posture it never ran.
39
- if (opts.permissionMode === "acceptEdits") {
37
+ // BAPI-725, widened by BAPI-1020: exactly ONE permission form, matching the
38
+ // Claude executor adapter's argv builder
39
+ // (`agent-launchers/claude-executor-adapter.ts`). `permissionMode` wins when
40
+ // both are supplied emitting both would let the skip flag take effect and the
41
+ // probe would report on a posture it never ran. That precedence is UNCHANGED;
42
+ // only the set of accepted modes grew, so a probe can now measure the `auto`
43
+ // posture every worker actually runs under.
44
+ if (opts.permissionMode === "auto") {
45
+ args.push("--permission-mode", "auto");
46
+ }
47
+ else if (opts.permissionMode === "acceptEdits") {
40
48
  args.push("--permission-mode", "acceptEdits");
41
49
  }
42
50
  else if (opts.skipPermissions === true) {
@@ -419,10 +419,15 @@ function evaluateDenyRun(run, allowedMarker) {
419
419
  export async function runDenyEnforcementCheck(ctx, opts) {
420
420
  const marker = ctx.marker("DENY_ALLOWED");
421
421
  const prompt = denyProbePrompt();
422
- const acceptEdits = opts?.permissionPosture === "accept_edits";
422
+ // Exactly ONE permission form per posture, mirroring the adapter's argv map.
423
+ const permissionMode = opts?.permissionPosture === "auto"
424
+ ? "auto"
425
+ : opts?.permissionPosture === "accept_edits"
426
+ ? "acceptEdits"
427
+ : undefined;
423
428
  const runOpts = {
424
- skipPermissions: !acceptEdits,
425
- permissionMode: acceptEdits ? "acceptEdits" : undefined,
429
+ skipPermissions: permissionMode === undefined,
430
+ permissionMode,
426
431
  model: opts?.model,
427
432
  timeoutMs: opts?.timeoutMs,
428
433
  };
@@ -493,7 +498,8 @@ export async function runDenyEnforcementCheck(ctx, opts) {
493
498
  return {
494
499
  result: withDenyLayer({
495
500
  status: "fail",
496
- detail: "deny is NOT enforced for this installed Claude version under --dangerously-skip-permissions " +
501
+ detail: "deny is NOT enforced for this installed Claude version under the probed " +
502
+ `permission posture (${opts?.permissionPosture ?? "skip_permissions"}) ` +
497
503
  "(neither settings permissions.deny nor a PreToolUse hook rejected the denied call)",
498
504
  elapsedMs: hookRun.elapsedMs,
499
505
  evidence: h.evidence,
@@ -507,8 +513,10 @@ const denyEnforcement = {
507
513
  description: "Runs a no-deny control run first (proving the agent actually attempts the canary call), then spawns " +
508
514
  "claude headless with --dangerously-skip-permissions in a disposable temp project carrying a " +
509
515
  "permissions.deny rule, attempts the denied call, and asserts it is rejected — reporting which layer " +
510
- "enforced (settings-deny vs a PreToolUse hook fallback). A fail means deny is NOT enforced on the " +
511
- "installed CLI (or cannot be verified) and is a load-bearing executor preflight signal.",
516
+ "enforced (settings-deny vs a PreToolUse hook fallback). Since BAPI-1020 this is the EXPLICIT REVERT " +
517
+ "posture rather than the worker default, and it is still probed for exactly that reason: " +
518
+ "`skip_permissions` remains selectable and must stay guarded. A fail means deny is NOT enforced on " +
519
+ "the installed CLI (or cannot be verified) and is a load-bearing executor preflight signal.",
512
520
  tier: "heavy",
513
521
  appliesTo: ["claude"],
514
522
  spawnsAgent: true,
@@ -517,6 +525,33 @@ const denyEnforcement = {
517
525
  return result;
518
526
  },
519
527
  };
528
+ /**
529
+ * The same check under the posture every worker actually runs (BAPI-1020).
530
+ *
531
+ * Registered as a SEPARATE probe rather than replacing the bypass probe, because
532
+ * the two answer different questions and one cannot stand in for the other. Under
533
+ * bypass the CLI's own classifier is out of the picture entirely, so the probe
534
+ * measures the deny layer alone; under `auto` the classifier is in the path and a
535
+ * denied call could in principle wait for an approval that headless `-p` mode can
536
+ * never supply. The control-run discipline is identical and applies unchanged: a
537
+ * marker-free run is a REFUSAL, never enforcement.
538
+ */
539
+ const denyEnforcementAuto = {
540
+ id: "deny-enforcement-auto",
541
+ title: "Enforces a permissions.deny rule under --permission-mode auto",
542
+ description: "The deny-enforcement check under the DEFAULT worker posture (BAPI-1020): a no-deny control run " +
543
+ "first, then claude headless with --permission-mode auto in a disposable temp project carrying a " +
544
+ "permissions.deny rule, asserting the denied call is rejected and reporting which layer enforced. " +
545
+ "This is the posture every conductor worker spawns under, so a fail here means the executor would " +
546
+ "be claiming jobs whose deny layer is unverified for the posture they actually run in.",
547
+ tier: "heavy",
548
+ appliesTo: ["claude"],
549
+ spawnsAgent: true,
550
+ async run(ctx) {
551
+ const { result } = await runDenyEnforcementCheck(ctx, { permissionPosture: "auto" });
552
+ return result;
553
+ },
554
+ };
520
555
  /** All probes in deterministic report order. */
521
556
  export const ALL_PROBES = [
522
557
  binaryResolves,
@@ -526,6 +561,7 @@ export const ALL_PROBES = [
526
561
  preambleMidPrompt,
527
562
  outputFormat,
528
563
  denyEnforcement,
564
+ denyEnforcementAuto,
529
565
  workspaceFlag,
530
566
  noCwdFlag,
531
567
  ];
@@ -2,7 +2,9 @@ import { isModelTier, isValidModelAlias, resolveModelAlias } from "../agent-regi
2
2
  import { evaluateClaudeMcpShadowingPolicy, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "../claude-user-config-doctor.js";
3
3
  import { runDenyEnforcementPreflight } from "../conductor/deny-enforcement-preflight.js";
4
4
  import { buildExecutorBaseWorkerEnv } from "../executor/env.js";
5
+ import { ExecutorNamedError } from "../executor/job-errors.js";
5
6
  import { provisionExecutorDenyLayer } from "../executor/permissions.js";
7
+ import { WORKER_GUARD_ROOT_FLAG } from "../executor/worker-guard-hook.js";
6
8
  import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
7
9
  import { normalizeMcpServerNames } from "../executor/observation.js";
8
10
  import { supported } from "./executor-adapter.js";
@@ -179,17 +181,31 @@ export function resolveClaudeModelAlias(spec, payload) {
179
181
  * Build the exact headless Claude argv.
180
182
  *
181
183
  * Moved verbatim from `worker-command.ts`'s `buildClaudeExecutorArgv`. The
182
- * ordering is the golden shape the tests pin:
184
+ * ordering is the golden shape the tests pin, unchanged by BAPI-1020:
183
185
  *
184
186
  * -p <prompt> --output-format stream-json --verbose
185
187
  * [--model <alias>]
186
- * (--dangerously-skip-permissions | --permission-mode acceptEdits)
188
+ * (--permission-mode auto | --permission-mode acceptEdits | --dangerously-skip-permissions)
187
189
  * --strict-mcp-config --mcp-config <verified path>
188
190
  *
191
+ * The DEFAULT form is now `--permission-mode auto` (BAPI-1020): Claude's own
192
+ * classifier runs, a flagged call is denied with a reason, and the worker
193
+ * continues. Claude CLI 2.1.258 headless behavior was confirmed during design —
194
+ * under `auto` a denied call does not wait for an approval that can never arrive
195
+ * in `-p` mode. `--dangerously-skip-permissions` is now emitted ONLY for the
196
+ * explicit `skip_permissions` revert.
197
+ *
189
198
  * Exactly ONE permission form is emitted. Emitting both would let the skip flag
190
199
  * win silently and make the `accept_edits` policy look effective while doing
191
- * nothing. The posture changes how Claude PROMPTS, not what it may do — the deny
192
- * layer is provisioned in both postures.
200
+ * nothing. The branch is an EXHAUSTIVE three-way map rather than the two-way
201
+ * `if/else` it replaced, whose `else` was the skip flag: under the old shape a
202
+ * posture the union grew but this function did not would have fallen through into
203
+ * bypass, silently. It now throws the same fixed, secret-free contract error the
204
+ * payload resolver uses, so an unmapped posture fails loudly instead of buying
205
+ * full bypass.
206
+ *
207
+ * The posture changes how Claude PROMPTS, not what it may do — the deny layer and
208
+ * the deterministic worker guard are provisioned in EVERY posture.
193
209
  *
194
210
  * Every entry is pushed SEPARATELY and never joined into a command string, so a
195
211
  * prompt or path containing a space or a shell metacharacter is just a value.
@@ -199,15 +215,34 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
199
215
  if (alias) {
200
216
  argv.push("--model", alias);
201
217
  }
202
- if (posture === "accept_edits") {
203
- argv.push("--permission-mode", "acceptEdits");
204
- }
205
- else {
206
- argv.push("--dangerously-skip-permissions");
207
- }
218
+ argv.push(...claudePermissionArgs(posture));
208
219
  argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
209
220
  return argv;
210
221
  }
222
+ /**
223
+ * The single permission form for one posture — the whole policy→argv mapping,
224
+ * in one exhaustive place.
225
+ *
226
+ * Returns an ARRAY so every caller appends exactly what this function decided;
227
+ * there is no path on which a caller can add a second form. The thrown message
228
+ * names the FIELD and the accepted spellings and never the supplied value,
229
+ * matching `resolveWorkerPermissionPosture` — this text can reach the server
230
+ * through `/fail`, so echoing an operator's mistyped policy value would turn a
231
+ * typo into an exfiltration path.
232
+ */
233
+ function claudePermissionArgs(posture) {
234
+ switch (posture) {
235
+ case "auto":
236
+ return ["--permission-mode", "auto"];
237
+ case "accept_edits":
238
+ return ["--permission-mode", "acceptEdits"];
239
+ case "skip_permissions":
240
+ return ["--dangerously-skip-permissions"];
241
+ default:
242
+ throw new ExecutorNamedError("ContractError.PermissionPosture", "worker_permission_posture is not a supported posture " +
243
+ "(expected 'auto', 'skip_permissions', or 'accept_edits').");
244
+ }
245
+ }
211
246
  /**
212
247
  * Build the Claude worker environment: the generic executor base, plus the one
213
248
  * conditional OAuth passthrough.
@@ -224,9 +259,11 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
224
259
  * and cannot reach a worker environment through any path in this function.
225
260
  *
226
261
  * `options` is forwarded to the generic builder unchanged, including BAPI-844's
227
- * `indexScope`. This adapter neither reads nor validates the scope: it is not an
228
- * agent-specific credential decision, so it stays owned by the generic builder,
229
- * where the deny-list and the shared shape check both live.
262
+ * `indexScope` and BAPI-1020's `workerBranch`. This adapter neither reads nor
263
+ * validates either one: neither is an agent-specific credential decision, so both
264
+ * stay owned by the generic builder, where the deny-list and the shared shape
265
+ * check both live. In particular `workerBranch` is never read from `parentEnv`
266
+ * here — the generic builder denies that key outright.
230
267
  */
231
268
  export function buildClaudeWorkerEnv(parentEnv, options = {}) {
232
269
  const env = buildExecutorBaseWorkerEnv(parentEnv, options);
@@ -236,6 +273,43 @@ export function buildClaudeWorkerEnv(parentEnv, options = {}) {
236
273
  }
237
274
  return env;
238
275
  }
276
+ /**
277
+ * Shell-quote a path for embedding in a hook command string.
278
+ *
279
+ * Same convention as `start-tickets-conductor.ts`'s conductor-hook command:
280
+ * single-quote for POSIX safety, escaping any embedded single quote. Claude Code
281
+ * interprets a hook `command` through a shell, so a worktree path containing a
282
+ * space — or an apostrophe — must survive that interpretation intact.
283
+ */
284
+ function shellQuoteHookPath(value) {
285
+ return `'${value.replace(/'/g, "'\\''")}'`;
286
+ }
287
+ /**
288
+ * The PreToolUse command that runs the packaged deterministic worker guard.
289
+ *
290
+ * Invoked through the EXECUTOR's own Node executable rather than a bare shebang,
291
+ * for the same reason `conductorNodePath` exists (BAPI-527): the worker's `PATH`
292
+ * may resolve a different Node — or none — and a guard that fails to launch is a
293
+ * guard that is not there. A launch failure still fails open, but it should not be
294
+ * something the environment can cause casually.
295
+ *
296
+ * The command carries exactly ONE argument: the trusted worktree root. Templating
297
+ * it here is what makes every path-containment answer trustworthy — provisioning
298
+ * already HOLDS the authoritative worktree path, whereas the hook process's own
299
+ * `cwd` is not guaranteed by the PreToolUse contract to be the project root, and a
300
+ * containment check measured from the wrong root reports success rather than
301
+ * failing safe. The path is not a secret (it is already visible in `ps` for the
302
+ * worker itself) and it is shell-quoted, so a directory containing a space or an
303
+ * apostrophe survives intact.
304
+ *
305
+ * The BRANCH is deliberately NOT an argument: it travels in `BAPI_WORKER_BRANCH`,
306
+ * where the executor's env deny-list can guarantee an ambient value never
307
+ * substitutes for the job's own.
308
+ */
309
+ export function buildWorkerGuardHookCommand(binPath, execPath, worktreePath) {
310
+ return (`${shellQuoteHookPath(execPath)} ${shellQuoteHookPath(binPath)} ` +
311
+ `${WORKER_GUARD_ROOT_FLAG} ${shellQuoteHookPath(worktreePath)}`);
312
+ }
239
313
  // ---------------------------------------------------------------------------
240
314
  // The adapter factory
241
315
  // ---------------------------------------------------------------------------
@@ -268,6 +342,9 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
268
342
  if (input.indexScope !== undefined) {
269
343
  envOptions.indexScope = input.indexScope;
270
344
  }
345
+ if (input.workerBranch !== undefined) {
346
+ envOptions.workerBranch = input.workerBranch;
347
+ }
271
348
  return {
272
349
  executable: spec.command,
273
350
  argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
@@ -323,7 +400,14 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
323
400
  "continuing fail-open",
324
401
  };
325
402
  }
326
- return provision(input.worktreePath, { baseBranch: input.baseBranch }, provisioningDeps);
403
+ const options = {
404
+ baseBranch: input.baseBranch,
405
+ workerGuardHookCommand: buildWorkerGuardHookCommand(input.workerGuardHookBinPath, deps.execPath ?? process.execPath, input.worktreePath),
406
+ };
407
+ if (input.workerBranch !== undefined) {
408
+ options.workerBranch = input.workerBranch;
409
+ }
410
+ return provision(input.worktreePath, options, provisioningDeps);
327
411
  },
328
412
  async probeEnforcement(options = {}) {
329
413
  const probe = deps.runDenyPreflight ?? runDenyEnforcementPreflight;