@bridge_gpt/mcp-server 0.2.57 → 0.2.58

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.
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,BUILD_COMMIT,LAUNCHER_ARGS,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.57",BUILD_COMMIT="dcfa5632b4eb",LAUNCHER_ARGS=["-y","--prefer-offline","@bridge_gpt/mcp-server@0.2.57","serve"]}});function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}var OFF_TOKENS,ON_TOKENS,init_env_flags=__esm({"src/env-flags.ts"(){"use strict";OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"])}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
2
+ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,BUILD_COMMIT,LAUNCHER_ARGS,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.58",BUILD_COMMIT="e29c3f46e4eb",LAUNCHER_ARGS=["-y","--prefer-offline","@bridge_gpt/mcp-server@0.2.58","serve"]}});function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}var OFF_TOKENS,ON_TOKENS,init_env_flags=__esm({"src/env-flags.ts"(){"use strict";OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"])}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
3
3
 
4
4
  $ARGUMENTS
5
5
 
@@ -708,11 +708,11 @@ This command is recipe-driven. Do not call MCP tools directly -- the recipe dete
708
708
 
709
709
  **A phase's durable-recording tool call is not the end of the command.** The \`record_phase_result\` and \`record_checkpoint\` tools return a success envelope; that envelope means the *current* \`agent_task\` step finished, nothing more. Continue immediately with the next step in the same turn. This applies in particular at the 3 \u2192 4, 5 \u2192 6, and 8 \u2192 9 boundaries, which are where the recipe previously stopped: those instruction files used to end in a text envelope addressed to an orchestrator that does not exist inline, so the turn ended there and the run stalled with no error. None of these boundaries introduces an approval pause.
710
710
 
711
- **Background work is not completion.** A step that submits server-side work is finished
712
- only when you have retrieved its terminal result. That includes step 2's
713
- \`request_plan_generation\`: a \`GATEWAY_TIMEOUT\`/\`504\` envelope carrying a \`recovery_get\`
714
- field means server-side processing may still be running, so poll \`get_plan\` (or that URL)
715
- until a terminal result \u2014 never reissue the step. The same rule covers any ticket review
711
+ **Background work is not completion.** Step 2's \`request_plan_generation\` submits
712
+ without waiting. Step 3 must retrieve the terminal plan with repeated bounded
713
+ \`get_plan\` calls (\`budget_ms: 20000\`); \`state: "pending"\` is normal. If the submission
714
+ outcome is ambiguous because of a dropped connection, use \`get_plan\` to inspect
715
+ the existing run \u2014 never reissue the step automatically. The same rule covers any ticket review
716
716
  started before the commit handoff: reach a terminal result through a bounded wait tool
717
717
  (\`wait_for_ticket_review\`, called again on each normal \`state: "pending"\` result) or the
718
718
  artifact's retrieval tool. A shell \`sleep\`, an unbounded foreground poll, and re-issuing
@@ -1841,7 +1841,7 @@ files that couldn't be modified), list them here. If no warnings, omit this sect
1841
1841
  On success, display the structured report from Stage 3 confirming that the follow-up changes are complete.
1842
1842
 
1843
1843
  On failure at any critical stage (Stage 0 or Stage 1), display which stage failed and the error details.
1844
- `,"review-and-implement.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"agent","flag":"--agent","type":"string"}]}\n---\n\n# Review and Implement: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command runs inside a single already-created worktree (typically spawned by `/start-tickets --workflow review-and-implement`, or its front door `/review-and-start`). It reviews the ticket via `/review-ticket` **in this same session**, pauses at a per-ticket human proceed/halt gate, and \u2014 only on approval \u2014 hands the ticket off to a **freshly spawned implementation session** (`start-tickets --workflow implement`) that reuses this same worktree. Review and implementation therefore run in **two separate agent sessions/contexts**: the review session ends the moment the fresh implementation session is spawned, and the fresh session reads all reviewed ticket state back from the server rather than inheriting this session\'s transcript. It creates no *new* worktrees, refreshes no base branch, and does not monitor sibling sessions \u2014 those responsibilities stay in the launcher (`start-tickets`) and the parent front-door command.\n\n---\n\n# Instructions\n\n## Argument Parsing\n\nParse `$ARGUMENTS`:\n\n1. **Ticket key**: exactly one required token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`). If zero tokens match, or more than one token matches, stop immediately and display:\n ```\n Invalid ticket key. Expected exactly one key, e.g. PROJ-123.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n2. **`--auto`**: an optional position-independent flag. When present, sets chain-level `auto_approve` to `true`. This single flag applies to BOTH the review phase and the implementation phase below \u2014 there is no separate review-auto or implementation-auto state.\n3. **`--rounds`**: an optional position-independent `--rounds <n>` / `--rounds=<n>` argument, `1` or `2`. Normalize either form to `--rounds=<n>`. Reject any other value:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n When omitted, forward no `--rounds` to `/review-ticket` (the backend\'s difficulty-adaptive review policy decides the shape).\n4. **`--base-branch`**: an optional position-independent `--base-branch <branch>` / `--base-branch=<branch>` argument. Validate it using the same rules `/start-tickets` Stage 0 uses: after trimming, non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`). Reject a malformed value:\n ```\n Invalid --base-branch value: <reason>\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n5. **Executor identity**: accept optional position-independent `--agent claude|cursor-agent|codex` and `--host claude-code|cursor|codex` in either space or `=` form. For example, `--agent cursor-agent` and `--agent=cursor-agent` both select Cursor; the launcher supplies `--agent codex --host codex` for Codex. Resolve `--agent` first when both are present; otherwise map the host to its registered agent. If neither is supplied in a Codex session, resolve `codex` as both agent and host. Map the effective agent to its host (`claude-code`, `cursor`, or `codex`) for handoff; an explicit agent wins over a conflicting host. Reject an unknown value before review. Retain the resolved pair for the fresh-session handoff; do not infer it from the main checkout.\n\n## Phase 0 \u2014 Capture Worktree Identity (before review)\n\nBefore any review work runs, perform the **capture step of the Cleanup Helper** (below): record this ticket worktree\'s absolute top-level path and current branch, and resolve the main checkout. These captured values are the ONLY inputs the later cleanup and handoff steps use \u2014 capturing them up front means a mid-run branch switch cannot redirect a later `git worktree remove`/`git branch -D` at the wrong target.\n\n## Cleanup Helper\n\nA single named procedure invoked by **both** halt paths \u2014 review failure (Phase 1) and a declined gate (Phase 2). It never runs on the success path (a successful handoff intentionally keeps the worktree). Perform its steps exactly; it is local to this command\'s workflow and adds **no** new cleanup CLI flag, launcher subcommand, or MCP surface.\n\n**Capture (run in Phase 0, before `/review-ticket`):**\n\n1. Capture the ticket worktree\'s **absolute top-level path** with `git rev-parse --show-toplevel` and its **current branch** with `git rev-parse --abbrev-ref HEAD`, both run from inside this worktree. Call these `<capturedPath>` and `<capturedBranch>`.\n2. Resolve the **main checkout** from `git worktree list --porcelain`: parse the `worktree <path>` records exactly, retaining each path verbatim **without splitting on whitespace** (a path may contain spaces), and select the main (non-linked, bare-repo-backed) checkout. **Refuse to treat `<capturedPath>` (the current ticket worktree) as the main checkout** \u2014 if the resolution is ambiguous or would resolve to the ticket worktree itself, abort cleanup and report retention instead. Call the result `<mainCheckout>`.\n\n**Remove (run only when a halt path fires):**\n\n3. Confirm `<capturedPath>` is still **registered against `<capturedBranch>`** in `git worktree list --porcelain` before deleting anything. If the path is unregistered, detached, or bound to a different branch than captured, **do not remove** \u2014 report retention.\n4. Confirm the ticket worktree is **clean** with `git status --porcelain` (run against `<capturedPath>`). If the output is non-empty (uncommitted or untracked changes), **report and retain** it \u2014 never force-remove unexpected local work.\n5. From `<mainCheckout>` (never from inside the ticket worktree), run `git worktree remove <capturedPath>` using shell-safe single-quoting of `<capturedPath>` and **no `--force`**.\n6. Only **after** the worktree removal succeeds, and again from `<mainCheckout>`, run `git branch -D -- <capturedBranch>` (shell-safe quoting). Preserve this remove-then-delete ordering; never delete the branch first.\n7. Report the exact branch and worktree removed (e.g. `Cleaned up worktree <capturedPath> and branch <capturedBranch>`), or, on any failure, a **bounded** cleanup-failure message that names what could not be removed and contains no credential or raw secret material.\n\n## Phase 1 \u2014 Review (inline)\n\n### Pre-review tier snapshot\n\nBefore invoking `/review-ticket`, call the `get_ticket` MCP tool with `ticket_number` set to `<ticket_key>` and `include_model_tier` set to `true`, and record a **tier snapshot** from the `tier` field it merges into the response. Retain the value **only** when it is one of the recognized tiers `cheap`, `basic`, or `premium`; treat any tool error, missing/`null` payload, or unrecognized value as an **unresolved** snapshot. A tier-snapshot failure is **non-critical**: do NOT abort \u2014 continue into review, and let the later implementation launcher perform its ordinary difficulty-based routing when the snapshot is unresolved. The snapshot exists only to let the fresh implementation session reuse the review-time tier decision (via `--tier`) instead of recomputing it; it is a coarse `cheap|basic|premium` label, never a raw model alias.\n\n### Run review\n\nInvoke `/review-ticket <ticket_key>` **inline, in this same session**, forwarding:\n- `--auto` when chain-level `auto_approve` is `true`.\n- The normalized `--rounds=<n>` when `--rounds` was supplied.\n- `--base-branch=<branch>` when it was supplied.\n\n`/review-ticket` grounds its codebase evaluation against a **fresh `git archive` of `origin/<base>`**, materialized into an isolated temp directory via `fresh_base` with `action: "materialize"` (see `docs/BAPI-474-ground-review-against-fresh-base.md`) \u2014 **not against this worktree\'s own working tree** (i.e. fresh-base grounding is not worktree-local). Review therefore behaves identically whether run standalone or, as here, inside a worktree that `start-tickets` already created; the worktree\'s isolation exists for the implementation phase below, not for review\'s codebase grounding.\n\nIf the review pipeline itself fails (any step reports `Status: Failed at step N`), halt this ticket immediately \u2014 in **both** auto and non-auto mode \u2014 before Phase 2 or Phase 3 run. Invoke the **Cleanup Helper** to remove this ticket\'s worktree and branch, then report:\n```\nReview failed for <ticket_key> at step N. Implementation was not started.\n```\nand stop. No implementation session is spawned.\n\n## Phase 2 \u2014 Halt Gate\n\n`/review-ticket` produces **decisions that rewrite the ticket** \u2014 it does NOT emit a binary approve/decline verdict token. Do not parse, infer, or synthesize an approve/decline signal from its output; no such token exists to parse.\n\n- **When chain-level `auto_approve` is `true`** and Phase 1 completed successfully: skip this gate entirely and proceed straight to Phase 3.\n- **Otherwise** (non-auto, and Phase 1 completed successfully): after the ticket has been rewritten, ask the user exactly:\n ```\n Proceed to implementation for <ticket_key>? (y/N)\n ```\n Treat an empty response, any negative response (`n`, `no`, or similar), or any ambiguous/unrecognized response as **decline** \u2014 do not guess intent. On decline, invoke the **Cleanup Helper** to remove this ticket\'s worktree and branch, then report:\n ```\n Declined implementation for <ticket_key>. The worktree and branch were cleaned up.\n ```\n and stop. Do not spawn an implementation session.\n- On an explicit affirmative response (`y` or `yes`), proceed to Phase 3.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Phase 3 \u2014 Implement (fresh session handoff)\n\n**Never invoke `/implement-ticket` inline in this session.** Instead, spawn a fresh implementation session that reuses this worktree, then end this review session.\n\n1. **Resolve the launcher directory.** Run the handoff from the `<mainCheckout>` resolved in Phase 0 \u2014 `start-tickets` manages worktrees and must run from the main checkout, never from inside the ticket worktree.\n2. **Construct exactly one launcher command** for this single ticket key, using the `<launcher>` resolved above:\n ```\n <launcher> start-tickets --workflow implement <handoff-flags> <ticket_key>\n ```\n Assemble `<handoff-flags>` conditionally:\n - `--tier=<snapshot>` **only** when the Phase 1 tier snapshot resolved to a valid `cheap|basic|premium` value. When the snapshot is unresolved, **omit `--tier` entirely** so the fresh launcher follows its normal fail-open difficulty routing.\n - `--branch <ticket_key>=<capturedBranch>` **only** when `<capturedBranch>` differs from the launcher default `feature/<ticket_key>` (i.e. an enriched branch such as `feature/<KEY>-<slug>`). This reuses the existing worktree/branch via the launcher\'s `switch` path instead of cutting a new one; when the branch is already the bare `feature/<ticket_key>`, no override is needed.\n - `--auto` **only** when chain-level `auto_approve` is active.\n - `--agent <resolved-agent>` and `--host <resolved-host>` when executor identity was supplied. For a Codex session, always pass `--agent codex --host codex`, even if the outer caller supplied an overriding host, so the implementation launcher cannot default to Claude.\n - `--base-branch=<value>` **only** when this command received `--base-branch` (Argument Parsing item 4). A fresh terminal does not inherit this session\'s environment, so re-emitting the flag is the only way the base survives into the implementation launcher \u2014 which exports it to the worker as `BAPI_BASE_BRANCH`, and the create-PR step targets it. Escape the value exactly as `/start-tickets` values are escaped: replace every single quote in the branch with `\'\\\'\'`, then wrap the complete value in single quotes before inserting it into the launcher command.\n3. **Credential invariant.** `BAPI_API_KEY` and every other credential MUST be absent from this launcher command and its environment assignment \u2014 the spawned CLI resolves credentials on its own through `resolveBapiCredentials` / the user-scoped credential store. Never inline a key, authorization header, or credential-store payload into the command string.\n4. **Starter prompt boundary.** The fresh session\'s starter prompt is `/implement-ticket <ticket_key>` for Claude/Cursor or `$bridge-implement-ticket <ticket_key>` for Codex (plus `--auto` when auto is active) \u2014 the launcher builds it. The review transcript, the tier snapshot, the branch metadata, and any review decisions are **excluded** from that prompt; the fresh session reads all reviewed ticket state back from the server.\n5. **On success** (launcher exits `0` and reports the ticket spawned): **end this review session immediately.** Do not monitor, parse, or wait for any output from the fresh implementation session \u2014 that session owns the implementation lifecycle from here.\n6. **On a non-zero launcher exit**: treat it as a **handoff failure**. Report the exit code and the launcher\'s error output, and **retain** the existing clean worktree for a manual retry \u2014 do **not** invoke the Cleanup Helper (a failed handoff is not a decline). \n\n## Scope\n\nThis command composes `/review-ticket` (inline) and a fresh `start-tickets --workflow implement` handoff; it owns none of their internals and must not:\n- create, re-cut, or switch a *new* Worktrunk worktree itself (the fresh launcher reuses the existing one),\n- refresh or fetch the launcher\'s base branch,\n- monitor or coordinate with sibling `/review-and-implement` sessions spawned for other tickets,\n- orchestrate the parent `start-tickets` / `/review-and-start` session,\n- carry the review transcript, tier snapshot, or branch metadata into the fresh implementation session\'s starter prompt.\n\nA halt (review failure or a declined gate) affects **this ticket only** \u2014 it cleans up this ticket\'s own worktree/branch and never touches sibling worktrees spawned for other keys, which continue independently. The fresh implementation session receives only `/implement-ticket <ticket_key> [--auto]` for Claude/Cursor or `$bridge-implement-ticket <ticket_key> [--auto]` for Codex as its starter prompt and reads all reviewed ticket state back from the server.\n',"review-and-start.md":'---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"agent","flag":"--agent","type":"string"},{"name":"host","flag":"--host","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"guardStaleBranch","flag":"--guard-stale-branch","type":"boolean"}]}\n---\n\n# Review and Start: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248` or `BAPI-248 BAPI-250` \u2014 both flow through the identical ordered `keys: string[]` path) and spawns one refreshed, self-contained Worktrunk worktree per ticket, each running `/review-and-implement <KEY>`: review runs in that spawned session, then a per-ticket human proceed/halt gate, and \u2014 only on approval \u2014 that session hands the ticket off to a **fresh implementation session** that reuses the same worktree. Review and implementation thus run in two separate agent sessions per ticket, not one shared context. It is the **recommended front door** for chaining "review these tickets, then implement the ones that pass" starting from *existing ticket keys* (the entry point `/idea-to-pr` lacks, since its server orchestrator only accepts an idea).\n\nIt is a thin shim over the packaged `start-tickets` CLI, invoked with `--workflow review-and-implement`: this command performs the same connectivity check and branch enrichment as `/start-tickets`, then hands off to the identical worktree/base-refresh/model-routing/credential/cross-platform-spawn/`doctor` engine \u2014 reused verbatim. The review\u2192implement halt-gate decision logic lives entirely in the spawned `/review-and-implement` session, never here or in the CLI. Using `start-tickets --workflow review-and-implement` directly (documented in `commands/src/start-tickets.md`) remains available as the lower-level launcher seam this command drives.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that resolves connectivity and per-ticket branch names, then spawns N parallel Worktrunk worktrees via the packaged CLI\'s `--workflow review-and-implement` seam. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys and pass-through flags:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). A single key and multiple keys flow through the identical ordered `keys: string[]` path \u2014 there is no separate single-ticket code path. If zero keys are found, stop immediately and display:\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /review-and-start [flags] <KEY> [KEY ...] (e.g., /review-and-start BAPI-248 BAPI-250)\n ```\n - **`--auto`**: a single chain-level flag. When present, it is forwarded to `start-tickets --auto`, which threads it into BOTH the review and implementation phases of every spawned `/review-and-implement <KEY>` session.\n - **`--rounds`**: normalize `--rounds 1`/`--rounds=1` and `--rounds 2`/`--rounds=2` to `--rounds=<n>`; reject any other value. This value is forwarded to the review phase of every spawned session.\n - **Selected agent**: validate `--agent <name>` / `--agent=<name>` against `claude|cursor-agent|codex`. It takes precedence over an optional `--host <platform>` / `--host=<platform>`. When no agent was supplied, resolve `--host` through the same host-aware capability matrix as `/start-tickets`; a non-runnable host returns manual-continuation guidance. In a Codex session with neither flag, set `--agent codex --host codex` for Stage 3; otherwise forward every supplied flag to the CLI so it makes the authoritative final decision.\n - **Command host default: none.** This line is specialized to `cursor` only in the Cursor command asset. If neither `--agent` nor `--host` was supplied and this default is `cursor`, forward `--host cursor` to the CLI. Never infer the host from the presence of a `.cursor` directory; an explicit `--agent` or `--host` always wins.\n - **User-supplied base branch**: validate `--base-branch <branch>` / `--base-branch=<branch>` using the same non-empty/\u2264255-character/no-leading-dash/no-control-character rules as `/start-tickets` Stage 0. A user-supplied value takes precedence over Stage 2a\'s `config_field` resolution below.\n - **`--max-parallel N`**: a positive integer; the CLI\'s own default (3) applies when omitted.\n - **`--dry-run`**: a boolean toggle.\n - **`--guard-stale-branch`**: a boolean toggle forwarded verbatim to `start-tickets`. It turns on the F7 stale-branch guard: a pre-existing `feature/<KEY>` branch whose tip is **not** an ancestor of the resolved base is refused with a `create-failed` row and the stale-worktree remedy, instead of being silently reused. Pass it on the **LLM-conductor pilot cut path**, where deterministic slugs mean a re-cut epic re-derives the same branch name and would otherwise land on the previous run\'s branch. Omitted by default, so an ordinary human run keeps reusing a same-named branch.\n - Reject malformed input before proceeding: an unsupported flag, a ticket key not matching `[A-Z]+-[0-9]+`, an unsupported `--agent`/`--rounds`/`--base-branch` value \u2014 stop and report the malformed argument.\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged `start-tickets` CLI (driven with `--workflow review-and-implement` in Stage 3) owns all platform pre-flight checks, refreshed worktree creation, secret-free credential provisioning, difficulty\u2192model-tier routing, and cross-platform terminal spawning \u2014 identical to `/start-tickets`. This command does not duplicate or re-verify any of that; see `commands/src/start-tickets.md` Stage 1 for the full prerequisite/provisioning description it drives. This command does not expose `--conductor`, `--terminal`, or `--no-refresh-main`/`--no-refresh-base` \u2014 it stays scoped to the front-door flags listed above. `--guard-stale-branch` is the one safety flag it *does* forward: `--conductor` implies that guard in the CLI, but this front door must not enable the v2 conductor context (a run id, hook provisioning, run-started emission) that `--conductor` also switches on, so the guard is reachable here on its own. Proceed to Stage 2.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve Base Branch\n\nIdentical to `/start-tickets` Stage 2a: if the user supplied `--base-branch`, use it and skip the `config_field` lookup entirely. Otherwise call `config_field` (`operation: "get"`, `field_name: "base_branch"`) and treat a non-empty string `value` as the configured base branch; treat `null`, an empty/whitespace-only string, an HTTP `400` response, or any other lookup failure as "unset" and omit `--base-branch` from Stage 3 (the CLI then defaults to `main`). When forwarding a resolved value into the Stage 3 Bash invocation, apply the same mandatory single-quote escaping rule as `/start-tickets` (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating it into the command string \u2014 never expand it unquoted.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nIdentical to `/start-tickets` Stage 2b: for each ticket key without a user-provided `--branch` override, call `get_ticket` (`ticket_number` set to the key, `save_locally: false`), slugify its `summary` (lowercase, collapse runs of `[^a-z0-9]+` to a single `-`, trim leading/trailing dashes, truncate to at most 40 characters preferring a dash boundary), and build `feature/<KEY>-<slug>`. On any per-key failure (404, network error, empty slug), emit a warning and let the CLI apply its default `feature/<KEY>` for that key only; never stop the pipeline. Credentials never reach the Bash-spawned CLI \u2014 this enrichment stays in the command, exactly as in `/start-tickets`.\n\nThis stage is **non-critical** \u2014 warnings are acceptable; the pipeline continues with the fallback branch for any key that fails enrichment.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the Stage 3 shell-out, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n### Dry-run intended-actions preview\n\nWhen the user passed `--dry-run`, print an intended-actions preview **before** the Stage 3 shell-out. The preview names the resolved launcher explicitly, so an operator can see at a glance whether this run will use the published package or a local build:\n\n```\nIntended actions (dry run \u2014 nothing is created)\n Launcher: <the resolved <launcher> value, exactly as it will be escaped into the shell command>\n Command: <launcher> start-tickets --workflow review-and-implement <flags> <ticket-keys>\n Tickets: <the ordered ticket keys>\n```\n\nWhen the override is set, show the **escaped** launcher exactly as it will appear in the Bash command string, so the preview and the command cannot disagree. `--dry-run` is then forwarded to the resolved launcher, which does the previewing of its own actions: **no worktree is created, no agent tab or session is opened, and no worker process is spawned.** Do not state or imply otherwise.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke exactly **one** CLI invocation covering every requested key \u2014 never a per-key loop:\n\n```\n<launcher> start-tickets --workflow review-and-implement <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` forwards `--auto` (only if supplied), the normalized `--rounds=<n>` (only if supplied), `--agent <name>` and `--host <platform>` (only if supplied; forward both when both were supplied so the CLI enforces agent precedence), the Cursor command asset\'s `--host cursor` default only when neither was supplied, `--max-parallel N` (only if supplied), `--dry-run` (only if supplied), and `--guard-stale-branch` (only if supplied) verbatim.\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` only when a value was resolved in Stage 2a; otherwise omitted entirely.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2b.\n- `<ticket-keys>` is the original ordered list of keys parsed in Stage 0.\n\nCredentials are never placed in this shell command \u2014 the same secret-free provisioning path `/start-tickets` uses applies here unchanged.\n\nExample, single key, hands-off:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=1 --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step BAPI-248\n```\n\nExample, multiple keys, manual per-ticket gate (no `--auto`):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step --branch BAPI-250=feature/BAPI-250-deep-research-durability BAPI-248 BAPI-250\n```\n\nExample, LLM-conductor pilot cut (epic base plus the stale-branch guard):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --guard-stale-branch --base-branch \'epic/BAPI-902\' --branch BAPI-902=feature/BAPI-902 BAPI-902\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket: `KEY branch=BRANCH status=STATUS`, optional trailing `path=PATH`) and reformat it as a markdown table, identical in shape to `/start-tickets` Stage 4 (`Ticket | Branch | Status`; statuses `dry-run`, `spawned`, `create-failed`, `spawn-failed`).\n\nThis report describes **worktree/spawn status only**. State explicitly in the report:\n- Each successfully spawned session first runs `/review-ticket` and its own per-ticket halt gate; on successful review and approval (or `--auto`), it then opens a **fresh implementation session** (`/implement-ticket`) that reuses the same worktree. Review and implementation run in two separate agent sessions, not one shared context. This parent session does not observe or report either the review outcome or the later implementation outcome. Never claim or imply that review or implementation has completed.\n- When `--dry-run` was passed, state explicitly that no worktrees were created and no tabs/sessions were opened.\n- As a fixed trade-off of this design: the review session\'s difficulty-derived model tier is snapshotted and reused (via `--tier`) by the fresh implementation session, so both phases run on the same tier; and a non-auto (including a non-auto scheduled) run pauses at each ticket\'s own gate rather than proceeding hands-off. For an `--auto` run, each ticket\'s eventual implementation completion is observed in its own fresh implementation tab, not here.\n\nIf the CLI reported any `create-failed`/`spawn-failed` statuses, or Stage 2b emitted enrichment warnings, list them under a `Warnings:` heading. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the underlying runbook this command builds on, and `commands/src/review-and-implement.md` for the per-ticket review\u2192gate\u2192implement composition each spawned session runs.\n',"review-ticket.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"baseSha","flag":"--base-sha","type":"string"}]}\n---\n\n# Review Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. Parse `$ARGUMENTS` to extract:\n - A required `ticket_key` matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`).\n - An optional position-independent `--auto` flag.\n - An optional position-independent `--rounds=<n>` argument, where `<n>` is `1` or `2`.\n - An optional position-independent `--no-refresh-base` flag.\n - An optional position-independent `--base-branch=<branch>` argument.\n - An optional position-independent `--base-sha=<sha>` argument.\n\n Tokenize `$ARGUMENTS` on whitespace. The first token matching the Jira key pattern is the `ticket_key`; ignore any additional ticket-key tokens. The presence of a `--auto` token (anywhere in `$ARGUMENTS`) sets `auto_approve` to `true`. A token matching `--rounds=1` sets `rounds` to `1`; a token matching `--rounds=2` sets `rounds` to `2`. If no `--rounds` token is present, leave `rounds` unset (omitted) so the backend\'s difficulty-adaptive review policy can decide the review shape when enabled for this repo; when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved, the backend falls back to a full premium second-opinion review. The presence of a `--no-refresh-base` token sets `no_refresh_base` to `true`. A token matching `--base-branch=<branch>` sets `base_branch` to `<branch>`. A token matching `--base-sha=<sha>` sets `base_sha` to `<sha>`.\n\n `--auto`, `--rounds`, `--no-refresh-base`, `--base-branch`, and `--base-sha` are all independent and may be supplied in any combination.\n\n If `$ARGUMENTS` is empty or contains no token matching the Jira key pattern, stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123 [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n If a `--rounds` token is present but its value is not `1` or `2`, stop and display:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2 (omit to let the backend decide adaptively; default falls back to a full review)\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"review-ticket"`\n - `variables`: `{ "ticket_key": "<ticket_key>", "base_branch": "<base_branch or "">", "base_sha": "<base_sha or "">", "no_refresh_base": "<"true" if --no-refresh-base was passed, else "">" }`\n - `auto_approve`: `true` \u2014 only when `--auto` was passed; otherwise omit this field entirely.\n - `rounds`: `1` \u2014 only when `--rounds=1` was explicitly passed on the command; `2` \u2014 only when `--rounds=2` was explicitly passed. When `--rounds` was NOT supplied, omit `rounds` entirely (do not pass `rounds: null`) so the backend\'s difficulty-adaptive review policy can choose the review shape when enabled for this repo. An explicit `rounds` value is forwarded to the backend and forces the review shape: `--rounds=1` requests a single-pass review, and `--rounds=2` forces the full second-opinion review even when adaptive routing is enabled. Do NOT translate `rounds` into `skip_steps` \u2014 the backend executor now owns all round orchestration (including any second-opinion rounds), so the recipe carries a single `request_ticket_review` step and you never pass `skip_steps` for round control.\n\n Example combined-mode payload (`--rounds=1 --auto --base-branch=develop`):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "develop", "base_sha": "", "no_refresh_base": "" },\n "auto_approve": true,\n "rounds": 1\n }\n ```\n\n Example explicit full-review payload (`--rounds=2`), which forces the full second-opinion review even if adaptive routing is enabled for this repo:\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" },\n "rounds": 2\n }\n ```\n\n Example adaptive payload (no `--rounds`), which lets the backend policy executor decide the review shape (falling back to a full premium review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" }\n }\n ```\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',"review-tickets.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"review","flag":"--review","type":"string","repeatable":true},{"name":"agent","flag":"--agent","type":"string"},{"name":"model","flag":"--model","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"}]}\n---\n\n# Review Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command is the single review front door for one or more Jira ticket keys. **Exactly one resolved ticket key** is reviewed **inline in the current session**: it invokes `/review-ticket <KEY>` directly, with no terminal tab, no worktree, and no packaged CLI subcommand. **Two or more resolved ticket keys** fall back to the packaged `@bridge_gpt/mcp-server` CLI subcommand `review-tickets`, which opens one terminal tab per ticket running the selected agent with `/review-ticket <KEY> [--auto] [--rounds=<1|2>]`. By default `--rounds` is omitted so the backend routes each review by difficulty (difficulty-adaptive review); pass an explicit `--rounds=1|2` (globally or per ticket) to force the review shape. Unlike `/start-tickets`, the multi-key path creates no Worktrunk worktrees \u2014 but it requires `git` on PATH: the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch before spawning tabs (BAPI-474), so every spawned review grounds its codebase evaluation against the same freshly-fetched base tree. Pass `--no-refresh-base` to skip this and restore the prior git-free, in-place-grounded behavior. The single-key inline path performs no batch fetch and pins no shared `base_sha` \u2014 `/review-ticket` grounds its own review independently (see Stage 2 below).\n\n---\n\n# Instructions\n\n## Stage 0 \u2014 Parse Arguments, Validate, and Check Connectivity\n\n1. **Parse `$ARGUMENTS`** to extract ticket keys, review modes, and pass-through flags:\n\n - **Recognized flags and their values are consumed first.** `--review`, `--agent`, `--model`, `--base-branch`, and every other value-taking flag, plus the value token immediately following it (or the `=value` suffix on the same token), are removed from consideration before ticket keys are resolved. **Every remaining whitespace-separated token** matching `[A-Z]+-[0-9]+` (e.g., `BAPI-1`) is a resolved positional ticket key. Jira-key-shaped text that appears **inside** a flag value (e.g. the `BAPI-1` inside `--review BAPI-1=auto,rounds=1`, or a coincidentally key-shaped `--model` or `--agent` value) is never added to the positional ticket-key list \u2014 it was already consumed as part of that flag. If zero resolved positional keys remain, stop immediately and display:\n ```\n No ticket keys found. Expected one or more keys like BAPI-1.\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n - **Review mode interpretation** (per ticket or global):\n - `auto` or `--auto` \u2192 per-ticket or global auto-approve flag.\n - `single-pass`, `one-pass`, `rounds=1`, or `--rounds=1` \u2192 `rounds=1` (single-pass review).\n - `full`, `two-pass`, `rounds=2`, or `--rounds=2` \u2192 `rounds=2` (full second-opinion review).\n - **omitted rounds \u2192 `adaptive`**: when no rounds mode is given for a ticket, do NOT choose a round count \u2014 leave it adaptive so the backend\'s difficulty-adaptive review policy decides the shape. `adaptive` is a distinct mode from `1` and `2`.\n - `--auto` and `--rounds` are independent: both may apply to the same ticket.\n - **Repeated or conflicting `--review KEY=...` overrides for the same key are last-wins**: the last occurrence of an override targeting a given key is the one that applies, matching this repository\'s existing last-wins CLI convention. A sole-key override such as `--review BAPI-123=auto,rounds=2` resolves to exactly the same state as explicit global `--auto --rounds=2` for that key.\n\n The backend executor now owns all review round orchestration (including any second-opinion rounds) server-side \u2014 there is no client-side step to skip, so this command never sends `skip_steps` and never translates `--rounds=1` into skipping a second-opinion step. An explicit `--rounds` value forwarded to each spawned `/review-ticket` forces the review shape (`1` = single pass, `2` = full second-opinion review). A spawned `/review-ticket` invoked without any `--rounds` (the default) lets the backend\'s difficulty-adaptive review policy decide the shape (falling back to a full premium second-opinion review when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved). This command therefore forwards **no** `--rounds` by default; it only forwards `--rounds` when the caller explicitly supplies a rounds mode (globally via `--rounds`, or per ticket via `--review`).\n\n - **Homogeneous modes** (two or more resolved keys only): when all tickets share the same auto and rounds mode, translate into global `--auto` (if all auto) and, only when all tickets share the same *explicit* rounds value, global `--rounds=1|2`. When all tickets are adaptive (no rounds given), omit `--rounds` entirely \u2014 do not synthesize a default.\n\n - **Heterogeneous modes** (two or more resolved keys only): when tickets differ in auto or rounds mode, translate into repeatable `--review KEY=auto,rounds=N` overrides. Only emit a `rounds=N` subtoken for tickets given an explicit `1`/`2`; adaptive tickets carry no `rounds` subtoken (a bare `--review KEY=auto` if they are auto, or no override at all). Do NOT set global `--auto` when only some tickets are auto-approved, and do NOT set global `--rounds` when only some tickets have an explicit rounds value.\n\n - **Pass-through flags**: collect `--dry-run`, `--max-parallel N`, `--agent claude|cursor-agent|codex`, `--model VALUE`, `--no-refresh-base`, and `--base-branch VALUE` if supplied. In a Codex session, pass `--agent codex` when no agent was supplied. Codex batch sessions use `$bridge-review-ticket` and require the current repository\'s skill bundle and repository-bound Bridge MCP scope before spawn.\n\n - **Validate `--base-branch`** (applies before dispatch, for both the single-key and multi-key paths): accept either `--base-branch VALUE` or `--base-branch=VALUE`. After trimming, the value must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`). Reject a malformed value:\n ```\n Invalid --base-branch value: <reason>\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n2. **Connectivity check**: after parsing and validation succeed (i.e. at least one ticket key resolved and `--base-branch`, if supplied, is valid), and **before** the ticket-count dispatch below and before either dispatch path\'s `--dry-run` handling, call the `ping` MCP tool. If it fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n## Stage 1 \u2014 Dispatch by Resolved Ticket Count\n\nCount the resolved positional ticket keys from Stage 0:\n\n- **Exactly one key** \u2192 Stage 2, the single-key inline path. Do not resolve the packaged-CLI launcher, spawn a terminal tab, or perform the batch base-branch fetch/pin on this path.\n- **Two or more keys** \u2192 Stage 4, the packaged CLI fan-out (unchanged from today).\n\n(Zero keys already stopped in Stage 0 and never reaches this dispatch.)\n\n## Stage 2 \u2014 Single-Key Inline Review\n\nThis path reviews the sole resolved key **in the current agent session**, mirroring how `/review-and-implement` invokes `/review-ticket` inline.\n\n1. **Construct the inline invocation** in this deterministic order: `/review-ticket <KEY>`, followed conditionally by:\n - `--auto` \u2014 forwarded when either the global `--auto` flag or the resolved per-key `--review KEY=...` override enables auto-approve for this key.\n - `--rounds=1` or `--rounds=2` \u2014 forwarded only when the resolved mode for this key (global `--rounds`, or a `rounds=N` subtoken in a `--review KEY=...` override targeting this key) contains an explicit round count. When the resolved mode is adaptive (no explicit rounds given anywhere for this key), omit `--rounds` entirely so the backend\'s difficulty-adaptive review policy decides the shape.\n - `--no-refresh-base` \u2014 forwarded unchanged when supplied.\n - `--base-branch=<value>` \u2014 the validated Stage 0 value, normalized to the `/review-ticket`-accepted `--base-branch=<value>` form, when supplied.\n\n `--agent`, `--model`, and `--max-parallel` are accepted for interface stability (they are fan-out concerns with no meaning for a single inline review) but are **never** added to the constructed invocation. When any of the three is supplied, print exactly one advisory line to **stderr** before continuing:\n ```\n Note: --agent, --model, and --max-parallel do not apply to a single-key inline review and were ignored.\n ```\n\n2. **`--dry-run`**: after the unconditional Stage 0 `ping` and any stderr inert-flag advisory above, write only the constructed invocation to **stdout**:\n ```\n /review-ticket <KEY> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=<value>]\n ```\n Then stop immediately. Do not invoke `/review-ticket`, resolve the packaged-CLI launcher, open a terminal tab, perform the batch fetch/base-SHA pin, or enter Stage 5\'s summary rendering.\n\n3. **Otherwise**, invoke the constructed `/review-ticket` command **inline, in this same session** \u2014 the same composition shape `/review-and-implement` uses for its own inline review phase. This path performs **no** batch `git fetch` and pins **no** shared `base_sha`: `/review-ticket` independently grounds its review through `fresh_base` materialization of a fresh `git archive` from `origin/<base>` (see `docs/BAPI-474-ground-review-against-fresh-base.md`), and the forwarded `--no-refresh-base` / `--base-branch` flags control that independent grounding.\n\n4. When the inline reviewer completes, the `/review-tickets` orchestration for this invocation is finished. Do not parse or render packaged-CLI `Summary:` output \u2014 that format belongs to the multi-key path only.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nUsed only by the multi-key fan-out (Stage 4) below \u2014 the single-key inline path (Stage 2) never resolves or uses this launcher.\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Stage 4 \u2014 Invoke the Packaged CLI (two or more keys)\n\nUse the **Bash tool** to invoke exactly one CLI invocation:\n\n```\n<launcher> review-tickets [--auto] [--rounds=1|2] [--review KEY=auto,rounds=N ...] [--agent <name>] [--model <alias>] [--max-parallel N] [--dry-run] [--no-refresh-base] [--base-branch BRANCH] KEY [KEY ...]\n```\n\n- `review-tickets` runs all tabs from the current repository cwd \u2014 it creates no worktrees.\n- The command never runs `wt` or `git-wt` \u2014 but it now requires `git` on PATH (BAPI-474): before spawning any tabs, the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch, so a mid-batch `origin` advance can never mix bases within one run. Pass `--no-refresh-base` to skip the fetch and restore the prior git-free, in-place-grounded behavior.\n- Prerequisites: macOS `osascript` + `git`, Windows `wt.exe` or PowerShell + `git`, Linux `tmux` + `git` (git is not required when `--no-refresh-base` is passed).\n\nPass through the CLI\'s stdout and stderr verbatim. If the CLI exits non-zero, treat it as a critical failure and report the exit code and error output.\n\n## Stage 5 \u2014 Final Report (two or more keys)\n\nOnce the CLI exits 0, parse its `Summary:` lines (each shaped like `KEY auto=<true|false> rounds=<1|2|adaptive> agent=<agent> model=<alias|default> status=<status>`) and render as a markdown table (`rounds=adaptive` means the backend chose the shape by difficulty):\n\n```\n| Ticket | Auto | Rounds | Agent | Model | Status |\n|----------|-------|----------|--------|---------|---------|\n| BAPI-1 | false | adaptive | claude | default | spawned |\n| BAPI-2 | true | 1 | claude | default | spawned |\n```\n\nRender any CLI `Warnings:` lines below the table. If there were none, omit the warnings section.\n',"run-tests.md":'Run the project\'s full test suite (unit and E2E) using the project-configured test stacks, triage failures, fix test-code issues, and produce a structured health-check report.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command discovers how to run tests by reading per-project configuration from the Bridge API, not from hardcoded paths. Stages run only when the project has the corresponding stack configured.\n\n## Stage 0 \u2014 Argument Parsing and Setup\n\n1. **Parse `$ARGUMENTS`** for optional flags. Supported flags:\n - `--skip-e2e` \u2014 skip the E2E test stage even if an E2E stack is configured (e.g., when no local server is running)\n - `--unit-only` \u2014 shorthand that implies `--skip-e2e`\n\n Resolve flags to boolean variables:\n - Start with: `run_unit = true`, `run_e2e = true`\n - If `--unit-only` is present: set `run_e2e = false`\n - If `--skip-e2e` is present: set `run_e2e = false`\n - Unknown flags: note them in the final report as "Unrecognized flag ignored" but do not fail\n\n2. **Generate a run timestamp** using the current date and time in `YYYY-MM-DD-HH-MM` format (e.g., `2026-03-10-14-35`). Store this as `run_timestamp`. Both output documents will use this value.\n\nThis stage has no failure conditions \u2014 proceed to Stage 1.\n\n## Stage 1 \u2014 Resolve Project Config via MCP\n\nRead the per-project test setup from the Bridge database. Every subsequent stage is driven by what these calls return.\n\n1. **Resolve docs directory**: Call the `ping` MCP tool (no parameters) and read `docs_dir` from its first (JSON) content item. Store that path as `docs_dir`.\n\n2. **Read unit-test stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_stack`. Store the returned value as `unit_stack` (may be null/empty).\n\n3. **Read unit-test instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_instructions`. Store the returned value as `unit_instructions` (may be null/empty).\n\n4. **Read E2E stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_stack`. Store as `e2e_stack`.\n\n5. **Read E2E instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_instructions`. Store as `e2e_instructions`.\n\n6. **Compute configuration booleans**:\n - `unit_configured` = `true` if either `unit_stack` or `unit_instructions` is a non-empty string; otherwise `false`\n - `e2e_configured` = `true` if either `e2e_stack` or `e2e_instructions` is a non-empty string; otherwise `false`\n\n7. **Create the output directory**:\n ```\n mkdir -p {docs_dir}/testing/\n ```\n If this fails, stop immediately and report: `Cannot create output directory {docs_dir}/testing/ \u2014 check permissions.`\n\nIf any MCP call fails (e.g., the API is unreachable or returns 4xx/5xx), stop immediately and report which call failed. Do not fall back to hardcoded commands \u2014 the whole point of this command is that test setup lives in config.\n\n## Stage 2 \u2014 Unit / Standard Tests\n\nIf `run_unit` is `false`, skip this stage and record: `Unit tests: SKIPPED \u2014 run_unit was set to false (this should not happen in normal use; report as a bug).`\n\nIf `unit_configured` is `false`, skip and record:\n```\nUnit tests: SKIPPED \u2014 no unit_testing_stack or unit_testing_instructions configured for this repo. Configure them with /learn-repository (a project admin must run it) or the project setup UI before running /run-tests.\n```\n\nOtherwise:\n\n1. Read `unit_instructions` carefully. It is the source of truth for **how to run unit tests in this repo** \u2014 runner binary, paths, environment activation, sub-suites (if the project distinguishes "unit" from "integration", both belong in this stage), and any flags. Pair it with `unit_stack` (a short label, e.g., `Pytest`, `Jest + React Testing Library`) for context.\n\n2. **Derive the test command(s)**: Extract the literal shell commands the instructions describe. If the instructions describe multiple sub-suites (e.g., a fast unit batch and a slower integration batch), plan to run each as a **separate batch** in the order described. Do not invent runners or paths that the instructions do not mention.\n\n3. **If the instructions do not specify any runnable command**, skip and record:\n ```\n Unit tests: SKIPPED \u2014 unit_testing_instructions does not describe how to invoke tests; update that field with /teach-bridge, or re-run /learn-repository (a project admin must run it).\n ```\n\n4. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output of each batch, including the runner\'s summary line (e.g., `47 passed, 3 failed in 12.4s` or `Tests: 5 failed, 22 passed`).\n\n5. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Stage 3 \u2014 E2E Tests\n\nIf `run_e2e` is `false`, skip this stage and record: `E2E tests: SKIPPED \u2014 --skip-e2e or --unit-only flag was set.`\n\nIf `e2e_configured` is `false`, skip and record:\n```\nE2E tests: SKIPPED \u2014 no e2e_testing_stack or e2e_testing_instructions configured (the project may not have an E2E suite).\n```\n\nOtherwise:\n\n1. Read `e2e_instructions`. It is the source of truth for the E2E runner, spec paths, browser config, and any prerequisites. Pair with `e2e_stack` for context.\n\n2. **Detect server prerequisites**: If `e2e_instructions` indicates that a local server must be running (look for explicit cues such as "server", "running", "localhost", "started", "dev server", a URL, or a port number) and describes a readiness check, perform that check exactly as described. If the instructions describe a server prerequisite but do not describe a check, attempt the check the instructions imply (e.g., curl the URL the instructions mention) and skip the stage if it fails:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions describe a server prerequisite that wasn\'t met. Start the server per the instructions and re-run.\n ```\n\n3. **Derive the test command(s)** from the instructions, including any spec-directory batching the instructions specify.\n\n4. **If the instructions do not specify any runnable command**, skip and record:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions does not describe how to invoke tests; update that field with /teach-bridge, or re-run /learn-repository (a project admin must run it).\n ```\n\n5. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output and summary line of each batch.\n\n6. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Triage Logic\n\nFor every failing test, examine the test file and the code it tests. Classify as ONE of the following:\n\n### TEST-CODE ISSUE \u2014 fix it directly\n\nClassify as a test-code issue if ANY of the following applies:\n- The test asserts against a hardcoded value that no longer matches current behavior (outdated mock data)\n- The test imports or calls a function that was renamed, moved, or removed\n- The test asserts on a response field that was restructured\n- The test expects a specific error message string that has since changed\n- A fixture references a removed table column, model field, or schema member\n\n**Action**: Apply a minimal, targeted fix to the test file only. Then re-run just that failing test, using the runner described in the relevant instructions field (`unit_instructions` for unit-test failures, `e2e_instructions` for E2E failures). Adapt the runner invocation that the instructions provide to target a single test, following whatever convention the instructions or stack idiomatically use.\n\nIf the re-run **still fails** after your fix, do not make further edits \u2014 escalate to implementation-code issue instead and revert your change.\n\n### IMPLEMENTATION-CODE ISSUE (or UNCERTAIN) \u2014 document, do not fix\n\nClassify as an implementation issue if ANY of the following applies:\n- The production function raises an unexpected exception\n- A handler returns the wrong status code or response shape for a documented behavior\n- Business logic produces incorrect output that the test correctly asserts against\n- You are not confident the test is wrong\n\n**Action**: Do NOT modify any file outside the test directories described in `unit_testing_instructions` / `e2e_testing_instructions`. When in doubt about whether a path is test-only, treat it as production code and escalate. Record the failure in the implementation-issues document for the user to triage.\n\n## Stage 4 \u2014 Write Output Documents\n\n### Document 1: Test Run Report (always write this)\n\nWrite to: `{docs_dir}/testing/test-run-{run_timestamp}.md`\n\n```markdown\n# Test Run: {run_timestamp}\n\n## Configuration\n- Unit stack: {unit_stack or "not configured"}\n- E2E stack: {e2e_stack or "not configured"}\n- Unit tests: RUN | SKIPPED \u2014 (reason)\n- E2E tests: RUN | SKIPPED \u2014 (reason)\n\n## Unit Tests\n**Stack**: {unit_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**:\n- `path/to/test_file`: brief description of what was fixed\n- (or "none" if no fixes were needed)\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## E2E Tests\n**Stack**: {e2e_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## Overall Summary\n- Total test fixes applied: N\n- Suspected implementation issues found: N\n- Implementation issues document: {docs_dir}/testing/implementation-issues-{run_timestamp}.md\n (or "not created \u2014 no issues found")\n```\n\n### Document 2: Implementation Issues (only write if issues were found)\n\nIf at least one failure was escalated as an implementation-code issue, write to:\n`{docs_dir}/testing/implementation-issues-{run_timestamp}.md`\n\n```markdown\n# Suspected Implementation Issues: {run_timestamp}\n\nThese test failures were NOT fixed. They may indicate bugs in production code.\nA developer should investigate each item before merging.\n\n## Issue 1\n- **Test**: `path/to/test_file::test_function_name`\n- **Tier**: unit | e2e\n- **Failure message**: (paste the key assertion or exception line)\n- **Why not fixed**: (brief reasoning, e.g., "production function raises KeyError on valid input")\n\n## Issue 2\n...\n```\n\nIf no implementation issues were found, do NOT create this file.\n\n## Final Output\n\nAfter writing all documents, print this summary:\n\n```\nTest run complete: {run_timestamp}\nReport saved to: {docs_dir}/testing/test-run-{run_timestamp}.md\nImplementation issues: {docs_dir}/testing/implementation-issues-{run_timestamp}.md (if applicable)\nNo suspected implementation issues found. (if none)\n```\n',"scan-test-coverage.md":'Scan recently shipped tickets from git history and report which features have or could gain integration tests, and which can only be smoke tested.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nScan the git history for recently shipped tickets and, for each shipped feature, determine whether it already has an integration test, whether it *could* gain one (per this repo\'s conventions \u2014 a test that genuinely executes the system end-to-end via real database operations, real LLM calls, or real FastAPI routing), and \u2014 where integration testing is not possible \u2014 how it could be smoke tested so it still genuinely executes the system.\n\nThis is an **investigation and discovery** command. Describe features (citing code) and *how* they would be tested at a high level. Do **not** design tests in detail, build or edit any tests, or modify feature code. Orchestrate this run in the main thread, and **fan out one subagent per shipped feature** for the per-feature investigation.\n\nThe report is written to a **durable, committed** directory (`docs/test-coverage/`), and a marker file records when the analysis last ran so subsequent runs only inspect git history since the last run.\n\n## Stage 0 \u2014 Parse Arguments and Resolve Analysis Window\n\n1. Read `$ARGUMENTS`. All flags are optional and default-safe. If a flag is malformed, ignore it and add a warning:\n - `--since=YYYY-MM-DD` \u2014 override the window start date.\n - `--full` \u2014 ignore the marker and use a default lookback of 6 months.\n - `--limit=N` \u2014 cap the number of features investigated (parse `N` as an integer; ignore if not a valid integer).\n With no arguments, run **incrementally** from the marker.\n\n2. Set the durable directory to `docs/test-coverage/` (relative to the repo root) and the marker file to `docs/test-coverage/STATE.md`. This command deliberately does **not** use the configured docs directory (`ping`\'s `docs_dir`) \u2014 its default (`docs/tmp`) is ephemeral, and this report must be durable.\n\n3. Read `docs/test-coverage/STATE.md` if it exists. It records two values: `last_run_utc` (an ISO-8601 UTC timestamp) and `last_analyzed_commit` (a git commit SHA).\n\n4. Resolve the analysis window with this precedence:\n - If `--since=YYYY-MM-DD` was given, use `git log --since=<date>`.\n - Else if `STATE.md` provides `last_analyzed_commit`, use the commit range `<last_analyzed_commit>..HEAD`.\n - Else (first run, no marker), default to `git log --since=<3 months ago>` (mirrors the `/scan-tickets` default of 3 months). Format the date as `YYYY-MM-DD`. Example: if today is 2026-07-07, the default `--since` is `2026-04-07`.\n - `--full` overrides the above and uses a 6-month lookback (`--since=<6 months ago>`).\n\n5. Robustness of the marker: capture `head_sha` by running `git rev-parse HEAD`, and capture the current UTC timestamp now. These become the **new** marker values, but only write them after the report is successfully produced (Stage 4). If a stored `last_analyzed_commit` is not present in history (e.g. a rebase/rewrite), fall back to `git log --since=<the date part of last_run_utc>` and add a warning noting the fallback.\n\n6. Initialize tracking variables:\n - `features` = [] (one entry per shipped feature)\n - `warnings` = [] (per-item failures and fallbacks; the run never aborts on these)\n\n7. Display the resolved window, e.g. "Analyzing shipped features in `<range or --since date>` (HEAD = {head_sha})".\n\n## Stage 1 \u2014 Collect Shipped Features from Git History\n\n1. List merged commits in the resolved window with:\n ```bash\n git log <range> --first-parent --pretty=format:"%H|%h|%ad|%s" --date=short\n ```\n `--first-parent` yields roughly one entry per squashed PR merge.\n\n2. For each commit, extract the ticket key by matching `^BAPI-[0-9]+` against the subject. Group commits by ticket key. Commits with no ticket prefix (e.g. `Fix 500 on ...`) each become a standalone feature labeled as an "untracked change".\n\n3. For each group, collect the changed-file footprint across its commit(s) using `git show --stat <sha>` or `git diff --name-only`. This file footprint is the primary input to the per-feature investigation.\n\n4. Best-effort enrichment: for each ticket key, call the `get_ticket` MCP tool to fetch the ticket summary. This is **fail-open** \u2014 Jira tokens can be expired \u2014 so on any error, add a warning and continue without the summary. Do not abort.\n\n5. Build a `features` entry per group: `{ticket_key, subject, commit_shas, changed_files, jira_summary?}`. If `--limit=N` was given, keep only the first `N` features (most recent first).\n\n6. Display: "Found {count} shipped features to investigate."\n\n7. If `git log` returns no commits, skip to Stage 4 and write a report noting an empty window (and still refresh the marker).\n\n## Stage 2 \u2014 Investigate Each Feature (fan out subagents)\n\nFor each feature in `features`, launch an **Explore** subagent (batch several in parallel). Give each subagent the feature\'s `ticket_key`, `subject`, `changed_files`, and `jira_summary`, and instruct it to do read-only investigation only \u2014 no edits, no test design, no solutioning \u2014 and to return a structured finding.\n\nEach subagent must:\n\n1. Read the changed files and describe what the feature does in 2\u20134 sentences, with concrete `file:line` citations.\n\n2. Identify the feature\'s runtime surface \u2014 one or more of: real database operations (`postgres_client` / a DAL in `api/library/db/`), real LLM calls (`src/python/llms/ai_client.py`, `async_send_message_to_ai`), real FastAPI routing (a route handler under `api/routes/`), an MCP tool (`mcp_server/`), a shell-spawned / CLI flow, a frontend / Playwright surface, or pure logic / config / docs / tests.\n\n3. Check whether an **integration test already exists**: search `tests/integration/` for a mirror path or for references to the changed modules/functions. The reliable classifier is a path under `tests/integration/` plus `@pytest.mark.integration` or reliance on the `--run-integration` flag (conventions in `docs/claude/testing-integration.md`). Cite any test found.\n\n4. Classify the feature into exactly one `bucket`:\n - **`has_integration_test`** \u2014 already covered end-to-end; cite the existing integration test file.\n - **`integration_testable`** \u2014 no test yet, but the feature exercises real DB / LLM / routing and fits an existing `tests/integration/<area>/` pattern. Give a **high-level** approach only: which real entrypoint to call, which backend it would exercise, and the relevant cost/guard note (the gpt-5-nano override via `INTEGRATION_TEST_MODEL`; the local-DB `skipif` guard; `save_to_db=False`). Cite the entrypoint in code.\n - **`smoke_only`** \u2014 genuine end-to-end execution is possible but not as an automated integration test (e.g. MCP tool behavior inside a host, cross-platform terminal spawning, a headless agent session, or browser E2E). Describe how to smoke test it so it **genuinely executes the system**, citing the relevant runbook: the MCP smoke-test runbook under `mcp_server/smoke-test/`, `tests/mcp/`, `docs/claude/runbooks/self-install-smoke-test.md`, `docs/claude/runbooks/start-tickets-smoke-test.md`, or Playwright (`tests/playwright/`, which needs a running server plus `npm run build`).\n - **`not_testable`** \u2014 nothing to execute end-to-end (docs-only, a wording/comment change, pure config, or a test-only change); state why.\n\n5. Return a structured finding with these fields: `ticket_key`, `subject`, `description_with_cites`, `surface`, `bucket`, `existing_test`, `approach`, `why_not`.\n\nCollect all findings. If a per-feature subagent fails, add a warning and continue \u2014 never abort the whole run.\n\n## Stage 3 \u2014 Classify and Synthesize\n\n1. Deduplicate features that span multiple commits (merge by `ticket_key`).\n\n2. Sort each finding into the two required report sections:\n - **Section 1 \u2014 Integration Testing (covered or addable):** findings with `bucket` `has_integration_test` (sub-group "Already covered") or `integration_testable` (sub-group "Could be added").\n - **Section 2 \u2014 Not Integration-Testable:** findings with `bucket` `smoke_only` (sub-group "Smoke-testable \u2014 how") or `not_testable` (sub-group "Not testable \u2014 why").\n\n## Stage 4 \u2014 Write the Report and Update the Marker\n\n1. Create the `docs/test-coverage/` directory if it does not exist. Choose the report path `docs/test-coverage/REPORT-<YYYYMMDD>.md`; if a same-day file already exists, append `-<HHMMSS>` to avoid clobbering it.\n\n2. Write the report with this layout:\n - A title and a metadata block: generated-at UTC timestamp; the analysis window (`<from sha or since-date>` \u2192 `HEAD <head_sha>`); the feature count; and per-bucket tallies.\n - **Section 1 \u2014 Integration Testing: Covered or Addable.** One `### BAPI-NNN \u2014 <subject>` heading per feature, each with **What shipped** (with `file:line` citations), **Current coverage** (cite the existing integration test, or state "none"), and **How it could be integration tested (high level)**.\n - **Section 2 \u2014 Not Integration-Testable.** One heading per feature with the same feature description, plus **Why not integration-testable**, and \u2014 for `smoke_only` features \u2014 **How to smoke test (genuinely execute the system)** with the runbook citation.\n - A **Warnings** section listing each warning as a bullet \u2014 only if `warnings` is non-empty.\n\n3. **Only after** the report file is written successfully, update the marker `docs/test-coverage/STATE.md` with the new `last_run_utc` (the UTC timestamp captured in Stage 0) and `last_analyzed_commit` set to `head_sha`. This date/commit marker is what makes the next run incremental. If the report write fails, do not touch `STATE.md`.\n\n## Final Report\n\nPrint a short summary to chat:\n\n```\n**Test-coverage scan complete**\n\n* Features analyzed: {count}\n* Already covered by integration tests: {n_has}\n* Integration-testable (could be added): {n_addable}\n* Smoke-only: {n_smoke}\n* Not testable: {n_none}\n\nReport: docs/test-coverage/REPORT-<YYYYMMDD>.md\nMarker updated: last_analyzed_commit = {head_sha}\n```\n\nIf `warnings` is non-empty, add a "Warnings:" section listing each warning as a bullet. If there are no warnings, omit that section.\n',"scan-tickets.md":'$ARGUMENTS\n\n---\n\n# Instructions\n\nSynchronize recently-updated Jira tickets with the local `tickets` database table and backfill missing workflow state timestamps. Perform all work directly in the main thread.\n\n## Stage 0 \u2014 Parse Arguments and Calculate Date\n\n1. Read the value of `$ARGUMENTS`. If it is empty, whitespace-only, or not a valid integer, default `months_back` to `3`. If it contains multiple tokens, extract only the first token and attempt to parse it as an integer. If parsing fails, default to `3`.\n\n2. Calculate `updated_since` by subtracting `months_back` months from today\'s date. Format the result as `YYYY-MM-DD`. Example: if today is 2026-03-07 and `months_back` is 3, then `updated_since` is 2025-12-07.\n\n3. Display the parsed values: "Scanning tickets updated since {updated_since} (months_back = {months_back})"\n\n4. Initialize the following tracking variables:\n - `tickets_scanned` = 0 (total tickets fetched from Jira)\n - `newly_tracked` = 0 (tickets inserted into database for the first time)\n - `state_updated_list` = [] (list of objects with ticket key and fields updated)\n - `warnings` = [] (list of warning strings for any per-ticket failures)\n\n## Stage 1 \u2014 Fetch All Tickets from Jira\n\n1. Initialize an empty list `all_tickets` and set `offset` to `0`.\n\n2. Enter a pagination loop:\n - Call the `get_tickets` MCP tool with: `updated_since` set to the calculated date, `limit` set to `100`, and `offset` set to the current offset value.\n - Parse the JSON response. The response contains a `tickets` array of ticket objects. Each ticket object has a `ticket_number` field (the Jira key, e.g., `BAPI-42`), along with `summary`, `status`, `issue_type`, `assignee`, and `updated_at`.\n - Append all tickets from the response\'s `tickets` array to `all_tickets`.\n - If the number of tickets returned in this page equals `100`, increment `offset` by `100` and repeat the loop.\n - If fewer than `100` tickets are returned, exit the loop.\n\n3. Set `tickets_scanned` to the length of `all_tickets`.\n\n4. Display: "Fetched {tickets_scanned} tickets from Jira. Processing..."\n\n5. If the `get_tickets` call fails at any point during pagination, **stop** and report the error. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Track Each Ticket\n\n1. Iterate over each ticket in `all_tickets`. For each ticket:\n - Call the `track_ticket` MCP tool with `ticket_number` set to the ticket\'s `ticket_number` field. If the ticket object includes a `summary` field, pass it as the `description` parameter.\n - Inspect the response message. If the response indicates the ticket was newly created/inserted (look for words like "created" or "inserted" in the message, as opposed to "already exists" or "updated"), increment `newly_tracked` by 1.\n - If the `track_ticket` call fails for this ticket, add a warning to the `warnings` list (e.g., "Warning: Failed to track ticket {ticket_number}: {error}") and **continue** to the next ticket. Do not abort the scan.\n\n2. Display a brief progress indicator every 25 tickets, e.g., "Tracked {N} of {tickets_scanned} tickets..."\n\n## Stage 3 \u2014 Detect and Backfill Workflow State\n\nDisplay: "Checking workflow state for {tickets_scanned} tickets..."\n\nIterate over each ticket in `all_tickets`. For each ticket (referenced by its `ticket_number` field), perform the following sub-steps. Wrap the entire per-ticket block in error handling: if the `get_ticket_state` call or the subsequent `update_ticket_state` call fails for a ticket, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4a \u2014 Retrieve current state**: Call the `get_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key. The response contains:\n\n- Five timestamp fields (each is a timestamp string or null): `clarify_called`, `clarify_answered`, `critique_called`, `critique_answered`, `plan_generated`\n- Three boolean artifact flags: `has_clarifying_questions`, `has_critique`, `has_plan`\n\nIf the call returns a 404 or any error, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4b \u2014 Build fields_to_update list**: Initialize an empty `fields_to_update` list, then apply the following rules:\n\n- If `has_clarifying_questions` is `true` AND `clarify_called` is null -> add `"clarify_called"` to `fields_to_update`\n- If `has_clarifying_questions` is `true` AND `clarify_answered` is null -> add `"clarify_answered"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_called` is null -> add `"critique_called"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_answered` is null -> add `"critique_answered"` to `fields_to_update`\n- If `has_plan` is `true` AND `plan_generated` is null -> add `"plan_generated"` to `fields_to_update`\n\n**Sub-step 4c \u2014 Call update_ticket_state if needed**: If `fields_to_update` is non-empty, call the `update_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key and `fields` set to the `fields_to_update` array. If this succeeds, add an entry to `state_updated_list` recording the ticket key and the list of fields that were set. If `update_ticket_state` fails, add a warning to `warnings` and continue.\n\nDisplay a progress indicator every 25 tickets that includes the current ticket key, e.g., "Checked state for {TICKET-KEY} ({N} of {tickets_scanned} tickets)"\n\n## Stage 4 \u2014 Report Summary\n\n1. Calculate `state_updated_count` as the length of `state_updated_list`.\n\n2. Display the summary:\n\n ```\n **Scan complete**\n\n * Tickets scanned: {tickets_scanned}\n * Newly tracked: {newly_tracked}\n * State updated: {state_updated_count}\n ```\n\n3. If `state_updated_list` is non-empty, display a section titled "Updated tickets:" with one bullet per ticket showing the ticket key and the comma-separated list of fields that were set. Example:\n\n ```\n Updated tickets:\n * BAPI-101: clarify_called, clarify_answered\n * BAPI-105: critique_called, critique_answered, plan_generated\n ```\n\n4. If the `warnings` list is non-empty, display a section titled "Warnings:" listing each warning string as a bullet. Example:\n\n ```\n Warnings:\n * Warning: Failed to track ticket BAPI-99: Connection timeout\n * Warning: State query failed for BAPI-112: SQL error\n ```\n\n5. If there are no warnings, do not display the "Warnings:" section.\n',"start-tickets.md":'---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"agent","flag":"--agent","type":"string"},{"name":"host","flag":"--host","type":"string"},{"name":"workflow","flag":"--workflow","type":"string"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"guardStaleBranch","flag":"--guard-stale-branch","type":"boolean"}]}\n---\n\n# Start Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248 BAPI-250`) and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `start-tickets`, which creates a Worktrunk worktree for each key and opens one tab/session per worktree running the **selected agent**. An explicit `--agent` wins; otherwise `--host` resolves a declared runnable host (Claude Code, Cursor, or OpenAI Codex), and an omitted host retains the Claude default. Copilot and other non-runnable hosts return manual-continuation guidance. Codex receives a verified per-process Bridge MCP scope bound to its worktree.\n\nBecause the orchestration ships inside the `@bridge_gpt/mcp-server` npm package (not a repo-local script), this command works for every consumer \u2014 including projects that installed the package via `--init`.\n\nFor existing ticket keys, `/review-and-start <KEYS>` is the **recommended front door**: it supplies the same connectivity check and branch enrichment as this command, then drives this same packaged CLI with `--workflow review-and-implement` so each worktree reviews the ticket before implementing it. Using `start-tickets --workflow review-and-implement` directly (documented below) remains available as the lower-level launcher seam.\n\nStage 0 and Stage 1 are critical (stop on failure). Stage 2 is non-critical (per-ticket enrichment failures fall back to the default branch and continue). Stage 3 is critical (propagate the packaged CLI\'s exit code).\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that spawns N parallel Worktrunk worktrees and selected-agent sessions (Claude Code by default) via the packaged CLI. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys, pass-through flags, and branch overrides:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). If zero keys are found, stop immediately and display:\n\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /start-tickets [flags] <KEY> [KEY ...] (e.g., /start-tickets BAPI-248 BAPI-250)\n ```\n\n - **Pass-through flags**: collect any of `--agent <name>` (and the equals form `--agent=<name>`), `--host <platform>` (and the equals form `--host=<platform>`), `--terminal terminal|iterm`, `--dry-run`, `--auto`, `--no-refresh-main`, `--base-branch <branch>` (and the equals form `--base-branch=<branch>`), `--max-parallel N`, and `--guard-stale-branch` that the user supplied. These are forwarded verbatim to the CLI in Stage 3. `--guard-stale-branch` turns on the F7 stale-branch guard: a pre-existing `feature/<KEY>` branch whose tip is **not** an ancestor of the resolved base is refused with a `create-failed` row carrying the stale-worktree remedy, instead of being silently reused on top of another run\'s commits. It is off by default (a plain run keeps reusing a same-named branch) and is implied by `--conductor`. `--auto` makes each spawned agent run the selected workflow\'s slash command with `--auto` (hands-off); omit it to keep the spawned agents interactive.\n - **Selected agent**: track a `selected_agent` variable. Validate explicit `--agent <name>` / `--agent=<name>` against `claude`, `cursor-agent`, and `codex`; it always takes precedence. When omitted, accept optional `--host <platform>` and resolve it through the host capability matrix. A host lacking MCP configuration, custom commands, worktree provisioning, or session spawning returns manual-continuation guidance; it must never fall back to Claude. With neither flag in a Codex session, set `selected_agent` to `codex` and pass `--host codex` to the CLI. Otherwise retain the backward-compatible default `claude`.\n - **Selected workflow**: track a `selected_workflow` variable that defaults to `implement`. If the user passed `--workflow <value>` or `--workflow=<value>`, validate it against the two allowed values `implement` and `review-and-implement`, set `selected_workflow`, and reject any other value with the allowlist in the error. `implement` (the default) preserves today\'s behavior byte-for-byte \u2014 each spawned worktree runs `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]` instead, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` inside the same session. A single chain-level `--auto` applies to the selected workflow as a whole \u2014 under `review-and-implement` it auto-approves both the review and the implementation phase.\n - **Review rounds**: track a `review_rounds` value that defaults to unset. If the user passed `--rounds <n>` or `--rounds=<n>`, normalize it to `--rounds=1` or `--rounds=2` (reject any other value). `--rounds` is **review-only**: reject it (after parsing all flags, so flag order does not matter) if the final `selected_workflow` is not `review-and-implement`.\n - **User-supplied base branch**: track a `user_supplied_base_branch` boolean that defaults to `false`. If the user passed `--base-branch <branch>` or `--base-branch=<branch>`, set the boolean to `true` and capture the value. A user-supplied `--base-branch` value **takes precedence** over any value resolved from Bridge API config in Stage 2. Validate the user-supplied value before proceeding: after trimming surrounding whitespace it must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`); reject any malformed value with a clear error.\n - **User branch overrides**: collect any user-supplied repeatable `--branch KEY=BRANCH` flags. A user-provided override always takes precedence over Stage 2 enrichment for that key.\n - Reject malformed or unsupported combinations before proceeding: if a token looks like a flag but is not one of the supported flags, or a ticket key does not match `[A-Z]+-[0-9]+`, or a `--branch` value is not `KEY=BRANCH`, or `--agent`/`--host` is unrecognized, or `--workflow` names anything other than `implement`/`review-and-implement`, or `--rounds` is used outside `review-and-implement` or names anything other than `1`/`2`, or `--base-branch` fails the validation rules above, stop and report the malformed argument.\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `"status": "ok"`, stop immediately and display:\n\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged CLI runs its own per-platform pre-flight checks and then fetches `origin` and fast-forwards the local **configured base branch** (the value resolved in Stage 2 below, or `main` when none is configured) from `origin/<base>` so the new worktrees are based on an up-to-date base. The historical flag `--no-refresh-main` still controls this behavior \u2014 the flag name is preserved for backward compatibility, but it now skips refresh of whatever base branch resolves (default `main`). The required commands depend on the OS:\n\n- **macOS**: `wt`, `git`, `osascript`.\n- **Windows**: `git-wt`, `git`, Git for Windows / Git Bash (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash), and Windows Terminal **or** PowerShell.\n- **Linux**: `wt`, `git`, `tmux`.\n\nOn **Windows** the Worktrunk binary is `git-wt` (its winget alias), which is a different tool from Windows Terminal\'s `wt.exe`: the CLI uses `git-wt` to **create worktrees** and `wt.exe` to **open a tab**, and never conflates the two. On **Linux** the CLI opens one detached `tmux` session per ticket (a window is added if that ticket\'s session already exists); attach later with `tmux attach -t <session>`. An unsupported OS (not macOS/Windows/Linux) fails fast with a clear "unsupported platform" message.\n\nThis stage simply notes that the CLI will fail fast if any prerequisite is missing or if local `main` has diverged from `origin/main` \u2014 you do not need to verify anything separately here, and you must not run any pre-flight commands yourself. When the CLI\'s pre-flight fails it now hints the user to run the read-only diagnostics command `npx -y @bridge_gpt/mcp-server doctor`, which reports found/missing for every prerequisite on the current OS \u2014 the pre-flight set plus `uv` plus the selected agent\'s command \u2014 and prints the manual install command for each missing one. `doctor` is strictly read-only and never installs anything; never run install commands automatically on the user\'s behalf. The CLI does not call any Bridge API tools; all credential-bearing work (branch enrichment in Stage 2) stays in this command. Proceed to Stage 2.\n\nThe packaged CLI also performs **secret-free Bridge API MCP provisioning** inside each created worktree: synchronously after the worktree is created and **before the agent tab/session is opened**, it writes both `.mcp.json` (Claude Code) and `.cursor/mcp.json` (Cursor) pointing at the `mcp-invoke` shim. These registrations are **secret-free** \u2014 they contain no `env` block and no API key, because the shim resolves credentials at runtime. If a spawned agent (or difficulty\u2192model routing) reports missing Bridge API credentials, fix it by rerunning `/install-bridge` (its final stage persists the routing credential), by running `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate a key that lives only in `.mcp.json` / `.cursor/mcp.json`, or by adding a `bapi:<repo>` entry to the user-scoped credentials file (`~/.config/bridge/credentials.json`) \u2014 never by putting `BAPI_API_KEY` into the worktree `.mcp.json` or `.cursor/mcp.json` (that env is invisible to the Bash-spawned CLI).\n\nThis stage is **critical** in the sense that the CLI will abort if its pre-flight fails; you will see the error in Stage 3\'s output and must surface it.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve configured `base_branch`\n\nThe CLI must be told which branch to cut new worktrees from. Resolution order:\n\n1. If `user_supplied_base_branch` from Stage 0 is `true`, **skip the config-field lookup entirely** and use the user-supplied value. The user\'s explicit `--base-branch` always wins; never call `config_field` for `base_branch` in that case.\n2. Otherwise, call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch` (do not pass any other parameters; the tool resolves the repository from the MCP server\'s configured `BAPI_REPO_NAME`).\n3. Parse the response. Treat the result as the **configured base branch** only when the response is a JSON object whose `value` field is a non-empty string after trimming surrounding whitespace.\n4. Treat **all** of the following as "unset" \u2014 emit a single-line warning like `Warning: base_branch is unset; CLI will default to main` and **omit** the `--base-branch` flag entirely from the Stage 3 command (the CLI\'s own default is `main`):\n - `value` is `null`.\n - `value` is an empty string or a whitespace-only string.\n - The endpoint returns HTTP `400` (invalid field \u2014 happens before the registry includes `base_branch`).\n - The tool returns a network error, timeout, or non-JSON parse failure.\n - Any other lookup failure.\n5. When the configured value is usable, capture it in a `resolved_base_branch` variable. **Do not** stop the pipeline on a lookup failure; fall through to the CLI default.\n\nWhen forwarding `resolved_base_branch` into the Bash invocation in Stage 3, **shell-escape it safely**: replace every literal single quote `\'` in the value with the four-character sequence `\'\\\'\'`, then wrap the entire resulting string in single quotes (so the final argument looks like `\'<escaped-value>\'`). This is the standard POSIX single-quote escaping rule and is **mandatory** because `base_branch` is admin-configurable data that gets interpolated into a Bash command string; any unescaped single quote would otherwise break out of the surrounding quotes. Pass `--base-branch \'<escaped-value>\'` to the CLI as a single argv element \u2014 never expand the value unquoted into the command line.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nBranch enrichment happens here, in the command, **before** invoking the CLI \u2014 the `get_ticket` MCP tool runs inside the MCP server process, which holds the Bridge API credentials the shell-spawned CLI does not have. For each parsed ticket key that does **not** already have a user-provided `--branch` override:\n\n1. Call the `get_ticket` MCP tool with `ticket_number` set to the key and `save_locally` set to `false`.\n2. From the response, extract the `summary` field. Slugify it: lowercase the string, replace every run of non-alphanumeric characters (`[^a-z0-9]+`) with a single dash `-`, trim leading and trailing dashes, and truncate to at most `40` characters (cutting at a dash boundary if possible).\n3. The enriched branch name is `feature/<KEY>-<slug>`. Example: `BAPI-248` with summary `"Add PR rating pre-evaluation step"` becomes `feature/BAPI-248-add-pr-rating-pre-evaluation-step` (trimmed at 40 chars).\n4. If the `get_ticket` call fails for a particular key (404, network error, missing summary) or produces an empty slug, emit a single-line warning like `Warning: could not enrich BAPI-248, falling back to feature/BAPI-248` and let the CLI apply its default `feature/<KEY>` for that key only. Do NOT stop the pipeline.\n5. Build a list of `--branch <KEY>=<BRANCH>` arguments \u2014 one entry per key whose enrichment succeeded \u2014 and merge it with any user-provided overrides from Stage 0. **Do not** call `get_ticket` for keys that already have a user-provided override; those overrides win.\n\nThis stage is **non-critical** \u2014 warnings are acceptable, the pipeline continues with the fallback default for any key that fails. Do not call the Bridge API from the CLI itself; the CLI never has credentials.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke the packaged CLI. Build the command line as:\n\n```\n<launcher> start-tickets <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` are the supported flags collected in Stage 0 (`--agent`, `--host`, `--terminal`, `--dry-run`, `--auto`, `--no-refresh-main`, `--max-parallel`, `--guard-stale-branch`), forwarded verbatim. Forward `--agent <name>` or `--host <platform>` only if the user supplied it; otherwise omit both and the CLI defaults to `claude`. When both were supplied, forward both so the CLI can enforce its documented `--agent` precedence. Forward `--auto` only if the user supplied it.\n- Forward `--workflow <selected_workflow>` only when the user explicitly passed `--workflow`; otherwise omit it and the CLI defaults to `implement`. Forward the normalized `--rounds=<n>` from Stage 0 only when the user supplied it (which Stage 0 already guarantees is only possible under `review-and-implement`).\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` (single-quoted using the Stage 2a escaping rule) **only when** the user supplied `--base-branch` in Stage 0 **or** Stage 2a\'s `config_field` lookup returned a non-empty configured value. When the configured value is unset / lookup fails / user did not supply one, **omit this flag entirely** so the CLI\'s own default (`main`) takes effect.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2 (enrichment results merged with user overrides; omit any key whose enrichment failed and had no user override).\n- `<ticket-keys>` is the original list of ticket keys parsed in Stage 0, space-separated and in the original order.\n\nExample for two tickets after successful enrichment, throttled to 2 concurrent worktrees:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets \\\n --max-parallel 2 \\\n --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step \\\n --branch BAPI-250=feature/BAPI-250-deep-research-durability \\\n BAPI-248 BAPI-250\n```\n\nExample launching Cursor Agent instead of the default Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\nExample cutting worktrees from a non-`main` base (either user-supplied via `--base-branch develop` in Stage 0 or resolved from Bridge API config in Stage 2a):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --base-branch develop BAPI-248\n```\n\nExample using the lower-level review-and-implement workflow directly (the `/review-and-start` command is the recommended front door for this; this form is documented here as the advanced launcher seam it drives):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=2 BAPI-248\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket in the form `KEY branch=BRANCH status=STATUS`, with an optional trailing `path=PATH`) and reformat it as a markdown table:\n\n```\n| Ticket | Branch | Status |\n|----------|-----------------------------------------------------|----------|\n| BAPI-248 | feature/BAPI-248-add-pr-rating-pre-evaluation-step | spawned |\n| BAPI-250 | feature/BAPI-250-deep-research-durability | spawned |\n```\n\nStatus values are `dry-run`, `spawned`, `create-failed`, and `spawn-failed`. This table (and the report as a whole) describes **worktree/spawn status only** \u2014 it must never claim that review or implementation itself has completed; that work happens later, independently, inside each spawned session.\n\nCompute `spawned_command` from `selected_workflow`: `/implement-ticket <KEY>` or `/review-and-implement <KEY>` for Claude/Cursor, `$bridge-implement-ticket <KEY>` or `$bridge-review-and-implement <KEY>` for Codex. Append `--auto` when supplied and, for `review-and-implement`, the normalized `--rounds=<n>` when supplied; Codex review chains also carry `--agent codex --host codex`. End the report with the worktree-first explanation, rendered for the tracked `selected_agent` and `spawned_command`. When `selected_agent` is `claude` (the default):\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`claude \'<spawned_command>\'` inside its already-created worktree, which launches\nClaude Code with the starter prompt as its first message. Switch to each tab \u2014 or on\nLinux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `cursor-agent`, render the same explanation but with the Cursor handoff \u2014 do **not** claim it launches Claude Code:\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`cursor-agent \'<spawned_command>\'` inside its already-created worktree, which\nlaunches Cursor Agent with the starter prompt as its first message. Switch to each\ntab \u2014 or on Linux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `codex`, report the `$bridge-implement-ticket` or `$bridge-review-and-implement` starter prompt and the per-worktree MCP scope. Under `review-and-implement`, each spawned session independently runs the host\'s review-ticket command, pauses at its per-ticket halt gate (unless chain-level `--auto` was passed), and only then opens a fresh implementation session. Do not report that either phase succeeded from this parent session.\n\nIf the CLI reported any `create-failed` or `spawn-failed` statuses, or Stage 2 emitted any enrichment warnings, list them under a `Warnings:` heading at the bottom of the report. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the deep-dive runbook and the Worktrunk verification result behind this worktree-first model.\n\n## Difficulty-Based Implementation-Model Routing\n\nBefore launching the interactive agent for each ticket, the packaged CLI selects an\nimplementation **model tier** from the ticket\'s `difficulty` rating (1-10) and may\ninject `--model` at the agent spawn boundary. This happens entirely inside the\nCLI \u2014 it is **not** part of the server-side `/implement-ticket` recipe, because the\nmodel an interactive agent session uses is fixed at the moment the process is launched.\n\n- **Tier ladder (fixed):** `difficulty 1-2 \u2192 cheap`, `3-6 \u2192 basic`, `7-10 \u2192 premium`.\n- **Separation of concerns:** the Python backend returns only the coarse tier\n (`cheap`/`basic`/`premium`) via `GET /jira/tickets/{KEY}/model-tier`; difficulty is\n computed on demand and cached when absent. Claude/Cursor use the TypeScript\n agent registry and Cursor validates against `cursor-agent --list-models`. Codex\n fetches `GET /jira/codex-model-catalog?repo_name=...` once per invocation;\n Bridge derives its bare IDs and revision directly from `MODEL_REGISTRY`.\n- **Per-repo config:**\n - `difficulty_model_routing_enabled` \u2014 boolean, **default ON**. Set to `false` to\n disable routing for a repo (the CLI then omits `--model`).\n - `difficulty_model_tier_overrides` \u2014 a JSON object mapping a tier name to a model\n alias (e.g. `{"premium": "opus"}`), **not** raw CLI arguments. Only `cheap`,\n `basic`, and `premium` keys are accepted; aliases must match `^[A-Za-z0-9._:-]+$`.\n Applies to Claude/Cursor only; Codex uses the backend catalog.\n- **Fail-open:** routing never aborts a spawn. Credential, network, config, or\n no-tier routing failures retain the premium fallback for Claude/Cursor when\n its alias is valid. Codex omits `--model` on access, config, tier, catalog,\n revision, or alias failure and launches its configured default. Disabled\n routing also omits `--model`. Each degraded invocation emits a secret-free\n routing diagnostic. Bridge\'s catalog states provider policy, not whether the\n local Codex account can use a model; no supported non-interactive account\n entitlement probe is available, so the CLI reports that limitation.\n\n### Model routing credential\n\nDifficulty\u2192model routing needs Bridge API credentials, and the shell-spawned\n`start-tickets` CLI is a **different runtime surface** from the MCP server: a\n`BAPI_API_KEY` that lives only in `.mcp.json` / `.cursor/mcp.json` is visible to\nthe MCP server but **not** to the Bash-spawned CLI, so routing silently degrades.\nThe durable source of truth both runtimes can resolve is the user-scoped store\n`~/.config/bridge/credentials.json`, keyed `bapi:<repo>`. If a routing-diagnostic\nline reports the credential is missing (e.g. difficulty resolves as `?`), fix it\nby any one of:\n\n1. Rerun `/install-bridge` \u2014 its final stage now persists the validated routing\n credential into `~/.config/bridge/credentials.json` via the\n `persist_routing_credential` tool.\n2. Migrate a key that lives **only** in `.mcp.json` / `.cursor/mcp.json` into the\n user-scoped store with the consent-gated, one-shot command:\n\n ```\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials\n ```\n\n3. Manually add `BAPI_API_KEY` under the `bapi:<repo>` target in the user-scoped\n store `~/.config/bridge/credentials.json`.\n\nNever put `BAPI_API_KEY` into a worktree `.mcp.json` / `.cursor/mcp.json` as a fix \u2014\nthat env is invisible to the spawned CLI.\n\n## Conductor observability (opt-in via `--conductor`, BAPI-394)\n\nConductor is **opt-in**. By default `start-tickets` spawns the plain\n`cd <worktree> && <agent> \'/implement-ticket <KEY> [--auto]\'` \u2014 no\n`BAPI_CONDUCTOR_*` env, no supervisor window, and no message-relay instruction.\nPass `--conductor` (e.g. `/start-tickets --conductor BAPI-123`) to enable the\nConductor system below.\n\nWith `--conductor`, a run mints a single conductor `run_id` and attributes each\nworker\'s lifecycle events by `worker_id`, ticket key, and worktree path, and a\nsupervisor peer tab is opened. When the selected agent is **Claude Code**, the CLI\ninjects a conductor lifecycle hook into each created worktree\'s\n`.claude/settings.local.json` so the spawned session emits local `run.started` /\n`run.stopped` / `agent.notification` (and, when\n`BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1`, `tool.intent`) events into the local\nconductor ledger. These hooks apply **only** when the selected agent is Claude\nCode; other agents (e.g. `cursor-agent`) still participate in the run-level\n`run.started` event but receive no per-worktree Claude hook. Inspect the ledger\nwith the `epic-implementer` CLI (e.g. `epic-implementer doctor`). Conductor observability is\nbest-effort and never blocks or aborts a spawn.\n\nObservability under `--conductor` is one-directional: workers emit lifecycle events\ninto the local ledger and nothing is passed back into a running session. (Epic-tick\ndispatch always runs with conductor enabled, independent of this user-facing flag.)\n',"teach-bridge.md":`Update a Bridge API configuration field via a natural-language teaching.
1844
+ `,"review-and-implement.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"agent","flag":"--agent","type":"string"}]}\n---\n\n# Review and Implement: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command runs inside a single already-created worktree (typically spawned by `/start-tickets --workflow review-and-implement`, or its front door `/review-and-start`). It reviews the ticket via `/review-ticket` **in this same session**, pauses at a per-ticket human proceed/halt gate, and \u2014 only on approval \u2014 hands the ticket off to a **freshly spawned implementation session** (`start-tickets --workflow implement`) that reuses this same worktree. Review and implementation therefore run in **two separate agent sessions/contexts**: the review session ends the moment the fresh implementation session is spawned, and the fresh session reads all reviewed ticket state back from the server rather than inheriting this session\'s transcript. It creates no *new* worktrees, refreshes no base branch, and does not monitor sibling sessions \u2014 those responsibilities stay in the launcher (`start-tickets`) and the parent front-door command.\n\n---\n\n# Instructions\n\n## Argument Parsing\n\nParse `$ARGUMENTS`:\n\n1. **Ticket key**: exactly one required token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`). If zero tokens match, or more than one token matches, stop immediately and display:\n ```\n Invalid ticket key. Expected exactly one key, e.g. PROJ-123.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n2. **`--auto`**: an optional position-independent flag. When present, sets chain-level `auto_approve` to `true`. This single flag applies to BOTH the review phase and the implementation phase below \u2014 there is no separate review-auto or implementation-auto state.\n3. **`--rounds`**: an optional position-independent `--rounds <n>` / `--rounds=<n>` argument, `1` or `2`. Normalize either form to `--rounds=<n>`. Reject any other value:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n When omitted, forward no `--rounds` to `/review-ticket` (the backend\'s difficulty-adaptive review policy decides the shape).\n4. **`--base-branch`**: an optional position-independent `--base-branch <branch>` / `--base-branch=<branch>` argument. Validate it using the same rules `/start-tickets` Stage 0 uses: after trimming, non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`). Reject a malformed value:\n ```\n Invalid --base-branch value: <reason>\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n5. **Executor identity**: accept optional position-independent `--agent claude|cursor-agent|codex` and `--host claude-code|cursor|codex` in either space or `=` form. For example, `--agent cursor-agent` and `--agent=cursor-agent` both select Cursor; the launcher supplies `--agent codex --host codex` for Codex. Resolve `--agent` first when both are present; otherwise map the host to its registered agent. If neither is supplied in a Codex session, resolve `codex` as both agent and host. Map the effective agent to its host (`claude-code`, `cursor`, or `codex`) for handoff; an explicit agent wins over a conflicting host. Reject an unknown value before review. Retain the resolved pair for the fresh-session handoff; do not infer it from the main checkout.\n\n## Phase 0 \u2014 Capture Worktree Identity (before review)\n\nBefore any review work runs, perform the **capture step of the Cleanup Helper** (below): record this ticket worktree\'s absolute top-level path and current branch, and resolve the main checkout. These captured values are the ONLY inputs the later cleanup and handoff steps use \u2014 capturing them up front means a mid-run branch switch cannot redirect a later `git worktree remove`/`git branch -D` at the wrong target.\n\n## Cleanup Helper\n\nA single named procedure invoked when the implementation gate is declined (Phase 2). It never runs on a review failure or the success path: a failed review must leave this session\'s working directory available for a retry, and a successful handoff reuses the worktree. Perform its steps exactly; it is local to this command\'s workflow and adds **no** new cleanup CLI flag, launcher subcommand, or MCP surface.\n\n**Capture (run in Phase 0, before `/review-ticket`):**\n\n1. Capture the ticket worktree\'s **absolute top-level path** with `git rev-parse --show-toplevel` and its **current branch** with `git rev-parse --abbrev-ref HEAD`, both run from inside this worktree. Call these `<capturedPath>` and `<capturedBranch>`.\n2. Resolve the **main checkout** from `git worktree list --porcelain`: parse the `worktree <path>` records exactly, retaining each path verbatim **without splitting on whitespace** (a path may contain spaces), and select the main (non-linked, bare-repo-backed) checkout. **Refuse to treat `<capturedPath>` (the current ticket worktree) as the main checkout** \u2014 if the resolution is ambiguous or would resolve to the ticket worktree itself, abort cleanup and report retention instead. Call the result `<mainCheckout>`.\n\n**Remove (run only when a halt path fires):**\n\n3. Confirm `<capturedPath>` is still **registered against `<capturedBranch>`** in `git worktree list --porcelain` before deleting anything. If the path is unregistered, detached, or bound to a different branch than captured, **do not remove** \u2014 report retention.\n4. Confirm the ticket worktree is **clean** with `git status --porcelain` (run against `<capturedPath>`). If the output is non-empty (uncommitted or untracked changes), **report and retain** it \u2014 never force-remove unexpected local work.\n5. From `<mainCheckout>` (never from inside the ticket worktree), run `git worktree remove <capturedPath>` using shell-safe single-quoting of `<capturedPath>` and **no `--force`**.\n6. Only **after** the worktree removal succeeds, and again from `<mainCheckout>`, run `git branch -D -- <capturedBranch>` (shell-safe quoting). Preserve this remove-then-delete ordering; never delete the branch first.\n7. Report the exact branch and worktree removed (e.g. `Cleaned up worktree <capturedPath> and branch <capturedBranch>`), or, on any failure, a **bounded** cleanup-failure message that names what could not be removed and contains no credential or raw secret material.\n\n## Phase 1 \u2014 Review (inline)\n\n### Pre-review tier snapshot\n\nBefore invoking `/review-ticket`, call the `get_ticket` MCP tool with `ticket_number` set to `<ticket_key>` and `include_model_tier` set to `true`, and record a **tier snapshot** from the `tier` field it merges into the response. Retain the value **only** when it is one of the recognized tiers `cheap`, `basic`, or `premium`; treat any tool error, missing/`null` payload, or unrecognized value as an **unresolved** snapshot. A tier-snapshot failure is **non-critical**: do NOT abort \u2014 continue into review, and let the later implementation launcher perform its ordinary difficulty-based routing when the snapshot is unresolved. The snapshot exists only to let the fresh implementation session reuse the review-time tier decision (via `--tier`) instead of recomputing it; it is a coarse `cheap|basic|premium` label, never a raw model alias.\n\n### Run review\n\nInvoke `/review-ticket <ticket_key>` **inline, in this same session**, forwarding:\n- `--auto` when chain-level `auto_approve` is `true`.\n- The normalized `--rounds=<n>` when `--rounds` was supplied.\n- `--base-branch=<branch>` when it was supplied.\n\n`/review-ticket` grounds its codebase evaluation against a **fresh `git archive` of `origin/<base>`**, materialized into an isolated temp directory via `fresh_base` with `action: "materialize"` (see `docs/BAPI-474-ground-review-against-fresh-base.md`) \u2014 **not against this worktree\'s own working tree** (i.e. fresh-base grounding is not worktree-local). Review therefore behaves identically whether run standalone or, as here, inside a worktree that `start-tickets` already created; the worktree\'s isolation exists for the implementation phase below, not for review\'s codebase grounding.\n\nIf the review pipeline itself fails (any step reports `Status: Failed at step N`), halt this ticket immediately \u2014 in **both** auto and non-auto mode \u2014 before Phase 2 or Phase 3 run. **Retain the worktree and branch.** This session is rooted in that directory; deleting it prevents a follow-up turn from starting, including a retry of a failed Jira upload. Report the retained path and branch alongside:\n```\nReview failed for <ticket_key> at step N. Implementation was not started.\n```\nand stop. No implementation session is spawned. The user can retry the failed step in this session without re-running completed review decisions; once the review completes, the normal Phase 3 handoff can reuse the captured branch.\n\n## Phase 2 \u2014 Halt Gate\n\n`/review-ticket` produces **decisions that rewrite the ticket** \u2014 it does NOT emit a binary approve/decline verdict token. Do not parse, infer, or synthesize an approve/decline signal from its output; no such token exists to parse.\n\n- **When chain-level `auto_approve` is `true`** and Phase 1 completed successfully: skip this gate entirely and proceed straight to Phase 3.\n- **Otherwise** (non-auto, and Phase 1 completed successfully): after the ticket has been rewritten, ask the user exactly:\n ```\n Proceed to implementation for <ticket_key>? (y/N)\n ```\n Treat an empty response, any negative response (`n`, `no`, or similar), or any ambiguous/unrecognized response as **decline** \u2014 do not guess intent. On decline, invoke the **Cleanup Helper** to remove this ticket\'s worktree and branch, then report:\n ```\n Declined implementation for <ticket_key>. The worktree and branch were cleaned up.\n ```\n and stop. Do not spawn an implementation session.\n- On an explicit affirmative response (`y` or `yes`), proceed to Phase 3.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Phase 3 \u2014 Implement (fresh session handoff)\n\n**Never invoke `/implement-ticket` inline in this session.** Instead, spawn a fresh implementation session that reuses this worktree, then end this review session.\n\n1. **Resolve the launcher directory.** Run the handoff from the `<mainCheckout>` resolved in Phase 0 \u2014 `start-tickets` manages worktrees and must run from the main checkout, never from inside the ticket worktree.\n2. **Construct exactly one launcher command** for this single ticket key, using the `<launcher>` resolved above:\n ```\n <launcher> start-tickets --workflow implement <handoff-flags> <ticket_key>\n ```\n Assemble `<handoff-flags>` conditionally:\n - `--tier=<snapshot>` **only** when the Phase 1 tier snapshot resolved to a valid `cheap|basic|premium` value. When the snapshot is unresolved, **omit `--tier` entirely** so the fresh launcher follows its normal fail-open difficulty routing.\n - `--branch <ticket_key>=<capturedBranch>` **only** when `<capturedBranch>` differs from the launcher default `feature/<ticket_key>` (i.e. an enriched branch such as `feature/<KEY>-<slug>`). This reuses the existing worktree/branch via the launcher\'s `switch` path instead of cutting a new one; when the branch is already the bare `feature/<ticket_key>`, no override is needed.\n - `--auto` **only** when chain-level `auto_approve` is active.\n - `--agent <resolved-agent>` and `--host <resolved-host>` when executor identity was supplied. For a Codex session, always pass `--agent codex --host codex`, even if the outer caller supplied an overriding host, so the implementation launcher cannot default to Claude.\n - `--base-branch=<value>` **only** when this command received `--base-branch` (Argument Parsing item 4). A fresh terminal does not inherit this session\'s environment, so re-emitting the flag is the only way the base survives into the implementation launcher \u2014 which exports it to the worker as `BAPI_BASE_BRANCH`, and the create-PR step targets it. Escape the value exactly as `/start-tickets` values are escaped: replace every single quote in the branch with `\'\\\'\'`, then wrap the complete value in single quotes before inserting it into the launcher command.\n3. **Credential invariant.** `BAPI_API_KEY` and every other credential MUST be absent from this launcher command and its environment assignment \u2014 the spawned CLI resolves credentials on its own through `resolveBapiCredentials` / the user-scoped credential store. Never inline a key, authorization header, or credential-store payload into the command string.\n4. **Starter prompt boundary.** The fresh session\'s starter prompt is `/implement-ticket <ticket_key>` for Claude/Cursor or `$bridge-implement-ticket <ticket_key>` for Codex (plus `--auto` when auto is active) \u2014 the launcher builds it. The review transcript, the tier snapshot, the branch metadata, and any review decisions are **excluded** from that prompt; the fresh session reads all reviewed ticket state back from the server.\n5. **On success** (launcher exits `0` and reports the ticket spawned): **end this review session immediately.** Do not monitor, parse, or wait for any output from the fresh implementation session \u2014 that session owns the implementation lifecycle from here.\n6. **On a non-zero launcher exit**: treat it as a **handoff failure**. Report the exit code and the launcher\'s error output, and **retain** the existing clean worktree for a manual retry \u2014 do **not** invoke the Cleanup Helper (a failed handoff is not a decline). \n\n## Scope\n\nThis command composes `/review-ticket` (inline) and a fresh `start-tickets --workflow implement` handoff; it owns none of their internals and must not:\n- create, re-cut, or switch a *new* Worktrunk worktree itself (the fresh launcher reuses the existing one),\n- refresh or fetch the launcher\'s base branch,\n- monitor or coordinate with sibling `/review-and-implement` sessions spawned for other tickets,\n- orchestrate the parent `start-tickets` / `/review-and-start` session,\n- carry the review transcript, tier snapshot, or branch metadata into the fresh implementation session\'s starter prompt.\n\nA halt affects **this ticket only**: a review failure retains its worktree for recovery, while a declined gate cleans up its worktree/branch. Neither path touches sibling worktrees. The fresh implementation session receives only `/implement-ticket <ticket_key> [--auto]` for Claude/Cursor or `$bridge-implement-ticket <ticket_key> [--auto]` for Codex as its starter prompt and reads all reviewed ticket state back from the server.\n',"review-and-start.md":'---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"agent","flag":"--agent","type":"string"},{"name":"host","flag":"--host","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"guardStaleBranch","flag":"--guard-stale-branch","type":"boolean"}]}\n---\n\n# Review and Start: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248` or `BAPI-248 BAPI-250` \u2014 both flow through the identical ordered `keys: string[]` path) and spawns one refreshed, self-contained Worktrunk worktree per ticket, each running `/review-and-implement <KEY>`: review runs in that spawned session, then a per-ticket human proceed/halt gate, and \u2014 only on approval \u2014 that session hands the ticket off to a **fresh implementation session** that reuses the same worktree. Review and implementation thus run in two separate agent sessions per ticket, not one shared context. It is the **recommended front door** for chaining "review these tickets, then implement the ones that pass" starting from *existing ticket keys* (the entry point `/idea-to-pr` lacks, since its server orchestrator only accepts an idea).\n\nIt is a thin shim over the packaged `start-tickets` CLI, invoked with `--workflow review-and-implement`: this command performs the same connectivity check and branch enrichment as `/start-tickets`, then hands off to the identical worktree/base-refresh/model-routing/credential/cross-platform-spawn/`doctor` engine \u2014 reused verbatim. The review\u2192implement halt-gate decision logic lives entirely in the spawned `/review-and-implement` session, never here or in the CLI. Using `start-tickets --workflow review-and-implement` directly (documented in `commands/src/start-tickets.md`) remains available as the lower-level launcher seam this command drives.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that resolves connectivity and per-ticket branch names, then spawns N parallel Worktrunk worktrees via the packaged CLI\'s `--workflow review-and-implement` seam. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys and pass-through flags:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). A single key and multiple keys flow through the identical ordered `keys: string[]` path \u2014 there is no separate single-ticket code path. If zero keys are found, stop immediately and display:\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /review-and-start [flags] <KEY> [KEY ...] (e.g., /review-and-start BAPI-248 BAPI-250)\n ```\n - **`--auto`**: a single chain-level flag. When present, it is forwarded to `start-tickets --auto`, which threads it into BOTH the review and implementation phases of every spawned `/review-and-implement <KEY>` session.\n - **`--rounds`**: normalize `--rounds 1`/`--rounds=1` and `--rounds 2`/`--rounds=2` to `--rounds=<n>`; reject any other value. This value is forwarded to the review phase of every spawned session.\n - **Selected agent**: validate `--agent <name>` / `--agent=<name>` against `claude|cursor-agent|codex`. It takes precedence over an optional `--host <platform>` / `--host=<platform>`. When no agent was supplied, resolve `--host` through the same host-aware capability matrix as `/start-tickets`; a non-runnable host returns manual-continuation guidance. In a Codex session with neither flag, set `--agent codex --host codex` for Stage 3; otherwise forward every supplied flag to the CLI so it makes the authoritative final decision.\n - **Command host default: none.** This line is specialized to `cursor` only in the Cursor command asset. If neither `--agent` nor `--host` was supplied and this default is `cursor`, forward `--host cursor` to the CLI. Never infer the host from the presence of a `.cursor` directory; an explicit `--agent` or `--host` always wins.\n - **User-supplied base branch**: validate `--base-branch <branch>` / `--base-branch=<branch>` using the same non-empty/\u2264255-character/no-leading-dash/no-control-character rules as `/start-tickets` Stage 0. A user-supplied value takes precedence over Stage 2a\'s `config_field` resolution below.\n - **`--max-parallel N`**: a positive integer; the CLI\'s own default (3) applies when omitted.\n - **`--dry-run`**: a boolean toggle.\n - **`--guard-stale-branch`**: a boolean toggle forwarded verbatim to `start-tickets`. It turns on the F7 stale-branch guard: a pre-existing `feature/<KEY>` branch whose tip is **not** an ancestor of the resolved base is refused with a `create-failed` row and the stale-worktree remedy, instead of being silently reused. Pass it on the **LLM-conductor pilot cut path**, where deterministic slugs mean a re-cut epic re-derives the same branch name and would otherwise land on the previous run\'s branch. Omitted by default, so an ordinary human run keeps reusing a same-named branch.\n - Reject malformed input before proceeding: an unsupported flag, a ticket key not matching `[A-Z]+-[0-9]+`, an unsupported `--agent`/`--rounds`/`--base-branch` value \u2014 stop and report the malformed argument.\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged `start-tickets` CLI (driven with `--workflow review-and-implement` in Stage 3) owns all platform pre-flight checks, refreshed worktree creation, secret-free credential provisioning, difficulty\u2192model-tier routing, and cross-platform terminal spawning \u2014 identical to `/start-tickets`. This command does not duplicate or re-verify any of that; see `commands/src/start-tickets.md` Stage 1 for the full prerequisite/provisioning description it drives. This command does not expose `--conductor`, `--terminal`, or `--no-refresh-main`/`--no-refresh-base` \u2014 it stays scoped to the front-door flags listed above. `--guard-stale-branch` is the one safety flag it *does* forward: `--conductor` implies that guard in the CLI, but this front door must not enable the v2 conductor context (a run id, hook provisioning, run-started emission) that `--conductor` also switches on, so the guard is reachable here on its own. Proceed to Stage 2.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve Base Branch\n\nIdentical to `/start-tickets` Stage 2a: if the user supplied `--base-branch`, use it and skip the `config_field` lookup entirely. Otherwise call `config_field` (`operation: "get"`, `field_name: "base_branch"`) and treat a non-empty string `value` as the configured base branch; treat `null`, an empty/whitespace-only string, an HTTP `400` response, or any other lookup failure as "unset" and omit `--base-branch` from Stage 3 (the CLI then defaults to `main`). When forwarding a resolved value into the Stage 3 Bash invocation, apply the same mandatory single-quote escaping rule as `/start-tickets` (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating it into the command string \u2014 never expand it unquoted.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nIdentical to `/start-tickets` Stage 2b: for each ticket key without a user-provided `--branch` override, call `get_ticket` (`ticket_number` set to the key, `save_locally: false`), slugify its `summary` (lowercase, collapse runs of `[^a-z0-9]+` to a single `-`, trim leading/trailing dashes, truncate to at most 40 characters preferring a dash boundary), and build `feature/<KEY>-<slug>`. On any per-key failure (404, network error, empty slug), emit a warning and let the CLI apply its default `feature/<KEY>` for that key only; never stop the pipeline. Credentials never reach the Bash-spawned CLI \u2014 this enrichment stays in the command, exactly as in `/start-tickets`.\n\nThis stage is **non-critical** \u2014 warnings are acceptable; the pipeline continues with the fallback branch for any key that fails enrichment.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the Stage 3 shell-out, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n### Dry-run intended-actions preview\n\nWhen the user passed `--dry-run`, print an intended-actions preview **before** the Stage 3 shell-out. The preview names the resolved launcher explicitly, so an operator can see at a glance whether this run will use the published package or a local build:\n\n```\nIntended actions (dry run \u2014 nothing is created)\n Launcher: <the resolved <launcher> value, exactly as it will be escaped into the shell command>\n Command: <launcher> start-tickets --workflow review-and-implement <flags> <ticket-keys>\n Tickets: <the ordered ticket keys>\n```\n\nWhen the override is set, show the **escaped** launcher exactly as it will appear in the Bash command string, so the preview and the command cannot disagree. `--dry-run` is then forwarded to the resolved launcher, which does the previewing of its own actions: **no worktree is created, no agent tab or session is opened, and no worker process is spawned.** Do not state or imply otherwise.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke exactly **one** CLI invocation covering every requested key \u2014 never a per-key loop:\n\n```\n<launcher> start-tickets --workflow review-and-implement <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` forwards `--auto` (only if supplied), the normalized `--rounds=<n>` (only if supplied), `--agent <name>` and `--host <platform>` (only if supplied; forward both when both were supplied so the CLI enforces agent precedence), the Cursor command asset\'s `--host cursor` default only when neither was supplied, `--max-parallel N` (only if supplied), `--dry-run` (only if supplied), and `--guard-stale-branch` (only if supplied) verbatim.\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` only when a value was resolved in Stage 2a; otherwise omitted entirely.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2b.\n- `<ticket-keys>` is the original ordered list of keys parsed in Stage 0.\n\nCredentials are never placed in this shell command \u2014 the same secret-free provisioning path `/start-tickets` uses applies here unchanged.\n\nExample, single key, hands-off:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=1 --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step BAPI-248\n```\n\nExample, multiple keys, manual per-ticket gate (no `--auto`):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step --branch BAPI-250=feature/BAPI-250-deep-research-durability BAPI-248 BAPI-250\n```\n\nExample, LLM-conductor pilot cut (epic base plus the stale-branch guard):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --guard-stale-branch --base-branch \'epic/BAPI-902\' --branch BAPI-902=feature/BAPI-902 BAPI-902\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket: `KEY branch=BRANCH status=STATUS`, optional trailing `path=PATH`) and reformat it as a markdown table, identical in shape to `/start-tickets` Stage 4 (`Ticket | Branch | Status`; statuses `dry-run`, `spawned`, `create-failed`, `spawn-failed`).\n\nThis report describes **worktree/spawn status only**. State explicitly in the report:\n- Each successfully spawned session first runs `/review-ticket` and its own per-ticket halt gate; on successful review and approval (or `--auto`), it then opens a **fresh implementation session** (`/implement-ticket`) that reuses the same worktree. Review and implementation run in two separate agent sessions, not one shared context. This parent session does not observe or report either the review outcome or the later implementation outcome. Never claim or imply that review or implementation has completed.\n- When `--dry-run` was passed, state explicitly that no worktrees were created and no tabs/sessions were opened.\n- As a fixed trade-off of this design: the review session\'s difficulty-derived model tier is snapshotted and reused (via `--tier`) by the fresh implementation session, so both phases run on the same tier; and a non-auto (including a non-auto scheduled) run pauses at each ticket\'s own gate rather than proceeding hands-off. For an `--auto` run, each ticket\'s eventual implementation completion is observed in its own fresh implementation tab, not here.\n\nIf the CLI reported any `create-failed`/`spawn-failed` statuses, or Stage 2b emitted enrichment warnings, list them under a `Warnings:` heading. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the underlying runbook this command builds on, and `commands/src/review-and-implement.md` for the per-ticket review\u2192gate\u2192implement composition each spawned session runs.\n',"review-ticket.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"baseSha","flag":"--base-sha","type":"string"}]}\n---\n\n# Review Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. Parse `$ARGUMENTS` to extract:\n - A required `ticket_key` matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`).\n - An optional position-independent `--auto` flag.\n - An optional position-independent `--rounds=<n>` argument, where `<n>` is `1` or `2`.\n - An optional position-independent `--no-refresh-base` flag.\n - An optional position-independent `--base-branch=<branch>` argument.\n - An optional position-independent `--base-sha=<sha>` argument.\n\n Tokenize `$ARGUMENTS` on whitespace. The first token matching the Jira key pattern is the `ticket_key`; ignore any additional ticket-key tokens. The presence of a `--auto` token (anywhere in `$ARGUMENTS`) sets `auto_approve` to `true`. A token matching `--rounds=1` sets `rounds` to `1`; a token matching `--rounds=2` sets `rounds` to `2`. If no `--rounds` token is present, leave `rounds` unset (omitted) so the backend\'s difficulty-adaptive review policy can decide the review shape when enabled for this repo; when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved, the backend falls back to a full premium second-opinion review. The presence of a `--no-refresh-base` token sets `no_refresh_base` to `true`. A token matching `--base-branch=<branch>` sets `base_branch` to `<branch>`. A token matching `--base-sha=<sha>` sets `base_sha` to `<sha>`.\n\n `--auto`, `--rounds`, `--no-refresh-base`, `--base-branch`, and `--base-sha` are all independent and may be supplied in any combination.\n\n If `$ARGUMENTS` is empty or contains no token matching the Jira key pattern, stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123 [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n If a `--rounds` token is present but its value is not `1` or `2`, stop and display:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2 (omit to let the backend decide adaptively; default falls back to a full review)\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"review-ticket"`\n - `variables`: `{ "ticket_key": "<ticket_key>", "base_branch": "<base_branch or "">", "base_sha": "<base_sha or "">", "no_refresh_base": "<"true" if --no-refresh-base was passed, else "">" }`\n - `auto_approve`: `true` \u2014 only when `--auto` was passed; otherwise omit this field entirely.\n - `rounds`: `1` \u2014 only when `--rounds=1` was explicitly passed on the command; `2` \u2014 only when `--rounds=2` was explicitly passed. When `--rounds` was NOT supplied, omit `rounds` entirely (do not pass `rounds: null`) so the backend\'s difficulty-adaptive review policy can choose the review shape when enabled for this repo. An explicit `rounds` value is forwarded to the backend and forces the review shape: `--rounds=1` requests a single-pass review, and `--rounds=2` forces the full second-opinion review even when adaptive routing is enabled. Do NOT translate `rounds` into `skip_steps` \u2014 the backend executor now owns all round orchestration (including any second-opinion rounds), so the recipe carries a single `request_ticket_review` step and you never pass `skip_steps` for round control.\n\n Example combined-mode payload (`--rounds=1 --auto --base-branch=develop`):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "develop", "base_sha": "", "no_refresh_base": "" },\n "auto_approve": true,\n "rounds": 1\n }\n ```\n\n Example explicit full-review payload (`--rounds=2`), which forces the full second-opinion review even if adaptive routing is enabled for this repo:\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" },\n "rounds": 2\n }\n ```\n\n Example adaptive payload (no `--rounds`), which lets the backend policy executor decide the review shape (falling back to a full premium review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" }\n }\n ```\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',"review-tickets.md":'---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"review","flag":"--review","type":"string","repeatable":true},{"name":"agent","flag":"--agent","type":"string"},{"name":"model","flag":"--model","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"}]}\n---\n\n# Review Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command is the single review front door for one or more Jira ticket keys. **Exactly one resolved ticket key** is reviewed **inline in the current session**: it invokes `/review-ticket <KEY>` directly, with no terminal tab, no worktree, and no packaged CLI subcommand. **Two or more resolved ticket keys** fall back to the packaged `@bridge_gpt/mcp-server` CLI subcommand `review-tickets`, which opens one terminal tab per ticket running the selected agent with `/review-ticket <KEY> [--auto] [--rounds=<1|2>]`. By default `--rounds` is omitted so the backend routes each review by difficulty (difficulty-adaptive review); pass an explicit `--rounds=1|2` (globally or per ticket) to force the review shape. Unlike `/start-tickets`, the multi-key path creates no Worktrunk worktrees \u2014 but it requires `git` on PATH: the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch before spawning tabs (BAPI-474), so every spawned review grounds its codebase evaluation against the same freshly-fetched base tree. Pass `--no-refresh-base` to skip this and restore the prior git-free, in-place-grounded behavior. The single-key inline path performs no batch fetch and pins no shared `base_sha` \u2014 `/review-ticket` grounds its own review independently (see Stage 2 below).\n\n---\n\n# Instructions\n\n## Stage 0 \u2014 Parse Arguments, Validate, and Check Connectivity\n\n1. **Parse `$ARGUMENTS`** to extract ticket keys, review modes, and pass-through flags:\n\n - **Recognized flags and their values are consumed first.** `--review`, `--agent`, `--model`, `--base-branch`, and every other value-taking flag, plus the value token immediately following it (or the `=value` suffix on the same token), are removed from consideration before ticket keys are resolved. **Every remaining whitespace-separated token** matching `[A-Z]+-[0-9]+` (e.g., `BAPI-1`) is a resolved positional ticket key. Jira-key-shaped text that appears **inside** a flag value (e.g. the `BAPI-1` inside `--review BAPI-1=auto,rounds=1`, or a coincidentally key-shaped `--model` or `--agent` value) is never added to the positional ticket-key list \u2014 it was already consumed as part of that flag. If zero resolved positional keys remain, stop immediately and display:\n ```\n No ticket keys found. Expected one or more keys like BAPI-1.\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n - **Review mode interpretation** (per ticket or global):\n - `auto` or `--auto` \u2192 per-ticket or global auto-approve flag.\n - `single-pass`, `one-pass`, `rounds=1`, or `--rounds=1` \u2192 `rounds=1` (single-pass review).\n - `full`, `two-pass`, `rounds=2`, or `--rounds=2` \u2192 `rounds=2` (full second-opinion review).\n - **omitted rounds \u2192 `adaptive`**: when no rounds mode is given for a ticket, do NOT choose a round count \u2014 leave it adaptive so the backend\'s difficulty-adaptive review policy decides the shape. `adaptive` is a distinct mode from `1` and `2`.\n - `--auto` and `--rounds` are independent: both may apply to the same ticket.\n - **Repeated or conflicting `--review KEY=...` overrides for the same key are last-wins**: the last occurrence of an override targeting a given key is the one that applies, matching this repository\'s existing last-wins CLI convention. A sole-key override such as `--review BAPI-123=auto,rounds=2` resolves to exactly the same state as explicit global `--auto --rounds=2` for that key.\n\n The backend executor now owns all review round orchestration (including any second-opinion rounds) server-side \u2014 there is no client-side step to skip, so this command never sends `skip_steps` and never translates `--rounds=1` into skipping a second-opinion step. An explicit `--rounds` value forwarded to each spawned `/review-ticket` forces the review shape (`1` = single pass, `2` = full second-opinion review). A spawned `/review-ticket` invoked without any `--rounds` (the default) lets the backend\'s difficulty-adaptive review policy decide the shape (falling back to a full premium second-opinion review when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved). This command therefore forwards **no** `--rounds` by default; it only forwards `--rounds` when the caller explicitly supplies a rounds mode (globally via `--rounds`, or per ticket via `--review`).\n\n - **Homogeneous modes** (two or more resolved keys only): when all tickets share the same auto and rounds mode, translate into global `--auto` (if all auto) and, only when all tickets share the same *explicit* rounds value, global `--rounds=1|2`. When all tickets are adaptive (no rounds given), omit `--rounds` entirely \u2014 do not synthesize a default.\n\n - **Heterogeneous modes** (two or more resolved keys only): when tickets differ in auto or rounds mode, translate into repeatable `--review KEY=auto,rounds=N` overrides. Only emit a `rounds=N` subtoken for tickets given an explicit `1`/`2`; adaptive tickets carry no `rounds` subtoken (a bare `--review KEY=auto` if they are auto, or no override at all). Do NOT set global `--auto` when only some tickets are auto-approved, and do NOT set global `--rounds` when only some tickets have an explicit rounds value.\n\n - **Pass-through flags**: collect `--dry-run`, `--max-parallel N`, `--agent claude|cursor-agent|codex`, `--model VALUE`, `--no-refresh-base`, and `--base-branch VALUE` if supplied. In a Codex session, pass `--agent codex` when no agent was supplied. Codex batch sessions use `$bridge-review-ticket` and require the current repository\'s skill bundle and repository-bound Bridge MCP scope before spawn.\n\n - **Validate `--base-branch`** (applies before dispatch, for both the single-key and multi-key paths): accept either `--base-branch VALUE` or `--base-branch=VALUE`. After trimming, the value must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`). Reject a malformed value:\n ```\n Invalid --base-branch value: <reason>\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n2. **Connectivity check**: after parsing and validation succeed (i.e. at least one ticket key resolved and `--base-branch`, if supplied, is valid), and **before** the ticket-count dispatch below and before either dispatch path\'s `--dry-run` handling, call the `ping` MCP tool. If it fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n## Stage 1 \u2014 Dispatch by Resolved Ticket Count\n\nCount the resolved positional ticket keys from Stage 0:\n\n- **Exactly one key** \u2192 Stage 2, the single-key inline path. Do not resolve the packaged-CLI launcher, spawn a terminal tab, or perform the batch base-branch fetch/pin on this path.\n- **Two or more keys** \u2192 Stage 4, the packaged CLI fan-out (unchanged from today).\n\n(Zero keys already stopped in Stage 0 and never reaches this dispatch.)\n\n## Stage 2 \u2014 Single-Key Inline Review\n\nThis path reviews the sole resolved key **in the current agent session**, mirroring how `/review-and-implement` invokes `/review-ticket` inline.\n\n1. **Construct the inline invocation** in this deterministic order: `/review-ticket <KEY>`, followed conditionally by:\n - `--auto` \u2014 forwarded when either the global `--auto` flag or the resolved per-key `--review KEY=...` override enables auto-approve for this key.\n - `--rounds=1` or `--rounds=2` \u2014 forwarded only when the resolved mode for this key (global `--rounds`, or a `rounds=N` subtoken in a `--review KEY=...` override targeting this key) contains an explicit round count. When the resolved mode is adaptive (no explicit rounds given anywhere for this key), omit `--rounds` entirely so the backend\'s difficulty-adaptive review policy decides the shape.\n - `--no-refresh-base` \u2014 forwarded unchanged when supplied.\n - `--base-branch=<value>` \u2014 the validated Stage 0 value, normalized to the `/review-ticket`-accepted `--base-branch=<value>` form, when supplied.\n\n `--agent`, `--model`, and `--max-parallel` are accepted for interface stability (they are fan-out concerns with no meaning for a single inline review) but are **never** added to the constructed invocation. When any of the three is supplied, print exactly one advisory line to **stderr** before continuing:\n ```\n Note: --agent, --model, and --max-parallel do not apply to a single-key inline review and were ignored.\n ```\n\n2. **`--dry-run`**: after the unconditional Stage 0 `ping` and any stderr inert-flag advisory above, write only the constructed invocation to **stdout**:\n ```\n /review-ticket <KEY> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=<value>]\n ```\n Then stop immediately. Do not invoke `/review-ticket`, resolve the packaged-CLI launcher, open a terminal tab, perform the batch fetch/base-SHA pin, or enter Stage 5\'s summary rendering.\n\n3. **Otherwise**, invoke the constructed `/review-ticket` command **inline, in this same session** \u2014 the same composition shape `/review-and-implement` uses for its own inline review phase. This path performs **no** batch `git fetch` and pins **no** shared `base_sha`: `/review-ticket` independently grounds its review through `fresh_base` materialization of a fresh `git archive` from `origin/<base>` (see `docs/BAPI-474-ground-review-against-fresh-base.md`), and the forwarded `--no-refresh-base` / `--base-branch` flags control that independent grounding.\n\n4. When the inline reviewer completes, the `/review-tickets` orchestration for this invocation is finished. Do not parse or render packaged-CLI `Summary:` output \u2014 that format belongs to the multi-key path only.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nUsed only by the multi-key fan-out (Stage 4) below \u2014 the single-key inline path (Stage 2) never resolves or uses this launcher.\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Stage 4 \u2014 Invoke the Packaged CLI (two or more keys)\n\nUse the **Bash tool** to invoke exactly one CLI invocation:\n\n```\n<launcher> review-tickets [--auto] [--rounds=1|2] [--review KEY=auto,rounds=N ...] [--agent <name>] [--model <alias>] [--max-parallel N] [--dry-run] [--no-refresh-base] [--base-branch BRANCH] KEY [KEY ...]\n```\n\n- `review-tickets` runs all tabs from the current repository cwd \u2014 it creates no worktrees.\n- The command never runs `wt` or `git-wt` \u2014 but it now requires `git` on PATH (BAPI-474): before spawning any tabs, the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch, so a mid-batch `origin` advance can never mix bases within one run. Pass `--no-refresh-base` to skip the fetch and restore the prior git-free, in-place-grounded behavior.\n- Prerequisites: macOS `osascript` + `git`, Windows `wt.exe` or PowerShell + `git`, Linux `tmux` + `git` (git is not required when `--no-refresh-base` is passed).\n\nPass through the CLI\'s stdout and stderr verbatim. If the CLI exits non-zero, treat it as a critical failure and report the exit code and error output.\n\n## Stage 5 \u2014 Final Report (two or more keys)\n\nOnce the CLI exits 0, parse its `Summary:` lines (each shaped like `KEY auto=<true|false> rounds=<1|2|adaptive> agent=<agent> model=<alias|default> status=<status>`) and render as a markdown table (`rounds=adaptive` means the backend chose the shape by difficulty):\n\n```\n| Ticket | Auto | Rounds | Agent | Model | Status |\n|----------|-------|----------|--------|---------|---------|\n| BAPI-1 | false | adaptive | claude | default | spawned |\n| BAPI-2 | true | 1 | claude | default | spawned |\n```\n\nRender any CLI `Warnings:` lines below the table. If there were none, omit the warnings section.\n',"run-tests.md":'Run the project\'s full test suite (unit and E2E) using the project-configured test stacks, triage failures, fix test-code issues, and produce a structured health-check report.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command discovers how to run tests by reading per-project configuration from the Bridge API, not from hardcoded paths. Stages run only when the project has the corresponding stack configured.\n\n## Stage 0 \u2014 Argument Parsing and Setup\n\n1. **Parse `$ARGUMENTS`** for optional flags. Supported flags:\n - `--skip-e2e` \u2014 skip the E2E test stage even if an E2E stack is configured (e.g., when no local server is running)\n - `--unit-only` \u2014 shorthand that implies `--skip-e2e`\n\n Resolve flags to boolean variables:\n - Start with: `run_unit = true`, `run_e2e = true`\n - If `--unit-only` is present: set `run_e2e = false`\n - If `--skip-e2e` is present: set `run_e2e = false`\n - Unknown flags: note them in the final report as "Unrecognized flag ignored" but do not fail\n\n2. **Generate a run timestamp** using the current date and time in `YYYY-MM-DD-HH-MM` format (e.g., `2026-03-10-14-35`). Store this as `run_timestamp`. Both output documents will use this value.\n\nThis stage has no failure conditions \u2014 proceed to Stage 1.\n\n## Stage 1 \u2014 Resolve Project Config via MCP\n\nRead the per-project test setup from the Bridge database. Every subsequent stage is driven by what these calls return.\n\n1. **Resolve docs directory**: Call the `ping` MCP tool (no parameters) and read `docs_dir` from its first (JSON) content item. Store that path as `docs_dir`.\n\n2. **Read unit-test stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_stack`. Store the returned value as `unit_stack` (may be null/empty).\n\n3. **Read unit-test instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_instructions`. Store the returned value as `unit_instructions` (may be null/empty).\n\n4. **Read E2E stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_stack`. Store as `e2e_stack`.\n\n5. **Read E2E instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_instructions`. Store as `e2e_instructions`.\n\n6. **Compute configuration booleans**:\n - `unit_configured` = `true` if either `unit_stack` or `unit_instructions` is a non-empty string; otherwise `false`\n - `e2e_configured` = `true` if either `e2e_stack` or `e2e_instructions` is a non-empty string; otherwise `false`\n\n7. **Create the output directory**:\n ```\n mkdir -p {docs_dir}/testing/\n ```\n If this fails, stop immediately and report: `Cannot create output directory {docs_dir}/testing/ \u2014 check permissions.`\n\nIf any MCP call fails (e.g., the API is unreachable or returns 4xx/5xx), stop immediately and report which call failed. Do not fall back to hardcoded commands \u2014 the whole point of this command is that test setup lives in config.\n\n## Stage 2 \u2014 Unit / Standard Tests\n\nIf `run_unit` is `false`, skip this stage and record: `Unit tests: SKIPPED \u2014 run_unit was set to false (this should not happen in normal use; report as a bug).`\n\nIf `unit_configured` is `false`, skip and record:\n```\nUnit tests: SKIPPED \u2014 no unit_testing_stack or unit_testing_instructions configured for this repo. Configure them with /learn-repository (a project admin must run it) or the project setup UI before running /run-tests.\n```\n\nOtherwise:\n\n1. Read `unit_instructions` carefully. It is the source of truth for **how to run unit tests in this repo** \u2014 runner binary, paths, environment activation, sub-suites (if the project distinguishes "unit" from "integration", both belong in this stage), and any flags. Pair it with `unit_stack` (a short label, e.g., `Pytest`, `Jest + React Testing Library`) for context.\n\n2. **Derive the test command(s)**: Extract the literal shell commands the instructions describe. If the instructions describe multiple sub-suites (e.g., a fast unit batch and a slower integration batch), plan to run each as a **separate batch** in the order described. Do not invent runners or paths that the instructions do not mention.\n\n3. **If the instructions do not specify any runnable command**, skip and record:\n ```\n Unit tests: SKIPPED \u2014 unit_testing_instructions does not describe how to invoke tests; update that field with /teach-bridge, or re-run /learn-repository (a project admin must run it).\n ```\n\n4. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output of each batch, including the runner\'s summary line (e.g., `47 passed, 3 failed in 12.4s` or `Tests: 5 failed, 22 passed`).\n\n5. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Stage 3 \u2014 E2E Tests\n\nIf `run_e2e` is `false`, skip this stage and record: `E2E tests: SKIPPED \u2014 --skip-e2e or --unit-only flag was set.`\n\nIf `e2e_configured` is `false`, skip and record:\n```\nE2E tests: SKIPPED \u2014 no e2e_testing_stack or e2e_testing_instructions configured (the project may not have an E2E suite).\n```\n\nOtherwise:\n\n1. Read `e2e_instructions`. It is the source of truth for the E2E runner, spec paths, browser config, and any prerequisites. Pair with `e2e_stack` for context.\n\n2. **Detect server prerequisites**: If `e2e_instructions` indicates that a local server must be running (look for explicit cues such as "server", "running", "localhost", "started", "dev server", a URL, or a port number) and describes a readiness check, perform that check exactly as described. If the instructions describe a server prerequisite but do not describe a check, attempt the check the instructions imply (e.g., curl the URL the instructions mention) and skip the stage if it fails:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions describe a server prerequisite that wasn\'t met. Start the server per the instructions and re-run.\n ```\n\n3. **Derive the test command(s)** from the instructions, including any spec-directory batching the instructions specify.\n\n4. **If the instructions do not specify any runnable command**, skip and record:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions does not describe how to invoke tests; update that field with /teach-bridge, or re-run /learn-repository (a project admin must run it).\n ```\n\n5. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output and summary line of each batch.\n\n6. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Triage Logic\n\nFor every failing test, examine the test file and the code it tests. Classify as ONE of the following:\n\n### TEST-CODE ISSUE \u2014 fix it directly\n\nClassify as a test-code issue if ANY of the following applies:\n- The test asserts against a hardcoded value that no longer matches current behavior (outdated mock data)\n- The test imports or calls a function that was renamed, moved, or removed\n- The test asserts on a response field that was restructured\n- The test expects a specific error message string that has since changed\n- A fixture references a removed table column, model field, or schema member\n\n**Action**: Apply a minimal, targeted fix to the test file only. Then re-run just that failing test, using the runner described in the relevant instructions field (`unit_instructions` for unit-test failures, `e2e_instructions` for E2E failures). Adapt the runner invocation that the instructions provide to target a single test, following whatever convention the instructions or stack idiomatically use.\n\nIf the re-run **still fails** after your fix, do not make further edits \u2014 escalate to implementation-code issue instead and revert your change.\n\n### IMPLEMENTATION-CODE ISSUE (or UNCERTAIN) \u2014 document, do not fix\n\nClassify as an implementation issue if ANY of the following applies:\n- The production function raises an unexpected exception\n- A handler returns the wrong status code or response shape for a documented behavior\n- Business logic produces incorrect output that the test correctly asserts against\n- You are not confident the test is wrong\n\n**Action**: Do NOT modify any file outside the test directories described in `unit_testing_instructions` / `e2e_testing_instructions`. When in doubt about whether a path is test-only, treat it as production code and escalate. Record the failure in the implementation-issues document for the user to triage.\n\n## Stage 4 \u2014 Write Output Documents\n\n### Document 1: Test Run Report (always write this)\n\nWrite to: `{docs_dir}/testing/test-run-{run_timestamp}.md`\n\n```markdown\n# Test Run: {run_timestamp}\n\n## Configuration\n- Unit stack: {unit_stack or "not configured"}\n- E2E stack: {e2e_stack or "not configured"}\n- Unit tests: RUN | SKIPPED \u2014 (reason)\n- E2E tests: RUN | SKIPPED \u2014 (reason)\n\n## Unit Tests\n**Stack**: {unit_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**:\n- `path/to/test_file`: brief description of what was fixed\n- (or "none" if no fixes were needed)\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## E2E Tests\n**Stack**: {e2e_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## Overall Summary\n- Total test fixes applied: N\n- Suspected implementation issues found: N\n- Implementation issues document: {docs_dir}/testing/implementation-issues-{run_timestamp}.md\n (or "not created \u2014 no issues found")\n```\n\n### Document 2: Implementation Issues (only write if issues were found)\n\nIf at least one failure was escalated as an implementation-code issue, write to:\n`{docs_dir}/testing/implementation-issues-{run_timestamp}.md`\n\n```markdown\n# Suspected Implementation Issues: {run_timestamp}\n\nThese test failures were NOT fixed. They may indicate bugs in production code.\nA developer should investigate each item before merging.\n\n## Issue 1\n- **Test**: `path/to/test_file::test_function_name`\n- **Tier**: unit | e2e\n- **Failure message**: (paste the key assertion or exception line)\n- **Why not fixed**: (brief reasoning, e.g., "production function raises KeyError on valid input")\n\n## Issue 2\n...\n```\n\nIf no implementation issues were found, do NOT create this file.\n\n## Final Output\n\nAfter writing all documents, print this summary:\n\n```\nTest run complete: {run_timestamp}\nReport saved to: {docs_dir}/testing/test-run-{run_timestamp}.md\nImplementation issues: {docs_dir}/testing/implementation-issues-{run_timestamp}.md (if applicable)\nNo suspected implementation issues found. (if none)\n```\n',"scan-test-coverage.md":'Scan recently shipped tickets from git history and report which features have or could gain integration tests, and which can only be smoke tested.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nScan the git history for recently shipped tickets and, for each shipped feature, determine whether it already has an integration test, whether it *could* gain one (per this repo\'s conventions \u2014 a test that genuinely executes the system end-to-end via real database operations, real LLM calls, or real FastAPI routing), and \u2014 where integration testing is not possible \u2014 how it could be smoke tested so it still genuinely executes the system.\n\nThis is an **investigation and discovery** command. Describe features (citing code) and *how* they would be tested at a high level. Do **not** design tests in detail, build or edit any tests, or modify feature code. Orchestrate this run in the main thread, and **fan out one subagent per shipped feature** for the per-feature investigation.\n\nThe report is written to a **durable, committed** directory (`docs/test-coverage/`), and a marker file records when the analysis last ran so subsequent runs only inspect git history since the last run.\n\n## Stage 0 \u2014 Parse Arguments and Resolve Analysis Window\n\n1. Read `$ARGUMENTS`. All flags are optional and default-safe. If a flag is malformed, ignore it and add a warning:\n - `--since=YYYY-MM-DD` \u2014 override the window start date.\n - `--full` \u2014 ignore the marker and use a default lookback of 6 months.\n - `--limit=N` \u2014 cap the number of features investigated (parse `N` as an integer; ignore if not a valid integer).\n With no arguments, run **incrementally** from the marker.\n\n2. Set the durable directory to `docs/test-coverage/` (relative to the repo root) and the marker file to `docs/test-coverage/STATE.md`. This command deliberately does **not** use the configured docs directory (`ping`\'s `docs_dir`) \u2014 its default (`docs/tmp`) is ephemeral, and this report must be durable.\n\n3. Read `docs/test-coverage/STATE.md` if it exists. It records two values: `last_run_utc` (an ISO-8601 UTC timestamp) and `last_analyzed_commit` (a git commit SHA).\n\n4. Resolve the analysis window with this precedence:\n - If `--since=YYYY-MM-DD` was given, use `git log --since=<date>`.\n - Else if `STATE.md` provides `last_analyzed_commit`, use the commit range `<last_analyzed_commit>..HEAD`.\n - Else (first run, no marker), default to `git log --since=<3 months ago>` (mirrors the `/scan-tickets` default of 3 months). Format the date as `YYYY-MM-DD`. Example: if today is 2026-07-07, the default `--since` is `2026-04-07`.\n - `--full` overrides the above and uses a 6-month lookback (`--since=<6 months ago>`).\n\n5. Robustness of the marker: capture `head_sha` by running `git rev-parse HEAD`, and capture the current UTC timestamp now. These become the **new** marker values, but only write them after the report is successfully produced (Stage 4). If a stored `last_analyzed_commit` is not present in history (e.g. a rebase/rewrite), fall back to `git log --since=<the date part of last_run_utc>` and add a warning noting the fallback.\n\n6. Initialize tracking variables:\n - `features` = [] (one entry per shipped feature)\n - `warnings` = [] (per-item failures and fallbacks; the run never aborts on these)\n\n7. Display the resolved window, e.g. "Analyzing shipped features in `<range or --since date>` (HEAD = {head_sha})".\n\n## Stage 1 \u2014 Collect Shipped Features from Git History\n\n1. List merged commits in the resolved window with:\n ```bash\n git log <range> --first-parent --pretty=format:"%H|%h|%ad|%s" --date=short\n ```\n `--first-parent` yields roughly one entry per squashed PR merge.\n\n2. For each commit, extract the ticket key by matching `^BAPI-[0-9]+` against the subject. Group commits by ticket key. Commits with no ticket prefix (e.g. `Fix 500 on ...`) each become a standalone feature labeled as an "untracked change".\n\n3. For each group, collect the changed-file footprint across its commit(s) using `git show --stat <sha>` or `git diff --name-only`. This file footprint is the primary input to the per-feature investigation.\n\n4. Best-effort enrichment: for each ticket key, call the `get_ticket` MCP tool to fetch the ticket summary. This is **fail-open** \u2014 Jira tokens can be expired \u2014 so on any error, add a warning and continue without the summary. Do not abort.\n\n5. Build a `features` entry per group: `{ticket_key, subject, commit_shas, changed_files, jira_summary?}`. If `--limit=N` was given, keep only the first `N` features (most recent first).\n\n6. Display: "Found {count} shipped features to investigate."\n\n7. If `git log` returns no commits, skip to Stage 4 and write a report noting an empty window (and still refresh the marker).\n\n## Stage 2 \u2014 Investigate Each Feature (fan out subagents)\n\nFor each feature in `features`, launch an **Explore** subagent (batch several in parallel). Give each subagent the feature\'s `ticket_key`, `subject`, `changed_files`, and `jira_summary`, and instruct it to do read-only investigation only \u2014 no edits, no test design, no solutioning \u2014 and to return a structured finding.\n\nEach subagent must:\n\n1. Read the changed files and describe what the feature does in 2\u20134 sentences, with concrete `file:line` citations.\n\n2. Identify the feature\'s runtime surface \u2014 one or more of: real database operations (`postgres_client` / a DAL in `api/library/db/`), real LLM calls (`src/python/llms/ai_client.py`, `async_send_message_to_ai`), real FastAPI routing (a route handler under `api/routes/`), an MCP tool (`mcp_server/`), a shell-spawned / CLI flow, a frontend / Playwright surface, or pure logic / config / docs / tests.\n\n3. Check whether an **integration test already exists**: search `tests/integration/` for a mirror path or for references to the changed modules/functions. The reliable classifier is a path under `tests/integration/` plus `@pytest.mark.integration` or reliance on the `--run-integration` flag (conventions in `docs/claude/testing-integration.md`). Cite any test found.\n\n4. Classify the feature into exactly one `bucket`:\n - **`has_integration_test`** \u2014 already covered end-to-end; cite the existing integration test file.\n - **`integration_testable`** \u2014 no test yet, but the feature exercises real DB / LLM / routing and fits an existing `tests/integration/<area>/` pattern. Give a **high-level** approach only: which real entrypoint to call, which backend it would exercise, and the relevant cost/guard note (the gpt-5-nano override via `INTEGRATION_TEST_MODEL`; the local-DB `skipif` guard; `save_to_db=False`). Cite the entrypoint in code.\n - **`smoke_only`** \u2014 genuine end-to-end execution is possible but not as an automated integration test (e.g. MCP tool behavior inside a host, cross-platform terminal spawning, a headless agent session, or browser E2E). Describe how to smoke test it so it **genuinely executes the system**, citing the relevant runbook: the MCP smoke-test runbook under `mcp_server/smoke-test/`, `tests/mcp/`, `docs/claude/runbooks/self-install-smoke-test.md`, `docs/claude/runbooks/start-tickets-smoke-test.md`, or Playwright (`tests/playwright/`, which needs a running server plus `npm run build`).\n - **`not_testable`** \u2014 nothing to execute end-to-end (docs-only, a wording/comment change, pure config, or a test-only change); state why.\n\n5. Return a structured finding with these fields: `ticket_key`, `subject`, `description_with_cites`, `surface`, `bucket`, `existing_test`, `approach`, `why_not`.\n\nCollect all findings. If a per-feature subagent fails, add a warning and continue \u2014 never abort the whole run.\n\n## Stage 3 \u2014 Classify and Synthesize\n\n1. Deduplicate features that span multiple commits (merge by `ticket_key`).\n\n2. Sort each finding into the two required report sections:\n - **Section 1 \u2014 Integration Testing (covered or addable):** findings with `bucket` `has_integration_test` (sub-group "Already covered") or `integration_testable` (sub-group "Could be added").\n - **Section 2 \u2014 Not Integration-Testable:** findings with `bucket` `smoke_only` (sub-group "Smoke-testable \u2014 how") or `not_testable` (sub-group "Not testable \u2014 why").\n\n## Stage 4 \u2014 Write the Report and Update the Marker\n\n1. Create the `docs/test-coverage/` directory if it does not exist. Choose the report path `docs/test-coverage/REPORT-<YYYYMMDD>.md`; if a same-day file already exists, append `-<HHMMSS>` to avoid clobbering it.\n\n2. Write the report with this layout:\n - A title and a metadata block: generated-at UTC timestamp; the analysis window (`<from sha or since-date>` \u2192 `HEAD <head_sha>`); the feature count; and per-bucket tallies.\n - **Section 1 \u2014 Integration Testing: Covered or Addable.** One `### BAPI-NNN \u2014 <subject>` heading per feature, each with **What shipped** (with `file:line` citations), **Current coverage** (cite the existing integration test, or state "none"), and **How it could be integration tested (high level)**.\n - **Section 2 \u2014 Not Integration-Testable.** One heading per feature with the same feature description, plus **Why not integration-testable**, and \u2014 for `smoke_only` features \u2014 **How to smoke test (genuinely execute the system)** with the runbook citation.\n - A **Warnings** section listing each warning as a bullet \u2014 only if `warnings` is non-empty.\n\n3. **Only after** the report file is written successfully, update the marker `docs/test-coverage/STATE.md` with the new `last_run_utc` (the UTC timestamp captured in Stage 0) and `last_analyzed_commit` set to `head_sha`. This date/commit marker is what makes the next run incremental. If the report write fails, do not touch `STATE.md`.\n\n## Final Report\n\nPrint a short summary to chat:\n\n```\n**Test-coverage scan complete**\n\n* Features analyzed: {count}\n* Already covered by integration tests: {n_has}\n* Integration-testable (could be added): {n_addable}\n* Smoke-only: {n_smoke}\n* Not testable: {n_none}\n\nReport: docs/test-coverage/REPORT-<YYYYMMDD>.md\nMarker updated: last_analyzed_commit = {head_sha}\n```\n\nIf `warnings` is non-empty, add a "Warnings:" section listing each warning as a bullet. If there are no warnings, omit that section.\n',"scan-tickets.md":'$ARGUMENTS\n\n---\n\n# Instructions\n\nSynchronize recently-updated Jira tickets with the local `tickets` database table and backfill missing workflow state timestamps. Perform all work directly in the main thread.\n\n## Stage 0 \u2014 Parse Arguments and Calculate Date\n\n1. Read the value of `$ARGUMENTS`. If it is empty, whitespace-only, or not a valid integer, default `months_back` to `3`. If it contains multiple tokens, extract only the first token and attempt to parse it as an integer. If parsing fails, default to `3`.\n\n2. Calculate `updated_since` by subtracting `months_back` months from today\'s date. Format the result as `YYYY-MM-DD`. Example: if today is 2026-03-07 and `months_back` is 3, then `updated_since` is 2025-12-07.\n\n3. Display the parsed values: "Scanning tickets updated since {updated_since} (months_back = {months_back})"\n\n4. Initialize the following tracking variables:\n - `tickets_scanned` = 0 (total tickets fetched from Jira)\n - `newly_tracked` = 0 (tickets inserted into database for the first time)\n - `state_updated_list` = [] (list of objects with ticket key and fields updated)\n - `warnings` = [] (list of warning strings for any per-ticket failures)\n\n## Stage 1 \u2014 Fetch All Tickets from Jira\n\n1. Initialize an empty list `all_tickets` and set `offset` to `0`.\n\n2. Enter a pagination loop:\n - Call the `get_tickets` MCP tool with: `updated_since` set to the calculated date, `limit` set to `100`, and `offset` set to the current offset value.\n - Parse the JSON response. The response contains a `tickets` array of ticket objects. Each ticket object has a `ticket_number` field (the Jira key, e.g., `BAPI-42`), along with `summary`, `status`, `issue_type`, `assignee`, and `updated_at`.\n - Append all tickets from the response\'s `tickets` array to `all_tickets`.\n - If the number of tickets returned in this page equals `100`, increment `offset` by `100` and repeat the loop.\n - If fewer than `100` tickets are returned, exit the loop.\n\n3. Set `tickets_scanned` to the length of `all_tickets`.\n\n4. Display: "Fetched {tickets_scanned} tickets from Jira. Processing..."\n\n5. If the `get_tickets` call fails at any point during pagination, **stop** and report the error. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Track Each Ticket\n\n1. Iterate over each ticket in `all_tickets`. For each ticket:\n - Call the `track_ticket` MCP tool with `ticket_number` set to the ticket\'s `ticket_number` field. If the ticket object includes a `summary` field, pass it as the `description` parameter.\n - Inspect the response message. If the response indicates the ticket was newly created/inserted (look for words like "created" or "inserted" in the message, as opposed to "already exists" or "updated"), increment `newly_tracked` by 1.\n - If the `track_ticket` call fails for this ticket, add a warning to the `warnings` list (e.g., "Warning: Failed to track ticket {ticket_number}: {error}") and **continue** to the next ticket. Do not abort the scan.\n\n2. Display a brief progress indicator every 25 tickets, e.g., "Tracked {N} of {tickets_scanned} tickets..."\n\n## Stage 3 \u2014 Detect and Backfill Workflow State\n\nDisplay: "Checking workflow state for {tickets_scanned} tickets..."\n\nIterate over each ticket in `all_tickets`. For each ticket (referenced by its `ticket_number` field), perform the following sub-steps. Wrap the entire per-ticket block in error handling: if the `get_ticket_state` call or the subsequent `update_ticket_state` call fails for a ticket, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4a \u2014 Retrieve current state**: Call the `get_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key. The response contains:\n\n- Five timestamp fields (each is a timestamp string or null): `clarify_called`, `clarify_answered`, `critique_called`, `critique_answered`, `plan_generated`\n- Three boolean artifact flags: `has_clarifying_questions`, `has_critique`, `has_plan`\n\nIf the call returns a 404 or any error, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4b \u2014 Build fields_to_update list**: Initialize an empty `fields_to_update` list, then apply the following rules:\n\n- If `has_clarifying_questions` is `true` AND `clarify_called` is null -> add `"clarify_called"` to `fields_to_update`\n- If `has_clarifying_questions` is `true` AND `clarify_answered` is null -> add `"clarify_answered"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_called` is null -> add `"critique_called"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_answered` is null -> add `"critique_answered"` to `fields_to_update`\n- If `has_plan` is `true` AND `plan_generated` is null -> add `"plan_generated"` to `fields_to_update`\n\n**Sub-step 4c \u2014 Call update_ticket_state if needed**: If `fields_to_update` is non-empty, call the `update_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key and `fields` set to the `fields_to_update` array. If this succeeds, add an entry to `state_updated_list` recording the ticket key and the list of fields that were set. If `update_ticket_state` fails, add a warning to `warnings` and continue.\n\nDisplay a progress indicator every 25 tickets that includes the current ticket key, e.g., "Checked state for {TICKET-KEY} ({N} of {tickets_scanned} tickets)"\n\n## Stage 4 \u2014 Report Summary\n\n1. Calculate `state_updated_count` as the length of `state_updated_list`.\n\n2. Display the summary:\n\n ```\n **Scan complete**\n\n * Tickets scanned: {tickets_scanned}\n * Newly tracked: {newly_tracked}\n * State updated: {state_updated_count}\n ```\n\n3. If `state_updated_list` is non-empty, display a section titled "Updated tickets:" with one bullet per ticket showing the ticket key and the comma-separated list of fields that were set. Example:\n\n ```\n Updated tickets:\n * BAPI-101: clarify_called, clarify_answered\n * BAPI-105: critique_called, critique_answered, plan_generated\n ```\n\n4. If the `warnings` list is non-empty, display a section titled "Warnings:" listing each warning string as a bullet. Example:\n\n ```\n Warnings:\n * Warning: Failed to track ticket BAPI-99: Connection timeout\n * Warning: State query failed for BAPI-112: SQL error\n ```\n\n5. If there are no warnings, do not display the "Warnings:" section.\n',"start-tickets.md":'---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"agent","flag":"--agent","type":"string"},{"name":"host","flag":"--host","type":"string"},{"name":"workflow","flag":"--workflow","type":"string"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"guardStaleBranch","flag":"--guard-stale-branch","type":"boolean"}]}\n---\n\n# Start Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248 BAPI-250`) and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `start-tickets`, which creates a Worktrunk worktree for each key and opens one tab/session per worktree running the **selected agent**. An explicit `--agent` wins; otherwise `--host` resolves a declared runnable host (Claude Code, Cursor, or OpenAI Codex), and an omitted host retains the Claude default. Copilot and other non-runnable hosts return manual-continuation guidance. Codex receives a verified per-process Bridge MCP scope bound to its worktree.\n\nBecause the orchestration ships inside the `@bridge_gpt/mcp-server` npm package (not a repo-local script), this command works for every consumer \u2014 including projects that installed the package via `--init`.\n\nFor existing ticket keys, `/review-and-start <KEYS>` is the **recommended front door**: it supplies the same connectivity check and branch enrichment as this command, then drives this same packaged CLI with `--workflow review-and-implement` so each worktree reviews the ticket before implementing it. Using `start-tickets --workflow review-and-implement` directly (documented below) remains available as the lower-level launcher seam.\n\nStage 0 and Stage 1 are critical (stop on failure). Stage 2 is non-critical (per-ticket enrichment failures fall back to the default branch and continue). Stage 3 is critical (propagate the packaged CLI\'s exit code).\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that spawns N parallel Worktrunk worktrees and selected-agent sessions (Claude Code by default) via the packaged CLI. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys, pass-through flags, and branch overrides:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). If zero keys are found, stop immediately and display:\n\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /start-tickets [flags] <KEY> [KEY ...] (e.g., /start-tickets BAPI-248 BAPI-250)\n ```\n\n - **Pass-through flags**: collect any of `--agent <name>` (and the equals form `--agent=<name>`), `--host <platform>` (and the equals form `--host=<platform>`), `--terminal terminal|iterm`, `--dry-run`, `--auto`, `--no-refresh-main`, `--base-branch <branch>` (and the equals form `--base-branch=<branch>`), `--max-parallel N`, and `--guard-stale-branch` that the user supplied. These are forwarded verbatim to the CLI in Stage 3. `--guard-stale-branch` turns on the F7 stale-branch guard: a pre-existing `feature/<KEY>` branch whose tip is **not** an ancestor of the resolved base is refused with a `create-failed` row carrying the stale-worktree remedy, instead of being silently reused on top of another run\'s commits. It is off by default (a plain run keeps reusing a same-named branch) and is implied by `--conductor`. `--auto` makes each spawned agent run the selected workflow\'s slash command with `--auto` (hands-off); omit it to keep the spawned agents interactive.\n - **Selected agent**: track a `selected_agent` variable. Validate explicit `--agent <name>` / `--agent=<name>` against `claude`, `cursor-agent`, and `codex`; it always takes precedence. When omitted, accept optional `--host <platform>` and resolve it through the host capability matrix. A host lacking MCP configuration, custom commands, worktree provisioning, or session spawning returns manual-continuation guidance; it must never fall back to Claude. With neither flag in a Codex session, set `selected_agent` to `codex` and pass `--host codex` to the CLI. Otherwise retain the backward-compatible default `claude`.\n - **Selected workflow**: track a `selected_workflow` variable that defaults to `implement`. If the user passed `--workflow <value>` or `--workflow=<value>`, validate it against the two allowed values `implement` and `review-and-implement`, set `selected_workflow`, and reject any other value with the allowlist in the error. `implement` (the default) preserves today\'s behavior byte-for-byte \u2014 each spawned worktree runs `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]` instead, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` inside the same session. A single chain-level `--auto` applies to the selected workflow as a whole \u2014 under `review-and-implement` it auto-approves both the review and the implementation phase.\n - **Review rounds**: track a `review_rounds` value that defaults to unset. If the user passed `--rounds <n>` or `--rounds=<n>`, normalize it to `--rounds=1` or `--rounds=2` (reject any other value). `--rounds` is **review-only**: reject it (after parsing all flags, so flag order does not matter) if the final `selected_workflow` is not `review-and-implement`.\n - **User-supplied base branch**: track a `user_supplied_base_branch` boolean that defaults to `false`. If the user passed `--base-branch <branch>` or `--base-branch=<branch>`, set the boolean to `true` and capture the value. A user-supplied `--base-branch` value **takes precedence** over any value resolved from Bridge API config in Stage 2. Validate the user-supplied value before proceeding: after trimming surrounding whitespace it must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`); reject any malformed value with a clear error.\n - **User branch overrides**: collect any user-supplied repeatable `--branch KEY=BRANCH` flags. A user-provided override always takes precedence over Stage 2 enrichment for that key.\n - Reject malformed or unsupported combinations before proceeding: if a token looks like a flag but is not one of the supported flags, or a ticket key does not match `[A-Z]+-[0-9]+`, or a `--branch` value is not `KEY=BRANCH`, or `--agent`/`--host` is unrecognized, or `--workflow` names anything other than `implement`/`review-and-implement`, or `--rounds` is used outside `review-and-implement` or names anything other than `1`/`2`, or `--base-branch` fails the validation rules above, stop and report the malformed argument.\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `"status": "ok"`, stop immediately and display:\n\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged CLI runs its own per-platform pre-flight checks and then fetches `origin` and fast-forwards the local **configured base branch** (the value resolved in Stage 2 below, or `main` when none is configured) from `origin/<base>` so the new worktrees are based on an up-to-date base. The historical flag `--no-refresh-main` still controls this behavior \u2014 the flag name is preserved for backward compatibility, but it now skips refresh of whatever base branch resolves (default `main`). The required commands depend on the OS:\n\n- **macOS**: `wt`, `git`, `osascript`.\n- **Windows**: `git-wt`, `git`, Git for Windows / Git Bash (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash), and Windows Terminal **or** PowerShell.\n- **Linux**: `wt`, `git`, `tmux`.\n\nOn **Windows** the Worktrunk binary is `git-wt` (its winget alias), which is a different tool from Windows Terminal\'s `wt.exe`: the CLI uses `git-wt` to **create worktrees** and `wt.exe` to **open a tab**, and never conflates the two. On **Linux** the CLI opens one detached `tmux` session per ticket (a window is added if that ticket\'s session already exists); attach later with `tmux attach -t <session>`. An unsupported OS (not macOS/Windows/Linux) fails fast with a clear "unsupported platform" message.\n\nThis stage simply notes that the CLI will fail fast if any prerequisite is missing or if local `main` has diverged from `origin/main` \u2014 you do not need to verify anything separately here, and you must not run any pre-flight commands yourself. When the CLI\'s pre-flight fails it now hints the user to run the read-only diagnostics command `npx -y @bridge_gpt/mcp-server doctor`, which reports found/missing for every prerequisite on the current OS \u2014 the pre-flight set plus `uv` plus the selected agent\'s command \u2014 and prints the manual install command for each missing one. `doctor` is strictly read-only and never installs anything; never run install commands automatically on the user\'s behalf. The CLI does not call any Bridge API tools; all credential-bearing work (branch enrichment in Stage 2) stays in this command. Proceed to Stage 2.\n\nThe packaged CLI also performs **secret-free Bridge API MCP provisioning** inside each created worktree: synchronously after the worktree is created and **before the agent tab/session is opened**, it writes both `.mcp.json` (Claude Code) and `.cursor/mcp.json` (Cursor) pointing at the `mcp-invoke` shim. These registrations are **secret-free** \u2014 they contain no `env` block and no API key, because the shim resolves credentials at runtime. If a spawned agent (or difficulty\u2192model routing) reports missing Bridge API credentials, fix it by rerunning `/install-bridge` (its final stage persists the routing credential), by running `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate a key that lives only in `.mcp.json` / `.cursor/mcp.json`, or by adding a `bapi:<repo>` entry to the user-scoped credentials file (`~/.config/bridge/credentials.json`) \u2014 never by putting `BAPI_API_KEY` into the worktree `.mcp.json` or `.cursor/mcp.json` (that env is invisible to the Bash-spawned CLI).\n\nThis stage is **critical** in the sense that the CLI will abort if its pre-flight fails; you will see the error in Stage 3\'s output and must surface it.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve configured `base_branch`\n\nThe CLI must be told which branch to cut new worktrees from. Resolution order:\n\n1. If `user_supplied_base_branch` from Stage 0 is `true`, **skip the config-field lookup entirely** and use the user-supplied value. The user\'s explicit `--base-branch` always wins; never call `config_field` for `base_branch` in that case.\n2. Otherwise, call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch` (do not pass any other parameters; the tool resolves the repository from the MCP server\'s configured `BAPI_REPO_NAME`).\n3. Parse the response. Treat the result as the **configured base branch** only when the response is a JSON object whose `value` field is a non-empty string after trimming surrounding whitespace.\n4. Treat **all** of the following as "unset" \u2014 emit a single-line warning like `Warning: base_branch is unset; CLI will default to main` and **omit** the `--base-branch` flag entirely from the Stage 3 command (the CLI\'s own default is `main`):\n - `value` is `null`.\n - `value` is an empty string or a whitespace-only string.\n - The endpoint returns HTTP `400` (invalid field \u2014 happens before the registry includes `base_branch`).\n - The tool returns a network error, timeout, or non-JSON parse failure.\n - Any other lookup failure.\n5. When the configured value is usable, capture it in a `resolved_base_branch` variable. **Do not** stop the pipeline on a lookup failure; fall through to the CLI default.\n\nWhen forwarding `resolved_base_branch` into the Bash invocation in Stage 3, **shell-escape it safely**: replace every literal single quote `\'` in the value with the four-character sequence `\'\\\'\'`, then wrap the entire resulting string in single quotes (so the final argument looks like `\'<escaped-value>\'`). This is the standard POSIX single-quote escaping rule and is **mandatory** because `base_branch` is admin-configurable data that gets interpolated into a Bash command string; any unescaped single quote would otherwise break out of the surrounding quotes. Pass `--base-branch \'<escaped-value>\'` to the CLI as a single argv element \u2014 never expand the value unquoted into the command line.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nBranch enrichment happens here, in the command, **before** invoking the CLI \u2014 the `get_ticket` MCP tool runs inside the MCP server process, which holds the Bridge API credentials the shell-spawned CLI does not have. For each parsed ticket key that does **not** already have a user-provided `--branch` override:\n\n1. Call the `get_ticket` MCP tool with `ticket_number` set to the key and `save_locally` set to `false`.\n2. From the response, extract the `summary` field. Slugify it: lowercase the string, replace every run of non-alphanumeric characters (`[^a-z0-9]+`) with a single dash `-`, trim leading and trailing dashes, and truncate to at most `40` characters (cutting at a dash boundary if possible).\n3. The enriched branch name is `feature/<KEY>-<slug>`. Example: `BAPI-248` with summary `"Add PR rating pre-evaluation step"` becomes `feature/BAPI-248-add-pr-rating-pre-evaluation-step` (trimmed at 40 chars).\n4. If the `get_ticket` call fails for a particular key (404, network error, missing summary) or produces an empty slug, emit a single-line warning like `Warning: could not enrich BAPI-248, falling back to feature/BAPI-248` and let the CLI apply its default `feature/<KEY>` for that key only. Do NOT stop the pipeline.\n5. Build a list of `--branch <KEY>=<BRANCH>` arguments \u2014 one entry per key whose enrichment succeeded \u2014 and merge it with any user-provided overrides from Stage 0. **Do not** call `get_ticket` for keys that already have a user-provided override; those overrides win.\n\nThis stage is **non-critical** \u2014 warnings are acceptable, the pipeline continues with the fallback default for any key that fails. Do not call the Bridge API from the CLI itself; the CLI never has credentials.\n\n## Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation in this command. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** \u2014 `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** \u2014 `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set:\n\n- Apply this command\'s mandatory single-quote escaping rule (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string. Never expand it unquoted.\n- Keep every dynamic argument \u2014 ticket keys, branch names, base branches, file paths \u2014 independently quoted. Never concatenate an argument into the launcher value.\n- Never put a credential, an API key, or an environment assignment carrying one into the launcher value, an example, or a dry-run preview.\n\nWorked examples and printed remediation hints below show the **unset** resolution \u2014 the literal `npx -y @bridge_gpt/mcp-server` \u2014 because that is the default every operator gets. A stale local build is exactly as misleading as a stale npm publish: rebuild with `cd mcp_server && npm run build` before relying on the override.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke the packaged CLI. Build the command line as:\n\n```\n<launcher> start-tickets <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` are the supported flags collected in Stage 0 (`--agent`, `--host`, `--terminal`, `--dry-run`, `--auto`, `--no-refresh-main`, `--max-parallel`, `--guard-stale-branch`), forwarded verbatim. Forward `--agent <name>` or `--host <platform>` only if the user supplied it; otherwise omit both and the CLI defaults to `claude`. When both were supplied, forward both so the CLI can enforce its documented `--agent` precedence. Forward `--auto` only if the user supplied it.\n- Forward `--workflow <selected_workflow>` only when the user explicitly passed `--workflow`; otherwise omit it and the CLI defaults to `implement`. Forward the normalized `--rounds=<n>` from Stage 0 only when the user supplied it (which Stage 0 already guarantees is only possible under `review-and-implement`).\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` (single-quoted using the Stage 2a escaping rule) **only when** the user supplied `--base-branch` in Stage 0 **or** Stage 2a\'s `config_field` lookup returned a non-empty configured value. When the configured value is unset / lookup fails / user did not supply one, **omit this flag entirely** so the CLI\'s own default (`main`) takes effect.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2 (enrichment results merged with user overrides; omit any key whose enrichment failed and had no user override).\n- `<ticket-keys>` is the original list of ticket keys parsed in Stage 0, space-separated and in the original order.\n\nExample for two tickets after successful enrichment, throttled to 2 concurrent worktrees:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets \\\n --max-parallel 2 \\\n --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step \\\n --branch BAPI-250=feature/BAPI-250-deep-research-durability \\\n BAPI-248 BAPI-250\n```\n\nExample launching Cursor Agent instead of the default Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\nExample cutting worktrees from a non-`main` base (either user-supplied via `--base-branch develop` in Stage 0 or resolved from Bridge API config in Stage 2a):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --base-branch develop BAPI-248\n```\n\nExample using the lower-level review-and-implement workflow directly (the `/review-and-start` command is the recommended front door for this; this form is documented here as the advanced launcher seam it drives):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=2 BAPI-248\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket in the form `KEY branch=BRANCH status=STATUS`, with an optional trailing `path=PATH`) and reformat it as a markdown table:\n\n```\n| Ticket | Branch | Status |\n|----------|-----------------------------------------------------|----------|\n| BAPI-248 | feature/BAPI-248-add-pr-rating-pre-evaluation-step | spawned |\n| BAPI-250 | feature/BAPI-250-deep-research-durability | spawned |\n```\n\nStatus values are `dry-run`, `spawned`, `create-failed`, and `spawn-failed`. This table (and the report as a whole) describes **worktree/spawn status only** \u2014 it must never claim that review or implementation itself has completed; that work happens later, independently, inside each spawned session.\n\nCompute `spawned_command` from `selected_workflow`: `/implement-ticket <KEY>` or `/review-and-implement <KEY>` for Claude/Cursor, `$bridge-implement-ticket <KEY>` or `$bridge-review-and-implement <KEY>` for Codex. Append `--auto` when supplied and, for `review-and-implement`, the normalized `--rounds=<n>` when supplied; Codex review chains also carry `--agent codex --host codex`. End the report with the worktree-first explanation, rendered for the tracked `selected_agent` and `spawned_command`. When `selected_agent` is `claude` (the default):\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`claude \'<spawned_command>\'` inside its already-created worktree, which launches\nClaude Code with the starter prompt as its first message. Switch to each tab \u2014 or on\nLinux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `cursor-agent`, render the same explanation but with the Cursor handoff \u2014 do **not** claim it launches Claude Code:\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`cursor-agent \'<spawned_command>\'` inside its already-created worktree, which\nlaunches Cursor Agent with the starter prompt as its first message. Switch to each\ntab \u2014 or on Linux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `codex`, report the `$bridge-implement-ticket` or `$bridge-review-and-implement` starter prompt and the per-worktree MCP scope. Under `review-and-implement`, each spawned session independently runs the host\'s review-ticket command, pauses at its per-ticket halt gate (unless chain-level `--auto` was passed), and only then opens a fresh implementation session. Do not report that either phase succeeded from this parent session.\n\nIf the CLI reported any `create-failed` or `spawn-failed` statuses, or Stage 2 emitted any enrichment warnings, list them under a `Warnings:` heading at the bottom of the report. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the deep-dive runbook and the Worktrunk verification result behind this worktree-first model.\n\n## Difficulty-Based Implementation-Model Routing\n\nBefore launching the interactive agent for each ticket, the packaged CLI selects an\nimplementation **model tier** from the ticket\'s `difficulty` rating (1-10) and may\ninject `--model` at the agent spawn boundary. This happens entirely inside the\nCLI \u2014 it is **not** part of the server-side `/implement-ticket` recipe, because the\nmodel an interactive agent session uses is fixed at the moment the process is launched.\n\n- **Tier ladder (fixed):** `difficulty 1-2 \u2192 cheap`, `3-6 \u2192 basic`, `7-10 \u2192 premium`.\n- **Separation of concerns:** the Python backend returns only the coarse tier\n (`cheap`/`basic`/`premium`) via `GET /jira/tickets/{KEY}/model-tier`; difficulty is\n computed on demand and cached when absent. Claude/Cursor use the TypeScript\n agent registry and Cursor validates against `cursor-agent --list-models`. Codex\n fetches `GET /jira/codex-model-catalog?repo_name=...` once per invocation;\n Bridge derives its bare IDs and revision directly from `MODEL_REGISTRY`.\n- **Per-repo config:**\n - `difficulty_model_routing_enabled` \u2014 boolean, **default ON**. Set to `false` to\n disable routing for a repo (the CLI then omits `--model`).\n - `difficulty_model_tier_overrides` \u2014 a JSON object mapping a tier name to a model\n alias (e.g. `{"premium": "opus"}`), **not** raw CLI arguments. Only `cheap`,\n `basic`, and `premium` keys are accepted; aliases must match `^[A-Za-z0-9._:-]+$`.\n Applies to Claude/Cursor only; Codex uses the backend catalog.\n- **Fail-open:** routing never aborts a spawn. Credential, network, config, or\n no-tier routing failures retain the premium fallback for Claude/Cursor when\n its alias is valid. Codex omits `--model` on access, config, tier, catalog,\n revision, or alias failure and launches its configured default. Disabled\n routing also omits `--model`. Each degraded invocation emits a secret-free\n routing diagnostic. Bridge\'s catalog states provider policy, not whether the\n local Codex account can use a model; no supported non-interactive account\n entitlement probe is available, so the CLI reports that limitation.\n\n### Model routing credential\n\nDifficulty\u2192model routing needs Bridge API credentials, and the shell-spawned\n`start-tickets` CLI is a **different runtime surface** from the MCP server: a\n`BAPI_API_KEY` that lives only in `.mcp.json` / `.cursor/mcp.json` is visible to\nthe MCP server but **not** to the Bash-spawned CLI, so routing silently degrades.\nThe durable source of truth both runtimes can resolve is the user-scoped store\n`~/.config/bridge/credentials.json`, keyed `bapi:<repo>`. If a routing-diagnostic\nline reports the credential is missing (e.g. difficulty resolves as `?`), fix it\nby any one of:\n\n1. Rerun `/install-bridge` \u2014 its final stage now persists the validated routing\n credential into `~/.config/bridge/credentials.json` via the\n `persist_routing_credential` tool.\n2. Migrate a key that lives **only** in `.mcp.json` / `.cursor/mcp.json` into the\n user-scoped store with the consent-gated, one-shot command:\n\n ```\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials\n ```\n\n3. Manually add `BAPI_API_KEY` under the `bapi:<repo>` target in the user-scoped\n store `~/.config/bridge/credentials.json`.\n\nNever put `BAPI_API_KEY` into a worktree `.mcp.json` / `.cursor/mcp.json` as a fix \u2014\nthat env is invisible to the spawned CLI.\n\n## Conductor observability (opt-in via `--conductor`, BAPI-394)\n\nConductor is **opt-in**. By default `start-tickets` spawns the plain\n`cd <worktree> && <agent> \'/implement-ticket <KEY> [--auto]\'` \u2014 no\n`BAPI_CONDUCTOR_*` env, no supervisor window, and no message-relay instruction.\nPass `--conductor` (e.g. `/start-tickets --conductor BAPI-123`) to enable the\nConductor system below.\n\nWith `--conductor`, a run mints a single conductor `run_id` and attributes each\nworker\'s lifecycle events by `worker_id`, ticket key, and worktree path, and a\nsupervisor peer tab is opened. When the selected agent is **Claude Code**, the CLI\ninjects a conductor lifecycle hook into each created worktree\'s\n`.claude/settings.local.json` so the spawned session emits local `run.started` /\n`run.stopped` / `agent.notification` (and, when\n`BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1`, `tool.intent`) events into the local\nconductor ledger. These hooks apply **only** when the selected agent is Claude\nCode; other agents (e.g. `cursor-agent`) still participate in the run-level\n`run.started` event but receive no per-worktree Claude hook. Inspect the ledger\nwith the `epic-implementer` CLI (e.g. `epic-implementer doctor`). Conductor observability is\nbest-effort and never blocks or aborts a spawn.\n\nObservability under `--conductor` is one-directional: workers emit lifecycle events\ninto the local ledger and nothing is passed back into a running session. (Epic-tick\ndispatch always runs with conductor enabled, independent of this user-facing flag.)\n',"teach-bridge.md":`Update a Bridge API configuration field via a natural-language teaching.
1845
1845
 
1846
1846
  $ARGUMENTS
1847
1847
 
@@ -3640,9 +3640,9 @@ ${failures.map(f=>` ${f.relPath}: ${f.reason}`).join(`
3640
3640
 
3641
3641
  `;return{action:"append",content:content+boundary+renderCodexBridgeToml(entry)}}async function readTomlHostConfig(path59,deps){try{return{state:"text",content:await deps.readFile(path59)}}catch(err){return isEnoent(err)?{state:"missing"}:{state:"read-error",message:"config could not be read"}}}async function writeCodexHostConfig(path59,entry,deps){let read=await readTomlHostConfig(path59,deps),merge=mergeCodexHostConfig(read,entry);return merge.action==="duplicate-registration"?{status:"skipped-duplicate-registration",message:DUPLICATE_REGISTRATION_DETAIL}:merge.action==="manual-required"?{status:"manual-required"}:(await deps.mkdir(dirnameOf(path59),{recursive:!0}),await deps.writeFile(path59,merge.content),{status:merge.action==="create"?"created":"direct-written",path:path59})}async function inspectJsonHostEntry(path59,target,deps){let read=await readJsonHostConfig(path59,deps);if(read.state!=="valid")return{present:!1,registrationKey:null};let root=read.value[hostAdapterForTarget(target).topLevelKey];if(!root||typeof root!="object"||Array.isArray(root))return{present:!1,registrationKey:null};let resolution=resolveRegistrationKey(root);if(resolution.state==="conflict")return{present:!0,registrationKey:null,duplicateRegistration:!0};if(resolution.state==="absent")return{present:!1,registrationKey:null};let entry=root[resolution.key];if(!entry||typeof entry!="object")return{present:!1,registrationKey:null};let args=entry.args;return{present:!0,registrationKey:resolution.key,args:Array.isArray(args)?args.filter(a=>typeof a=="string"):void 0}}async function inspectTomlHostEntry(path59,deps){let read=await readTomlHostConfig(path59,deps);if(read.state!=="text")return{present:!1,registrationKey:null};let names=codexBridgeTableNames(read.content);if(names.length===0)return{present:!1,registrationKey:null};if(names.length>1)return{present:!0,registrationKey:null,duplicateRegistration:!0};let m=read.content.match(/^\s*args\s*=\s*\[(.*?)\]/m),args;return m&&(args=m[1].split(",").map(s=>s.trim().replace(/^["']|["']$/g,"")).filter(s=>s.length>0)),{present:!0,registrationKey:names[0],args}}async function inspectHostEntry(target,ctx,deps){let path59=resolveTargetAbsPath(target,ctx);return hostAdapterForTarget(target).format==="toml"?inspectTomlHostEntry(path59,deps):inspectJsonHostEntry(path59,target,deps)}function storedProfileTokens(value){return typeof value!="string"?[]:value.split(",").map(t=>t.trim()).filter(t=>t.length>0)}function entryProfileTokens(entry){if(!entry||typeof entry!="object"||Array.isArray(entry))return null;let env=entry.env,stored=env&&typeof env=="object"&&!Array.isArray(env)?env.BRIDGE_MCP_PROFILE:void 0;return storedProfileTokens(stored)}async function inspectOneProfileConfig(projectRoot,target,token,deps){let read=await readJsonHostConfig(joinPath2(projectRoot,target.path),deps);if(read.state==="missing")return{path:target.path,state:"absent"};if(read.state!=="valid")return{path:target.path,state:"unreadable"};let root=read.value[target.topLevelKey];if(!root||typeof root!="object"||Array.isArray(root))return{path:target.path,state:"no-bridge-entry"};let servers=root,tokenSets=RECOGNIZED_SERVER_NAMES.filter(name=>Object.prototype.hasOwnProperty.call(servers,name)).map(name=>entryProfileTokens(servers[name])).filter(tokens=>tokens!==null);return tokenSets.length===0?{path:target.path,state:"no-bridge-entry"}:{path:target.path,state:tokenSets.some(tokens=>tokens.includes(token))?"token-present":"token-absent"}}async function inspectBridgeApiProfileToken(projectRoot,token,deps){let configs=[];for(let target of BRIDGE_PROFILE_TOKEN_CONFIGS)configs.push(await inspectOneProfileConfig(projectRoot,target,token,deps));return{token,configs,tokenPresent:configs.some(c=>c.state==="token-present"),bridgeEntryPresent:configs.some(c=>c.state==="token-present"||c.state==="token-absent")}}function isVendorContractVerified(kind){return kind==="claude-add-json"}function buildVendorInvocation(target,entry){let vendor=target.vendorCli;if(!vendor||!isVendorContractVerified(vendor.kind))return null;if(vendor.kind==="claude-add-json"){let json=JSON.stringify({command:entry.command,args:entry.args,env:entry.env});return{bin:vendor.bin,args:["mcp","add-json",MCP_SERVER_NAME,json,"--scope","project"]}}return null}function outcome(target,status,detail){return{platform:target.id,status,displayPath:target.displayPath,detail}}async function directWriteTarget(target,entry,deps){let path59=resolveTargetAbsPath(target,deps);if(hostAdapterForTarget(target).format==="toml"){let res2=await writeCodexHostConfig(path59,entry,deps.fs);return res2.status==="skipped-duplicate-registration"?outcome(target,"skipped-duplicate-registration",res2.message):res2.status==="manual-required"?outcome(target,"manual-required","existing config could not be safely updated"):outcome(target,"direct-written")}let adapted=adaptBridgeEntryForHostTarget(entry,target),res=await writeJsonHostConfig(path59,target,adapted,deps.fs);return res.status==="skipped-duplicate-registration"?outcome(target,"skipped-duplicate-registration",res.message):res.status==="skipped-invalid"?outcome(target,"skipped-invalid",res.message):outcome(target,"direct-written")}async function provisionHostTarget(target,entry,deps){if(target.writeStrategy==="manual-instructions")return outcome(target,"manual-required","global config \u2014 manual setup");if(target.writeStrategy==="direct")return directWriteTarget(target,entry,deps);let preInspection=await inspectHostEntry(target,deps,deps.fs);if(preInspection.duplicateRegistration)return outcome(target,"skipped-duplicate-registration",DUPLICATE_REGISTRATION_DETAIL);if(!(preInspection.present&&preInspection.registrationKey!==null&&preInspection.registrationKey!==void 0&&preInspection.registrationKey!==MCP_SERVER_NAME)){let invocation=buildVendorInvocation(target,entry);if(invocation&&await deps.vendor.probeBinary(invocation.bin,deps.env)&&(await deps.vendor.invokeVendorAdd(invocation,deps.env)).ok){let inspected=await inspectHostEntry(target,deps,deps.fs);if(inspected.duplicateRegistration)return outcome(target,"skipped-duplicate-registration",DUPLICATE_REGISTRATION_DETAIL);if(inspected.present)return outcome(target,"vendor-written")}}return directWriteTarget(target,entry,deps)}function sanitizeProbeEnv(env){let clone={...env};for(let key of PROBE_SECRET_KEYS)delete clone[key];return clone}function currentLauncherSpec(){return`${MCP_PACKAGE_NAME}@${VERSION}`}function createDefaultVendorProcessDeps(spawnFn){return{probeBinary:(bin,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2(ok))};try{let child=spawnFn(bin,["--version"],{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}}),invokeVendorAdd:(invocation,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2({ok}))};try{let child=spawnFn(invocation.bin,invocation.args,{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}})}}var DUPLICATE_REGISTRATION_DETAIL,CODEX_ENV_ORDER,BRIDGE_TABLE_PATTERNS,BRIDGE_PROFILE_TOKEN_CONFIGS,PROBE_SECRET_KEYS,VENDOR_PROCESS_TIMEOUT_MS,init_mcp_host_config=__esm({"src/mcp-host-config.ts"(){"use strict";init_version_generated();init_codex_home();init_init();init_mcp_host_targets();init_mcp_host_entry_adapter();init_mcp_host_entry_adapter();init_mcp_identity();DUPLICATE_REGISTRATION_DETAIL=`both \`${MCP_SERVER_NAME}\` and \`${LEGACY_SERVER_NAMES.join("`, `")}\` are registered here; confirm which one to keep, remove the other manually, then re-run. Nothing was changed.`;CODEX_ENV_ORDER=["BAPI_BASE_URL","BAPI_REPO_NAME","BAPI_API_KEY","BAPI_DOCS_DIR","BAPI_PROJECT_ROOT"];BRIDGE_TABLE_PATTERNS=RECOGNIZED_SERVER_NAMES.map(name=>({name,pattern:new RegExp(`^[ \\t]*\\[mcp_servers\\.${escapeRegExp(name)}(\\.[^\\]]*)?\\][ \\t]*$`,"m")}));BRIDGE_PROFILE_TOKEN_CONFIGS=[{path:".mcp.json",topLevelKey:"mcpServers"},{path:".vscode/mcp.json",topLevelKey:"servers"},{path:".cursor/mcp.json",topLevelKey:"mcpServers"}];PROBE_SECRET_KEYS=["BAPI_API_KEY","BAPI_INVITE","BAPI_SIGNUP_EMAIL"];VENDOR_PROCESS_TIMEOUT_MS=8e3}});import path15 from"node:path";import{createHash as createHash2}from"node:crypto";function codexProfilePaths(home){return{profile:path15.join(home,PROFILE_FILE),state:path15.join(home,STATE_FILE),config:path15.join(home,"config.toml")}}function renderBridgeCodexProfile(key){return[MARKER,`# Applies only when selected with codex --profile ${BRIDGE_CODEX_PROFILE}.`,...BRIDGE_WORKFLOW_TOOLS.flatMap(tool=>[`[mcp_servers.${key}.tools.${tool}]`,'approval_mode = "auto"',""])].join(`
3642
3642
  `)+`
3643
- `}function fingerprint(value){return createHash2("sha256").update(value).digest("hex")}async function optionalRead(readFile22,filePath){try{return await readFile22(filePath)}catch(error){return error.code==="ENOENT"?null:void 0}}async function inspectBridgeCodexProfile(home,readFile22){let paths=codexProfilePaths(home),[config,profile,state]=await Promise.all([optionalRead(readFile22,paths.config),optionalRead(readFile22,paths.profile),optionalRead(readFile22,paths.state)]);if(config===void 0||profile===void 0||state===void 0)return{status:"unreadable"};let names=codexBridgeTableNames(config??"");if(names.length>1)return{status:"conflict"};let key=names[0]??MCP_SERVER_NAME;if(state===null)return{status:profile===null?"missing":"conflict",key};let parsed;try{parsed=JSON.parse(state)}catch{return{status:"conflict",key}}return!parsed||typeof parsed!="object"||!("enabled"in parsed)||typeof parsed.enabled!="boolean"?{status:"conflict",key}:parsed.enabled?profile===null||!("hash"in parsed)||typeof parsed.hash!="string"?{status:"conflict",key}:profile!==renderBridgeCodexProfile(key)||fingerprint(profile)!==parsed.hash?{status:"conflict",key}:{status:"active",key}:{status:"declined",key}}async function configureBridgeCodexProfile(home,choice,deps){let paths=codexProfilePaths(home),current=await inspectBridgeCodexProfile(home,deps.readFile);if(choice==="decline"){let oldState2=await optionalRead(deps.readFile,paths.state);if(oldState2===void 0)return{status:"unreadable"};let existingHash;if(oldState2!==null)try{let parsed=JSON.parse(oldState2);if(!parsed||typeof parsed.enabled!="boolean")return{status:"conflict"};typeof parsed.hash=="string"&&(existingHash=parsed.hash)}catch{return{status:"conflict"}}return await deps.mkdir(home,{recursive:!0}),await deps.writeFile(paths.state,JSON.stringify({enabled:!1,...existingHash?{hash:existingHash}:{}})+`
3643
+ `}function fingerprint(value){return createHash2("sha256").update(value).digest("hex")}async function optionalRead(readFile22,filePath){try{return await readFile22(filePath)}catch(error){return error.code==="ENOENT"?null:void 0}}async function inspectBridgeCodexProfile(home,readFile22){let paths=codexProfilePaths(home),[config,profile,state]=await Promise.all([optionalRead(readFile22,paths.config),optionalRead(readFile22,paths.profile),optionalRead(readFile22,paths.state)]);if(config===void 0||profile===void 0||state===void 0)return{status:"unreadable"};let names=codexBridgeTableNames(config??"");if(names.length>1)return{status:"conflict"};let key=names[0]??MCP_SERVER_NAME;if(state===null)return{status:profile===null?"missing":"conflict",key};let parsed;try{parsed=JSON.parse(state)}catch{return{status:"conflict",key}}return!parsed||typeof parsed!="object"||!("enabled"in parsed)||typeof parsed.enabled!="boolean"?{status:"conflict",key}:parsed.enabled?profile===null||!("hash"in parsed)||typeof parsed.hash!="string"?{status:"conflict",key}:fingerprint(profile)!==parsed.hash?{status:"conflict",key}:profile!==renderBridgeCodexProfile(key)?{status:profile.startsWith(MARKER)?"stale":"conflict",key}:{status:"active",key}:{status:"declined",key}}async function configureBridgeCodexProfile(home,choice,deps){let paths=codexProfilePaths(home),current=await inspectBridgeCodexProfile(home,deps.readFile);if(choice==="decline"){let oldState2=await optionalRead(deps.readFile,paths.state);if(oldState2===void 0)return{status:"unreadable"};let existingHash;if(oldState2!==null)try{let parsed=JSON.parse(oldState2);if(!parsed||typeof parsed.enabled!="boolean")return{status:"conflict"};typeof parsed.hash=="string"&&(existingHash=parsed.hash)}catch{return{status:"conflict"}}return await deps.mkdir(home,{recursive:!0}),await deps.writeFile(paths.state,JSON.stringify({enabled:!1,...existingHash?{hash:existingHash}:{}})+`
3644
3644
  `),{status:"declined",key:current.key}}if(current.status==="unreadable")return current;let config=await optionalRead(deps.readFile,paths.config);if(config===void 0)return{status:"unreadable"};let names=codexBridgeTableNames(config??"");if(names.length>1)return{status:"conflict"};let key=names[0]??MCP_SERVER_NAME,existing=await optionalRead(deps.readFile,paths.profile),oldState=await optionalRead(deps.readFile,paths.state),desired=renderBridgeCodexProfile(key);if(existing===void 0||oldState===void 0)return{status:"unreadable",key};if(oldState!==null)try{let parsed=JSON.parse(oldState);if(!parsed||typeof parsed.enabled!="boolean"||parsed.enabled&&(typeof parsed.hash!="string"||existing===null||parsed.hash!==fingerprint(existing)))return{status:"conflict",key}}catch{return{status:"conflict",key}}if(existing!==null&&existing!==desired){let managed=!1;if(oldState!==null&&existing.startsWith(MARKER))try{let parsed=JSON.parse(oldState);managed=typeof parsed?.enabled=="boolean"&&parsed?.hash===fingerprint(existing)}catch{}if(!managed)return{status:"conflict",key}}return await deps.mkdir(home,{recursive:!0}),existing!==desired&&await deps.writeFile(paths.profile,desired),await deps.writeFile(paths.state,JSON.stringify({enabled:!0,hash:fingerprint(desired)})+`
3645
- `),{status:"active",key}}var BRIDGE_CODEX_PROFILE,PROFILE_FILE,STATE_FILE,MARKER,BRIDGE_WORKFLOW_TOOLS,init_codex_bridge_profile=__esm({"src/codex-bridge-profile.ts"(){"use strict";init_mcp_host_config();init_mcp_identity();init_codex_home();BRIDGE_CODEX_PROFILE="bridge",PROFILE_FILE="bridge.config.toml",STATE_FILE="bridge.profile-state.json",MARKER="# Bridge-managed Codex approval profile (BAPI-1214)",BRIDGE_WORKFLOW_TOOLS=["ping","get_ticket","get_pipeline_recipe","request_ticket_review","wait_for_ticket_review","fresh_base","generate_decision_page","capture_review_decisions","request_plan_generation","get_plan","get_phase_context","record_phase_result","create_pull_request","update_jira_status","config_field","resolve_ci_checks","poll_ci_checks","wait_for_ci_checks"]}});async function resolveCodexLaunchScope(root,deps,codexConfigPath=codexProfilePaths(codexHome(deps.env??process.env,deps.homedir?.())).config){let normalized=normalizeWorktreePathForRegistration(root,deps);if(!normalized.ok)return{ok:!1,error:"Codex MCP scope unavailable: invalid repository path."};let manifest;try{let read=await readBridgeConfig(normalized.path,{readFile:deps.readFile});if(!read.ok)return{ok:!1,error:"Codex MCP scope unavailable: .bridge/config is missing or invalid."};manifest=read.manifest}catch{return{ok:!1,error:"Codex MCP scope unavailable: could not read .bridge/config."}}let built=buildMcpServerEntriesForManifest(manifest,normalized.path,deps.mcpServerInvocation,deps.bridgeApiBaseUrl);if(!built.entries[MCP_SERVER_NAME])return{ok:!1,error:"Codex MCP scope unavailable: .bridge/config has no Bridge target."};let existingConfig="";try{existingConfig=await deps.readFile(codexConfigPath)}catch(error){if(error.code!=="ENOENT")return{ok:!1,error:"Codex MCP scope unavailable: could not inspect Codex host config."}}let registeredNames=codexBridgeTableNames(existingConfig);if(registeredNames.length>1)return{ok:!1,error:"Codex MCP scope unavailable: duplicate Bridge registrations in Codex host config."};let bridgeKey=registeredNames[0]??MCP_SERVER_NAME,home=codexHome(deps.env??process.env,deps.homedir?.()),profile=await inspectBridgeCodexProfile(home,deps.readFile),argv=["-C",normalized.path];profile.status==="active"&&profile.key===bridgeKey&&argv.push("--profile",BRIDGE_CODEX_PROFILE);for(let[name,entry]of Object.entries(built.entries)){let key=name===MCP_SERVER_NAME?bridgeKey:name;argv.push("-c",`mcp_servers.${key}.command=${JSON.stringify(entry.command)}`),argv.push("-c",`mcp_servers.${key}.args=${JSON.stringify(entry.args)}`),argv.push("-c",`mcp_servers.${key}.enabled=true`)}return{ok:!0,argv}}var init_codex_launch_scope=__esm({"src/codex-launch-scope.ts"(){"use strict";init_bridge_config();init_mcp_host_config();init_codex_home();init_codex_bridge_profile();init_mcp_provisioning();init_mcp_identity()}});import path16 from"node:path";function asHookEntries(value){return Array.isArray(value)?value:[]}function entriesContainCommand(entries,command){return entries.some(entry=>Array.isArray(entry?.hooks)&&entry.hooks.some(h=>h&&h.type==="command"&&h.command===command))}function detectExistingPreToolUseMatcher(settings){let hooks=settings.hooks;if(hooks===null||typeof hooks!="object"||Array.isArray(hooks))return;let entries=asHookEntries(hooks.PreToolUse);for(let entry of entries)if(typeof entry?.matcher=="string")return entry.matcher}function mergeClaudeSettingsWithCommandHook(settings,command,events,options={}){let hooks={...settings.hooks!==null&&typeof settings.hooks=="object"&&!Array.isArray(settings.hooks)?settings.hooks:{}},allEvents=[...events];options.enablePreToolUse&&!allEvents.includes("PreToolUse")&&allEvents.push("PreToolUse");for(let event of allEvents){let entries=asHookEntries(hooks[event]);if(entriesContainCommand(entries,command)){hooks[event]=entries;continue}let newEntry={hooks:[{type:"command",command}]};event==="PreToolUse"&&(newEntry.matcher=options.preToolUseMatcher??detectExistingPreToolUseMatcher(settings)??DEFAULT_PRE_TOOL_USE_MATCHER),hooks[event]=[...entries,newEntry]}return{...settings,hooks}}async function provisionClaudeSettingsForWorktree(worktreePath,mergeSettings,deps){let claudeDir=path16.join(worktreePath,".claude"),settingsPath=path16.join(claudeDir,"settings.local.json"),existing={},raw=null;try{raw=await deps.readFile(settingsPath)}catch{raw=null}if(raw!==null)try{let parsed=JSON.parse(raw);if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json is not a JSON object"};existing=parsed}catch{return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json contains invalid JSON"}}let merged=mergeSettings(existing);try{await deps.mkdir(claudeDir,{recursive:!0}),await deps.writeFile(settingsPath,`${JSON.stringify(merged,null,2)}
3645
+ `),{status:"active",key}}var BRIDGE_CODEX_PROFILE,PROFILE_FILE,STATE_FILE,MARKER,BRIDGE_WORKFLOW_TOOLS,init_codex_bridge_profile=__esm({"src/codex-bridge-profile.ts"(){"use strict";init_mcp_host_config();init_mcp_identity();init_codex_home();BRIDGE_CODEX_PROFILE="bridge",PROFILE_FILE="bridge.config.toml",STATE_FILE="bridge.profile-state.json",MARKER="# Bridge-managed Codex approval profile (BAPI-1214)",BRIDGE_WORKFLOW_TOOLS=["ping","get_ticket","get_pipeline_recipe","request_ticket_review","wait_for_ticket_review","fresh_base","generate_decision_page","capture_review_decisions","request_plan_generation","get_plan","attachment","get_phase_context","record_phase_result","create_pull_request","update_jira_status","config_field","resolve_ci_checks","poll_ci_checks","wait_for_ci_checks"]}});async function resolveCodexLaunchScope(root,deps,codexConfigPath=codexProfilePaths(codexHome(deps.env??process.env,deps.homedir?.())).config){let normalized=normalizeWorktreePathForRegistration(root,deps);if(!normalized.ok)return{ok:!1,error:"Codex MCP scope unavailable: invalid repository path."};let manifest;try{let read=await readBridgeConfig(normalized.path,{readFile:deps.readFile});if(!read.ok)return{ok:!1,error:"Codex MCP scope unavailable: .bridge/config is missing or invalid."};manifest=read.manifest}catch{return{ok:!1,error:"Codex MCP scope unavailable: could not read .bridge/config."}}let built=buildMcpServerEntriesForManifest(manifest,normalized.path,deps.mcpServerInvocation,deps.bridgeApiBaseUrl);if(!built.entries[MCP_SERVER_NAME])return{ok:!1,error:"Codex MCP scope unavailable: .bridge/config has no Bridge target."};let existingConfig="";try{existingConfig=await deps.readFile(codexConfigPath)}catch(error){if(error.code!=="ENOENT")return{ok:!1,error:"Codex MCP scope unavailable: could not inspect Codex host config."}}let registeredNames=codexBridgeTableNames(existingConfig);if(registeredNames.length>1)return{ok:!1,error:"Codex MCP scope unavailable: duplicate Bridge registrations in Codex host config."};let bridgeKey=registeredNames[0]??MCP_SERVER_NAME,home=codexHome(deps.env??process.env,deps.homedir?.()),profile=await inspectBridgeCodexProfile(home,deps.readFile);if(profile.status==="stale"){try{profile=await configureBridgeCodexProfile(home,"enable",deps)}catch{return{ok:!1,error:"Codex MCP scope unavailable: could not refresh the managed Bridge approval profile."}}if(profile.status!=="active")return{ok:!1,error:"Codex MCP scope unavailable: managed Bridge approval profile refresh failed."}}let argv=["-C",normalized.path];profile.status==="active"&&profile.key===bridgeKey&&argv.push("--profile",BRIDGE_CODEX_PROFILE);for(let[name,entry]of Object.entries(built.entries)){let key=name===MCP_SERVER_NAME?bridgeKey:name;argv.push("-c",`mcp_servers.${key}.command=${JSON.stringify(entry.command)}`),argv.push("-c",`mcp_servers.${key}.args=${JSON.stringify(entry.args)}`),argv.push("-c",`mcp_servers.${key}.enabled=true`)}return{ok:!0,argv}}var init_codex_launch_scope=__esm({"src/codex-launch-scope.ts"(){"use strict";init_bridge_config();init_mcp_host_config();init_codex_home();init_codex_bridge_profile();init_mcp_provisioning();init_mcp_identity()}});import path16 from"node:path";function asHookEntries(value){return Array.isArray(value)?value:[]}function entriesContainCommand(entries,command){return entries.some(entry=>Array.isArray(entry?.hooks)&&entry.hooks.some(h=>h&&h.type==="command"&&h.command===command))}function detectExistingPreToolUseMatcher(settings){let hooks=settings.hooks;if(hooks===null||typeof hooks!="object"||Array.isArray(hooks))return;let entries=asHookEntries(hooks.PreToolUse);for(let entry of entries)if(typeof entry?.matcher=="string")return entry.matcher}function mergeClaudeSettingsWithCommandHook(settings,command,events,options={}){let hooks={...settings.hooks!==null&&typeof settings.hooks=="object"&&!Array.isArray(settings.hooks)?settings.hooks:{}},allEvents=[...events];options.enablePreToolUse&&!allEvents.includes("PreToolUse")&&allEvents.push("PreToolUse");for(let event of allEvents){let entries=asHookEntries(hooks[event]);if(entriesContainCommand(entries,command)){hooks[event]=entries;continue}let newEntry={hooks:[{type:"command",command}]};event==="PreToolUse"&&(newEntry.matcher=options.preToolUseMatcher??detectExistingPreToolUseMatcher(settings)??DEFAULT_PRE_TOOL_USE_MATCHER),hooks[event]=[...entries,newEntry]}return{...settings,hooks}}async function provisionClaudeSettingsForWorktree(worktreePath,mergeSettings,deps){let claudeDir=path16.join(worktreePath,".claude"),settingsPath=path16.join(claudeDir,"settings.local.json"),existing={},raw=null;try{raw=await deps.readFile(settingsPath)}catch{raw=null}if(raw!==null)try{let parsed=JSON.parse(raw);if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json is not a JSON object"};existing=parsed}catch{return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json contains invalid JSON"}}let merged=mergeSettings(existing);try{await deps.mkdir(claudeDir,{recursive:!0}),await deps.writeFile(settingsPath,`${JSON.stringify(merged,null,2)}
3646
3646
  `)}catch{return{ok:!1,reason:"io",error:"failed to write .claude/settings.local.json"}}return{ok:!0}}var DEFAULT_PRE_TOOL_USE_MATCHER,init_claude_settings=__esm({"src/claude-settings.ts"(){"use strict";DEFAULT_PRE_TOOL_USE_MATCHER="*"}});import{spawnSync}from"node:child_process";function normalizeRepoRelativePath(input){if(typeof input!="string")return null;let trimmed=input.trim();if(trimmed.length===0||trimmed.startsWith("/")||trimmed.startsWith("\\")||/^[A-Za-z]:[\\/]/.test(trimmed))return null;let p=trimmed.replace(/\\/g,"/");p.startsWith("./")&&(p=p.slice(2));let segments=p.split("/");if(segments.some(s=>s===".."))return null;let cleaned=segments.filter(s=>s!==""&&s!==".").join("/");return cleaned.length>0?cleaned:null}function normalizeDeclaredTouchedFiles(list){if(!Array.isArray(list))return[];let out=new Set;for(let item of list){let norm=normalizeRepoRelativePath(item);norm&&out.add(norm)}return Array.from(out).sort()}function parseDeclaredTouchedFilesFromEnv(env=process.env){let raw=env[DECLARED_TOUCHED_FILES_ENV];if(typeof raw!="string"||raw.trim().length===0)return{specified:!1};let parsed;try{parsed=JSON.parse(raw)}catch{return{specified:!1}}if(!Array.isArray(parsed))return{specified:!1};let files=normalizeDeclaredTouchedFiles(parsed);return files.length===0?{specified:!1}:{specified:!0,files}}function collectBranchChangedFiles(opts={}){let baseRef=opts.baseRef??FILE_SCOPE_GUARD_BASE_REF,spawn11=opts.spawnSyncFn??defaultSpawnSync,result;try{result=spawn11("git",["diff","--name-only",`${baseRef}...HEAD`],{cwd:opts.cwd,encoding:"utf-8",shell:!1})}catch{return{ok:!1,files:[]}}if(result.error||result.status!==0)return{ok:!1,files:[]};let stdout=typeof result.stdout=="string"?result.stdout:result.stdout?.toString("utf-8")??"",files=[],seen=new Set;for(let line of stdout.split(`
3647
3647
  `)){let norm=normalizeRepoRelativePath(line);norm&&!seen.has(norm)&&(seen.add(norm),files.push(norm))}return{ok:!0,files}}function analyzeDiffScope(input){if(!input.declared.specified)return{checked:!1,outOfScopeFiles:[],warning:null};let declaredSet=new Set(input.declared.files),outOfScope=input.changedFiles.filter(f=>!declaredSet.has(f)).sort();if(outOfScope.length===0)return{checked:!0,outOfScopeFiles:[],warning:null};let warning=`[file-scope-guard] ${input.ticketKey&&input.ticketKey.trim().length>0?input.ticketKey.trim():"unknown-ticket"}: ${outOfScope.length} file(s) changed outside the declared touched-file set (${input.declared.files.length} declared): ${outOfScope.join(", ")}. Warn-only \u2014 PR creation continues.`;return{checked:!0,outOfScopeFiles:outOfScope,warning}}function runFileScopeGuardCli(deps={}){let env=deps.env??process.env,writeOut=deps.writeOut??(m=>process.stdout.write(`${m}
3648
3648
  `)),writeErr=deps.writeErr??(m=>process.stderr.write(`${m}
@@ -3742,19 +3742,19 @@ ${failures.map(f=>` ${f.relPath}: ${f.reason}`).join(`
3742
3742
  WHERE run_id = @run_id AND worker_id = @worker_id AND state = 'pending'
3743
3743
  AND julianday(available_at) <= julianday('now')
3744
3744
  ORDER BY seq ASC
3745
- LIMIT @limit`),toDelivered=db.prepare("UPDATE messages SET state = 'delivered', updated_at = datetime('now') WHERE seq = @seq AND state = 'pending'"),toAcked=db.prepare("UPDATE messages SET state = 'acked', acked_at = datetime('now'), updated_at = datetime('now') WHERE seq = @seq AND state = 'delivered'"),reread=db.prepare("SELECT * FROM messages WHERE seq = ?"),delivered=[];return db.transaction(()=>{let pending=selectPending.all({run_id:runId,worker_id:workerId,limit});for(let row2 of pending){if(toDelivered.run({seq:row2.seq}).changes!==1)continue;insertRelayAuditEvent(db,{id:`message.delivered:${row2.id}`,source:"conductor-worker",type:"message.delivered",run_id:row2.run_id,worker_id:row2.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message delivered to worker",status:"delivered",details:{message_id:row2.id,message_type:row2.type}}}),toAcked.run({seq:row2.seq}),insertRelayAuditEvent(db,{id:`message.acked:${row2.id}`,source:"conductor-worker",type:"message.acked",run_id:row2.run_id,worker_id:row2.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message acknowledged by worker",status:"acked",details:{message_id:row2.id,message_type:row2.type}}});let finalRow=reread.get(row2.seq);delivered.push(rowToConductorWorkerMessage(finalRow))}}).immediate(),{messages:delivered,count:delivered.length,acked_count:delivered.length}}finally{db.close()}}var ConductorPersistenceUnavailableError,LEDGER_NATIVE_MODULE_NAME,ConductorNativeModuleLoadError,databaseModulePromise,dbLoadFailure,dbLoadDiagnosticEmitted,BUSY_TIMEOUT_DEFAULT,BUSY_TIMEOUT_MIN,BUSY_TIMEOUT_MAX,RETENTION_DAYS_DEFAULT,RETENTION_DAYS_MAX,RETENTION_MAX_ROWS_DEFAULT,RETENTION_MAX_ROWS_MIN,RETENTION_MAX_ROWS_MAX,POLL_LIMIT_DEFAULT,POLL_LIMIT_MAX,MESSAGE_COOLDOWN_DEFAULT_MS,MESSAGE_COOLDOWN_MIN_MS,MESSAGE_COOLDOWN_MAX_MS,CHECK_MESSAGES_LIMIT_DEFAULT,CHECK_MESSAGES_LIMIT_MAX,WAIT_TIMEOUT_MAX_MS,WAIT_POLL_INTERVAL_MS,SUMMARY_FIELD_MAX_CHARS,CURRENT_CONDUCTOR_SCHEMA_VERSION,MESSAGE_TYPE_PATTERN,init_store=__esm({"src/conductor/store.ts"(){"use strict";init_taxonomy();init_errors();init_data_normalization();init_paths();ConductorPersistenceUnavailableError=class extends Error{constructor(message="Conductor persistence is unavailable: the optional 'better-sqlite3' native module could not be loaded."){super(message),this.name="ConductorPersistenceUnavailableError"}},LEDGER_NATIVE_MODULE_NAME="better-sqlite3",ConductorNativeModuleLoadError=class extends ConductorPersistenceUnavailableError{details;failureKind;constructor(failureKind,details){super("Conductor ledger native module failed to load for this Node runtime."),this.name="ConductorNativeModuleLoadError",this.failureKind=failureKind,this.details=details}};databaseModulePromise=null,dbLoadFailure=null,dbLoadDiagnosticEmitted=!1;BUSY_TIMEOUT_DEFAULT=1e4,BUSY_TIMEOUT_MIN=250,BUSY_TIMEOUT_MAX=12e4,RETENTION_DAYS_DEFAULT=30,RETENTION_DAYS_MAX=3650,RETENTION_MAX_ROWS_DEFAULT=5e4,RETENTION_MAX_ROWS_MIN=100,RETENTION_MAX_ROWS_MAX=1e7,POLL_LIMIT_DEFAULT=100,POLL_LIMIT_MAX=1e3,MESSAGE_COOLDOWN_DEFAULT_MS=3e5,MESSAGE_COOLDOWN_MIN_MS=1e3,MESSAGE_COOLDOWN_MAX_MS=864e5,CHECK_MESSAGES_LIMIT_DEFAULT=10,CHECK_MESSAGES_LIMIT_MAX=100,WAIT_TIMEOUT_MAX_MS=12e4,WAIT_POLL_INTERVAL_MS=500,SUMMARY_FIELD_MAX_CHARS=500;CURRENT_CONDUCTOR_SCHEMA_VERSION=8;MESSAGE_TYPE_PATTERN=/^[A-Za-z0-9._:-]{1,100}$/}});import{randomBytes}from"node:crypto";import{fileURLToPath}from"node:url";function randomCorrelationFragment(){return randomBytes(4).toString("hex")}function sanitizeIdSegment(value){return value.replace(/[^A-Za-z0-9_-]/g,"-")}function mintStartTicketsRunId(keys,fragment=randomCorrelationFragment()){return`${keys.length>0?keys[0]:"start-tickets"}-start-tickets-${fragment}`}function mintStartTicketsWorkerId(ticketKey,agentName,fragment=randomCorrelationFragment()){return`${ticketKey}-${sanitizeIdSegment(agentName)}-${fragment}`}function buildEpicIdentityEnv(epic){let env={BAPI_CONDUCTOR_EPIC_KEY:epic.epic_key,BAPI_CONDUCTOR_EPIC_RUN_ID:epic.epic_run_id,BAPI_CONDUCTOR_PLAN_VERSION:String(epic.plan_version)},declared=normalizeDeclaredTouchedFiles(epic.declared_touched_files);return declared.length>0&&(env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON=JSON.stringify(declared)),env}function defaultResolveBinPath(filename){return fileURLToPath(new URL(`./${filename}`,import.meta.url))}function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}async function createStartTicketsConductorContext(options,agent,deps){let resolveRepoName2=deps.resolveRepoName??resolveStartTicketsRepoName,repoName=null;try{repoName=await resolveRepoName2({env:deps.env,cwd:deps.cwd,readFile:deps.readFile})}catch{repoName=null}let gateName=nonEmpty(deps.env.BAPI_CONDUCTOR_GATE_NAME)?deps.env.BAPI_CONDUCTOR_GATE_NAME.trim():DEFAULT_CONDUCTOR_GATE_NAME,supervisorMode=nonEmpty(deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE)?deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE.trim():options.autoApprove?"auto":"interactive",resolveBinPath=deps.resolveBinPath??defaultResolveBinPath,context={runId:mintStartTicketsRunId(options.keys,deps.fragment),repoName,gateName,supervisorMode,agentName:agent.name,cliFile:resolveBinPath("epic-implementer-bin.js"),conductorNodePath:deps.execPath??process.execPath,hookBinPath:resolveBinPath("epic-implementer-claude-hook-bin.js")};return options.epic&&(context.epic=options.epic),context}function isConductorFlagEnabled(value){if(typeof value!="string")return!1;let v=value.trim().toLowerCase();return v==="1"||v==="true"}function buildConductorWorkerEnv(context,worker,parentEnv){let parentActiveGroups=new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));parentActiveGroups.add("conductor");let env={BAPI_CONDUCTOR_ENABLED:"1",BRIDGE_MCP_PROFILE:Array.from(parentActiveGroups).join(","),BAPI_CONDUCTOR_RUN_ID:context.runId,BAPI_CONDUCTOR_WORKER_ID:worker.workerId,BAPI_CONDUCTOR_TICKET_KEY:worker.ticketKey,BAPI_CONDUCTOR_WORKTREE_PATH:worker.worktreePath,BAPI_CONDUCTOR_GATE_NAME:context.gateName,BAPI_CONDUCTOR_SUPERVISOR_MODE:context.supervisorMode,BAPI_CONDUCTOR_CLI_FILE:context.cliFile,CONDUCTOR_NODE_PATH:context.conductorNodePath};if(context.repoName&&(env.BAPI_CONDUCTOR_REPO_NAME=context.repoName),context.epic){let epicEnv=buildEpicIdentityEnv(context.epic);for(let[k,v]of Object.entries(epicEnv))env[k]=v}isConductorFlagEnabled(parentEnv.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE)&&(env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE="1");for(let key of CONDUCTOR_TUNING_ENV_KEYS)nonEmpty(parentEnv[key])&&(env[key]=parentEnv[key].trim());return env}function shellQuotePath(value){return`'${value.replace(/'/g,"'\\''")}'`}function resolveConductorHookCommand(env,hookBinPath,execPath=process.execPath){return nonEmpty(env.BAPI_CONDUCTOR_HOOK_COMMAND)?env.BAPI_CONDUCTOR_HOOK_COMMAND:`${shellQuotePath(execPath)} ${shellQuotePath(hookBinPath)}`}function mergeClaudeSettingsWithConductorHook(settings,command,options={}){return mergeClaudeSettingsWithCommandHook(settings,command,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,options)}async function provisionConductorHookForWorktree(worktreePath,command,options,deps){return provisionClaudeSettingsForWorktree(worktreePath,existing=>mergeClaudeSettingsWithConductorHook(existing,command,{enablePreToolUse:options.enablePreToolUse,preToolUseMatcher:options.preToolUseMatcher??detectExistingPreToolUseMatcher(existing)}),deps)}async function provisionConductorHooksForRows(rows,context,deps){let isClaude=context.agentName==="claude",enablePreToolUse=isConductorFlagEnabled(deps.env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE),command=resolveConductorHookCommand(deps.env,context.hookBinPath,deps.execPath),fragment=deps.workerFragment,out=[];for(let row2 of rows){let base={...row2,runId:context.runId};if(row2.status!=="created"||!row2.path){out.push(base);continue}if(!isClaude){out.push(base);continue}let workerId=mintStartTicketsWorkerId(row2.key,context.agentName,fragment?fragment():randomCorrelationFragment()),conductorEnv=buildConductorWorkerEnv(context,{workerId,ticketKey:row2.key,worktreePath:row2.path},deps.env),result=await provisionConductorHookForWorktree(row2.path,command,{enablePreToolUse},deps);if(!result.ok){out.push({...base,workerId,warnings:[...base.warnings??[],`conductor hook not injected: ${result.error}`]});continue}out.push({...base,workerId,conductorEnv,conductorHookInjected:!0})}return out}function buildStartTicketsRunStartedEventInput(context,rows,options){let worktreeRows=rows.filter(r=>typeof r.path=="string"&&r.path.length>0),workers=worktreeRows.map(r=>({ticket_key:r.key,worker_id:r.workerId??null,worktree_path:r.path??null,status:r.status})),subject=context.repoName??(options.keys.length>0?options.keys[0]:"start-tickets");return{source:"start-tickets",type:"run.started",run_id:context.runId,producer:"bridge-api-mcp-server",observed_via:"start-tickets",subject,data:{summary:"start-tickets run started",status:"started",details:{repo:context.repoName,requested_ticket_keys:options.keys,ticket_keys:worktreeRows.map(r=>r.key),worktree_paths:worktreeRows.map(r=>r.path),workers,gate_name:context.gateName,supervisor_mode:context.supervisorMode,dry_run:options.dryRun,agent:context.agentName,...context.epic?{epic_key:context.epic.epic_key,epic_run_id:context.epic.epic_run_id,plan_version:context.epic.plan_version}:{}}}}}async function emitStartTicketsRunStarted(context,rows,options,deps={}){let event=buildStartTicketsRunStartedEventInput(context,rows,options);try{if(deps.emit)deps.emit(event);else{let{emitConductorEvent:emitConductorEvent2}=await Promise.resolve().then(()=>(init_store(),store_exports));emitConductorEvent2(event)}return rows}catch{if(rows.length===0)return rows;let[first,...rest]=rows;return[{...first,warnings:[...first.warnings??[],CONDUCTOR_RUN_START_EMIT_FAILED_WARNING]},...rest]}}function posixSingleQuote(value){return`'${value.replace(/'/g,"'\\''")}'`}function powershellSingleQuote(value){return`'${value.replace(/'/g,"''")}'`}function injectConductorEnvIntoShellCommand(platform,shellCommand,env){if(!env)return shellCommand;let entries=Object.entries(env).filter(([key])=>ENV_KEY_PATTERN.test(key));if(entries.length===0)return shellCommand;let isWindows=platform==="win32";return`${entries.map(([key,value])=>isWindows?`$env:${key}=${powershellSingleQuote(value)};`:`export ${key}=${posixSingleQuote(value)};`).join(" ")} ${shellCommand}`}function buildSupervisorTabCommand(context,platform,nodeExecPath=process.execPath){let quote=platform==="win32"?powershellSingleQuote:posixSingleQuote;return`${quote(nodeExecPath)} ${quote(context.cliFile)} supervise --run-id ${quote(context.runId)}`}function isSupervisorLaunchEnabled(context){return context.supervisorMode.trim().toLowerCase()!=="off"}function supervisorSpawnKey(keys){return`${keys.length>0?keys[0]:"start-tickets"}-${SUPERVISOR_SPAWN_KEY_SUFFIX}`}var DEFAULT_CONDUCTOR_GATE_NAME,CONDUCTOR_TUNING_ENV_KEYS,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,CONDUCTOR_RUN_START_EMIT_FAILED_WARNING,ENV_KEY_PATTERN,SUPERVISOR_SPAWN_KEY_SUFFIX,init_start_tickets_conductor=__esm({"src/start-tickets-conductor.ts"(){"use strict";init_claude_settings();init_file_scope_guard();init_mcp_profile();init_start_tickets_repo();DEFAULT_CONDUCTOR_GATE_NAME="implement-ticket";CONDUCTOR_TUNING_ENV_KEYS=["BAPI_CONDUCTOR_BUSY_TIMEOUT_MS","BAPI_CONDUCTOR_RETENTION_DAYS","BAPI_CONDUCTOR_RETENTION_MAX_ROWS","BAPI_CONDUCTOR_MESSAGE_COOLDOWN_MS"];CONDUCTOR_HOOK_LIFECYCLE_EVENTS=["SessionStart","SessionEnd","Notification"];CONDUCTOR_RUN_START_EMIT_FAILED_WARNING="conductor run-start emit failed (continuing without run-level event)";ENV_KEY_PATTERN=/^[A-Z_][A-Z0-9_]*$/;SUPERVISOR_SPAWN_KEY_SUFFIX="supervisor"}});import{createHash as createHash3}from"node:crypto";function normalizeRepoName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function normalizeSha(value){if(typeof value!="string")return null;let lowered=value.trim().toLowerCase();return SHA_RE.test(lowered)?lowered:null}function normalizePrNumber(value){return typeof value!="number"||!Number.isSafeInteger(value)||value<=0?null:value}function normalizeCheckName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function canonicalize(value){if(Array.isArray(value))return value.map(item=>canonicalize(item));if(value!==null&&typeof value=="object"){let record=value,sortedKeys=Object.keys(record).sort(),out={};for(let key of sortedKeys)out[key]=canonicalize(record[key]);return out}return value}function stableJsonHash(value){let canonical=canonicalize(value),json=JSON.stringify(canonical)??"null";return createHash3("sha256").update(json).digest("hex")}var GIT_CI_PRODUCER,GIT_HOOK_PRODUCER,REQUIRED_CI_CHECKS_GREEN,REVIEW_STATE,DEFAULT_GATE_NAME,CONTROL_CHAR_RE,SHA_RE,VERDICTLESS_DISPOSITION_PARK,VERDICTLESS_DISPOSITION_FAIL_OPEN,VERDICTLESS_DISPOSITIONS,MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON,init_git_ci_types=__esm({"src/conductor/git-ci-types.ts"(){"use strict";GIT_CI_PRODUCER="git-pr-ci-producer",GIT_HOOK_PRODUCER="git-hook",REQUIRED_CI_CHECKS_GREEN="required_ci_checks_green",REVIEW_STATE="review_state",DEFAULT_GATE_NAME="done",CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/,SHA_RE=/^[0-9a-f]{40}$|^[0-9a-f]{64}$/;VERDICTLESS_DISPOSITION_PARK="park",VERDICTLESS_DISPOSITION_FAIL_OPEN="fail_open",VERDICTLESS_DISPOSITIONS=[VERDICTLESS_DISPOSITION_PARK,VERDICTLESS_DISPOSITION_FAIL_OPEN],MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON="review_waived_verdictless_fail_open"}});function nonblank(candidate){if(typeof candidate!="string")return;let trimmed=candidate.trim();return trimmed.length>0?trimmed:void 0}function resolveDeclaredRunBaseBranch(policyJson){if(policyJson===null||typeof policyJson!="object"||Array.isArray(policyJson))return;let policy=policyJson;for(let key of POLICY_BASE_BRANCH_KEYS){if(!Object.prototype.hasOwnProperty.call(policy,key))continue;let resolved=nonblank(policy[key]);if(resolved!==void 0)return resolved}}var POLICY_BASE_BRANCH_KEYS,init_run_branch=__esm({"src/conductor/run-branch.ts"(){"use strict";POLICY_BASE_BRANCH_KEYS=["base_branch","baseBranch"]}});var bridge_api_client_exports={};__export(bridge_api_client_exports,{CONDUCTOR_DEFAULT_BASE_URL:()=>CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS:()=>CONDUCTOR_FETCH_TIMEOUT_MS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES:()=>CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,ConductorBridgeApiError:()=>ConductorBridgeApiError,EPIC_RUN_STATUS_VALUES:()=>EPIC_RUN_STATUS_VALUES,READINESS_FRESH_RECONCILER_COUNT_CAP:()=>READINESS_FRESH_RECONCILER_COUNT_CAP,adoptCurrentHeadAndUnparkTicket:()=>adoptCurrentHeadAndUnparkTicket,advanceEpicTicketStatus:()=>advanceEpicTicketStatus,approveEpicPlan:()=>approveEpicPlan,bootstrapConductorSupervisorDefaults:()=>bootstrapConductorSupervisorDefaults,buildConductorJiraUrl:()=>buildConductorJiraUrl,buildConductorVcsUrl:()=>buildConductorVcsUrl,buildEpicDispatchKey:()=>buildEpicDispatchKey,claimEpicSupervisionLease:()=>claimEpicSupervisionLease,completeMergeChild:()=>completeMergeChild,createEpicRun:()=>createEpicRun,createEpicRunWithDisposition:()=>createEpicRunWithDisposition,createEpicTicketStatus:()=>createEpicTicketStatus,deletePullRequestBranch:()=>deletePullRequestBranch,dispatchRemediation:()=>dispatchRemediation,extractSanitizedErrorDiagnostics:()=>extractSanitizedErrorDiagnostics,fetchActiveEpicRuns:()=>fetchActiveEpicRuns,fetchConductorConfigField:()=>fetchConductorConfigField,fetchConductorJsonPatchWithTimeout:()=>fetchConductorJsonPatchWithTimeout,fetchConductorJsonPostWithTimeout:()=>fetchConductorJsonPostWithTimeout,fetchConductorJsonPutWithTimeout:()=>fetchConductorJsonPutWithTimeout,fetchConductorJsonWithTimeout:()=>fetchConductorJsonWithTimeout,fetchConductorReadiness:()=>fetchConductorReadiness,fetchEffectiveSupervisorConfig:()=>fetchEffectiveSupervisorConfig,fetchEffectiveSupervisorSetup:()=>fetchEffectiveSupervisorSetup,fetchEpicRunState:()=>fetchEpicRunState,fetchExplainRun:()=>fetchExplainRun,fetchParseStatus:()=>fetchParseStatus,fetchPrReviewStatus:()=>fetchPrReviewStatus,fetchShadowDispatchFreshness:()=>fetchShadowDispatchFreshness,getEpicPlan:()=>getEpicPlan,mergeChild:()=>mergeChild,mergePullRequestForGate:()=>mergePullRequestForGate,parseConductorReadinessResponse:()=>parseConductorReadinessResponse,parseConductorSupervisorBootstrapResponse:()=>parseConductorSupervisorBootstrapResponse,pollCiChecksForCommit:()=>pollCiChecksForCommit,readEpicRunCompletionState:()=>readEpicRunCompletionState,reconcileShadowMerge:()=>reconcileShadowMerge,recordEpicDispatch:()=>recordEpicDispatch,remediateEpicTicket:()=>remediateEpicTicket,replaceEpicRunPolicy:()=>replaceEpicRunPolicy,resolveConductorBridgeApiAccess:()=>resolveConductorBridgeApiAccess,safeDiagnosticMessage:()=>safeDiagnosticMessage,scopeReparse:()=>scopeReparse,stopEpicRun:()=>stopEpicRun,storeEpicPlan:()=>storeEpicPlan,transitionEpicDispatch:()=>transitionEpicDispatch,transitionJiraStatus:()=>transitionJiraStatus,triggerRepositoryParse:()=>triggerRepositoryParse,unparkEpicTicket:()=>unparkEpicTicket,updateApprovedPlanNodeTicketSpec:()=>updateApprovedPlanNodeTicketSpec,updateEpicRunStatus:()=>updateEpicRunStatus,validateEpicPlan:()=>validateEpicPlan});import os4 from"node:os";import{readFile as readFile4,stat as stat2}from"node:fs/promises";async function resolveConductorBridgeApiAccess(deps={}){let env=deps.env??process.env,cwd=deps.cwd??process.cwd(),homedir=deps.homedir??os4.homedir,platform=deps.platform??process.platform,readFileImpl=deps.readFile??(p=>readFile4(p,"utf-8")),statImpl=deps.stat??(p=>stat2(p)),repoName=deps.repoName?.trim()||await resolveStartTicketsRepoName({env,cwd,readFile:readFileImpl});if(!repoName)return{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"};let credResult;try{credResult=await resolveBapiCredentials(repoName,{env,homedir,platform,readFile:readFileImpl,stat:statImpl})}catch{return{ok:!1,kind:"credentials-unavailable",error:"failed to resolve Bridge API credentials"}}if(!credResult.ok)return{ok:!1,kind:"credentials-unavailable",error:"Bridge API credentials unavailable"};let baseUrlRaw=env.BAPI_BASE_URL,baseUrl=typeof baseUrlRaw=="string"&&baseUrlRaw.trim().length>0?baseUrlRaw.trim():CONDUCTOR_DEFAULT_BASE_URL;return{ok:!0,access:{repoName,apiKey:credResult.credentials.apiKey,baseUrl}}}function buildConductorJiraUrl(baseUrl,apiPath,params={}){let trimmed=baseUrl.replace(/\/+$/,""),url=new URL(`${trimmed}/jira${apiPath}`);for(let[k,v]of Object.entries(params))url.searchParams.set(k,v);return url.toString()}function redactErrorPreview(text3){return text3.replace(/sk-[A-Za-z0-9_-]{8,}/g,"[REDACTED]").replace(/(Bearer|X-API-Key|api[_-]?key)\b\s*[:=]?\s*\S+/gi,"$1 [REDACTED]").replace(/(webhook[_-]?url)\b["']?\s*[:=]?\s*["']?https?:\/\/\S+/gi,"$1 [REDACTED]").replace(/https?:\/\/[^\s/@]+:[^\s/@]+@\S+/gi,"[REDACTED]")}function stripWebhookUrlsDeep(value,depth=0){if(depth>12)return;if(Array.isArray(value))return value.map(item=>stripWebhookUrlsDeep(item,depth+1));if(!value||typeof value!="object")return value;let out={};for(let[key,item]of Object.entries(value))key!=="webhook_url"&&(out[key]=stripWebhookUrlsDeep(item,depth+1));return out}function boundedErrorPreview(text3){let redacted=redactErrorPreview(text3).replace(/\s+/g," ").trim();return redacted.length>CONDUCTOR_ERROR_PREVIEW_MAX?`${redacted.slice(0,CONDUCTOR_ERROR_PREVIEW_MAX)}\u2026`:redacted}function formatValidationDetailItem(item){if(!item||typeof item!="object")return;let record=item,msg=record.msg;if(typeof msg!="string"||!msg.trim())return;let loc=record.loc,parts=Array.isArray(loc)?loc.filter(part=>typeof part=="string"||typeof part=="number"):[],path59=(parts[0]==="body"?parts.slice(1):parts).slice(0,CONDUCTOR_VALIDATION_LOC_MAX_PARTS).join(".");return path59?`${path59}: ${msg}`:msg}function extractSanitizedErrorDiagnostics(body){if(typeof body=="string"){let trimmed=body.trim();return trimmed?{bodyPreview:boundedErrorPreview(trimmed)}:{}}if(!body||typeof body!="object")return{};let record=body,detail=record.detail,errorCode5,message;if(Array.isArray(detail))message=formatValidationDetailItem(detail[0]);else if(detail&&typeof detail=="object"){let d=detail;typeof d.error_code=="string"&&(errorCode5=d.error_code),typeof d.message=="string"&&(message=d.message)}else typeof detail=="string"&&(message=detail);!errorCode5&&typeof record.error_code=="string"&&(errorCode5=record.error_code),!message&&typeof record.message=="string"&&(message=record.message);let diagnostics={};errorCode5&&(diagnostics.errorCode=boundedErrorPreview(errorCode5)),message&&(diagnostics.bodyPreview=boundedErrorPreview(message));let rawCurrentRowVersion=detail&&typeof detail=="object"&&!Array.isArray(detail)?detail.current_row_version:record.current_row_version;return typeof rawCurrentRowVersion=="number"&&Number.isSafeInteger(rawCurrentRowVersion)&&rawCurrentRowVersion>=0&&(diagnostics.currentRowVersion=rawCurrentRowVersion),diagnostics}function redactDiagnosticValues(diagnostics,secrets){let scrub=text3=>{let out2=text3;for(let secret of secrets)secret&&secret.length>=4&&(out2=out2.split(secret).join("[REDACTED]"));return out2},out={};return diagnostics.errorCode&&(out.errorCode=scrub(diagnostics.errorCode)),diagnostics.bodyPreview&&(out.bodyPreview=scrub(diagnostics.bodyPreview)),typeof diagnostics.currentRowVersion=="number"&&(out.currentRowVersion=diagnostics.currentRowVersion),out}async function readSanitizedErrorDiagnostics(resp,headers={},options={}){try{let parsed=stripWebhookUrlsDeep(await resp.json()),diagnostics=extractSanitizedErrorDiagnostics(parsed);if(options.readErrorField&&!diagnostics.bodyPreview){let detail=parsed&&typeof parsed=="object"&&!Array.isArray(parsed)?parsed.detail:void 0,error=detail&&typeof detail=="object"&&!Array.isArray(detail)?detail.error:void 0;typeof error=="string"&&error.trim()&&(diagnostics.bodyPreview=boundedErrorPreview(error.trim()))}let secrets=Object.entries(headers).filter(([k])=>/key|authorization|token/i.test(k)).map(([,v])=>v);return redactDiagnosticValues(diagnostics,secrets)}catch{return{}}}function safeDiagnosticMessage(err,fallback){if(err instanceof ConductorBridgeApiError){let parts=[`kind=${err.kind}`];return typeof err.status=="number"&&parts.push(`status=${err.status}`),err.errorCode&&parts.push(`code=${err.errorCode}`),err.bodyPreview&&parts.push(err.bodyPreview),parts.join(" ")}return err instanceof Error?err.constructor.name:fallback}function conductorGetHeaders(access2){return{"X-API-Key":access2.apiKey}}async function fetchConductorJsonWithTimeout(url,headers,timeoutMs,fetchImpl=globalThis.fetch){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{headers,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorConfigField(access2,fieldName,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`/config-field/${encodeURIComponent(fieldName)}`,{repo_name:access2.repoName}),body=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(body&&typeof body=="object"&&"value"in body)return body.value}async function fetchEffectiveSupervisorSetup(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-setup/`:`${EPIC_RUNS_API_PREFIX}/supervisor-setup/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchEffectiveSupervisorConfig(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-config/`:`${EPIC_RUNS_API_PREFIX}/supervisor-config/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function pollCiChecksForCommit(access2,commitRef,fetchImpl=globalThis.fetch){let sha=normalizeSha(commitRef);if(sha===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,"/poll-ci-checks",{repo_name:access2.repoName,commit_ref:sha});return fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchPrReviewStatus(access2,prNumber,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/reviews/status`),fullUrl=new URL(url);return fullUrl.searchParams.set("repo_name",access2.repoName),fetchConductorJsonWithTimeout(fullUrl.toString(),conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function buildConductorVcsUrl(baseUrl,apiPath){let trimmed=baseUrl.replace(/\/+$/,""),path59=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path59}`).toString()}function conductorPostHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}async function fetchConductorJsonWithMethodAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions={}){let{body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions);return parsed}async function fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions={}){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{method,headers,body,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers,errorOptions);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return{status:resp.status,body:await resp.json()}}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorJsonPostWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("POST",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPatchWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PATCH",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPutWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PUT",url,headers,body,timeoutMs,fetchImpl)}async function mergePullRequestForGate(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number),sha=normalizeSha(request.expected_head_sha);if(pr===null||sha===null||!request.action_key||!request.repo_name)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/merge`),body=JSON.stringify({repo_name:request.repo_name,expected_head_sha:sha,gate:request.gate,action_key:request.action_key,...request.gate_event?{gate_event:request.gate_event}:{}});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function remediateEpicTicket(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number);if(pr===null||!request.epic_run_id||!request.ticket_key||!request.head_sha||!request.idempotency_key)throw new ConductorBridgeApiError("invalid-input");if(requireNonNegativeSafeInteger(request.expected_row_version),request.attempt_kind!=="nudge"&&request.attempt_kind!=="redispatch")throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/remediate`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epic_run_id,ticket_key:request.ticket_key,expected_row_version:request.expected_row_version,head_sha:request.head_sha,idempotency_key:request.idempotency_key,attempt_kind:request.attempt_kind,...request.reason?{reason:request.reason}:{}});try{return{ok:!0,conflict:!1,response:await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===409)return{ok:!0,conflict:!0};throw err}}function requireNonEmptyString(value){if(typeof value!="string"||value.trim().length===0)throw new ConductorBridgeApiError("invalid-input")}function requirePositiveSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<=0)throw new ConductorBridgeApiError("invalid-input")}function requireNonNegativeSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<0)throw new ConductorBridgeApiError("invalid-input")}function requireNoSlashPathSegment(value){if(value.includes("/"))throw new ConductorBridgeApiError("invalid-input")}function requireEpicTicketStatusValue(value){if(typeof value!="string"||!EPIC_TICKET_STATUS_VALUES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function requireEpicDispatchTransitionStatus(value){if(typeof value!="string"||!EPIC_DISPATCH_TRANSITION_STATUSES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function epicRunApiPath(epicKey){return`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}`}function epicDispatchTransitionApiPath(dispatchKey,nextStatus){return nextStatus==="run_spawned"?`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/run-spawned`:`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/terminal`}function buildEpicDispatchKey(epicKey,ticketKey,planVersion,attempt=0){requireNonEmptyString(epicKey),requireNonEmptyString(ticketKey),requireNonNegativeSafeInteger(planVersion),requireNonNegativeSafeInteger(attempt);let base=`dispatch:${epicKey}:${ticketKey}:${planVersion}`;return attempt>0?`${base}:r${attempt}`:base}function parseEpicSupervisionLeaseResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row2=p.row;if(p.claimed===!0&&row2&&typeof row2=="object")return{ok:!0,kind:"acquired-or-renewed",row:row2};if(p.claimed===!1&&p.reason==="lease_held"&&row2&&typeof row2=="object")return{ok:!1,kind:"held-by-other",reason:"lease_held",row:row2};if(p.claimed===!1&&p.reason==="terminal"&&row2&&typeof row2=="object")return{ok:!1,kind:"terminal",reason:"terminal",row:row2};throw new ConductorBridgeApiError("server")}async function claimEpicSupervisionLease(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.leaseOwner),requirePositiveSafeInteger(request.ttlSeconds);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/lease/claim`),body=JSON.stringify({repo_name:access2.repoName,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicSupervisionLeaseResult(parsed)}async function fetchEpicRunState(access2,epicKey,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/state`,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchExplainRun(access2,epicRunId,ticketKey,fetchImpl=globalThis.fetch){requireNonEmptyString(epicRunId);let apiPath=`${epicRunApiPath(epicRunId)}/explain`;ticketKey!==void 0&&(requireNonEmptyString(ticketKey),requireNoSlashPathSegment(ticketKey),apiPath=`${epicRunApiPath(epicRunId)}/tickets/${encodeURIComponent(ticketKey)}/explain`);let url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function isEpicRunStatus(value){return typeof value=="string"&&EPIC_RUN_STATUS_VALUES.includes(value)}async function readEpicRunCompletionState(access2,epicRunId,fetchImpl=globalThis.fetch){if(typeof epicRunId!="string"||epicRunId.trim().length===0)return{ok:!1,reason:"invalid-input"};let parsed;try{parsed=await fetchEpicRunState(access2,epicRunId,fetchImpl)}catch(err){return err instanceof ConductorBridgeApiError?err.kind==="http"&&err.status===404?{ok:!1,reason:"not-found"}:err.kind==="unauthorized"?{ok:!1,reason:"unauthorized"}:err.kind==="timeout"?{ok:!1,reason:"timeout"}:err.kind==="server"?{ok:!1,reason:"server"}:err.kind==="network"?{ok:!1,reason:"network"}:err.kind==="invalid-input"?{ok:!1,reason:"invalid-input"}:{ok:!1,reason:"malformed"}:{ok:!1,reason:"network"}}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed"};let epicRun=parsed.epic_run;if(!epicRun||typeof epicRun!="object"||Array.isArray(epicRun))return{ok:!1,reason:"malformed"};let status=epicRun.status;if(!isEpicRunStatus(status))return{ok:!1,reason:"malformed"};let policyJson=epicRun.policy_json,featureBranch=resolveDeclaredRunBaseBranch(policyJson);return{ok:!0,state:featureBranch!==void 0?{status,featureBranch}:{status}}}async function fetchActiveEpicRuns(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`,{repo_name:access2.repoName,status:"active",limit:"20"}),parsed=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(parsed&&typeof parsed=="object"){let runs=parsed.runs;if(Array.isArray(runs))return runs}return[]}async function createEpicRun(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let body={repo_name:access2.repoName,epic_key:request.epicKey,status:request.status??"planning",current_plan_version:request.currentPlanVersion??0};request.policyJson!==void 0&&(body.policy_json=request.policyJson),request.budgetWallClockSeconds!==void 0&&(body.budget_wall_clock_seconds=request.budgetWallClockSeconds),request.budgetCostCents!==void 0&&(body.budget_cost_cents=request.budgetCostCents);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`);return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function createEpicRunWithDisposition(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let body={repo_name:access2.repoName,epic_key:request.epicKey,status:request.status??"planning",current_plan_version:request.currentPlanVersion??0};request.policyJson!==void 0&&(body.policy_json=request.policyJson),request.budgetWallClockSeconds!==void 0&&(body.budget_wall_clock_seconds=request.budgetWallClockSeconds),request.budgetCostCents!==void 0&&(body.budget_cost_cents=request.budgetCostCents);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`),{status,body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout("POST",url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return{run:parsed,created:status===201}}async function replaceEpicRunPolicy(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId);let url=buildConductorJiraUrl(access2.baseUrl,epicRunApiPath(request.epicRunId)),body=JSON.stringify({repo_name:access2.repoName,policy_json:request.policyJson});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function updateEpicRunStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,epicRunApiPath(request.epicKey)),body=JSON.stringify({repo_name:access2.repoName,status:request.status,...request.expectedStatus?{expected_status:request.expectedStatus}:{}});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function stopEpicRun(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/stop`),body=JSON.stringify({repo_name:access2.repoName,...request.reason!==void 0?{reason:request.reason}:{},...request.expectedGeneration!==void 0?{expected_generation:request.expectedGeneration}:{}});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseAdvanceEpicTicketStatusResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed;if(p.ok===!1&&p.kind==="cas-conflict")return{ok:!1,kind:"cas-conflict",...typeof p.current_row_version=="number"?{current_row_version:p.current_row_version}:{},...p.ticket_status&&typeof p.ticket_status=="object"?{ticket_status:p.ticket_status}:{}};if(p.ok===!0&&p.ticket_status&&typeof p.ticket_status=="object")return{ok:!0,ticket_status:p.ticket_status};if("ticket_key"in p&&"status"in p&&"row_version"in p)return{ok:!0,ticket_status:parsed};throw new ConductorBridgeApiError("server")}async function advanceEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion),requireNonNegativeSafeInteger(request.planVersion),requireEpicTicketStatusValue(request.nextStatus),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let CAS_ENDPOINT_PATH=`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}`,url=buildConductorJiraUrl(access2.baseUrl,CAS_ENDPOINT_PATH),body=JSON.stringify({repo_name:access2.repoName,status:request.nextStatus,plan_version:request.planVersion,expected_row_version:request.expectedRowVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}}),parsed=await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseAdvanceEpicTicketStatusResult(parsed)}async function postUnparkLikeRequest(url,headers,body,fetchImpl){try{let parsed=await fetchConductorJsonPostWithTimeout(url,headers,body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseAdvanceEpicTicketStatusResult(parsed)}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===400&&typeof err.currentRowVersion=="number")return parseAdvanceEpicTicketStatusResult({ok:!1,kind:"cas-conflict",current_row_version:err.currentRowVersion});throw err}}async function unparkEpicTicket(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/unpark`),body=JSON.stringify({repo_name:access2.repoName,expected_row_version:request.expectedRowVersion,...request.idempotencyKey!==void 0?{idempotency_key:request.idempotencyKey}:{},...request.reason!==void 0?{reason:request.reason}:{}});return postUnparkLikeRequest(url,conductorPostHeaders(access2),body,fetchImpl)}async function adoptCurrentHeadAndUnparkTicket(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/adopt-current-head-and-unpark`),body=JSON.stringify({repo_name:access2.repoName,expected_row_version:request.expectedRowVersion,...request.idempotencyKey!==void 0?{idempotency_key:request.idempotencyKey}:{},...request.reason!==void 0?{reason:request.reason}:{}});return postUnparkLikeRequest(url,conductorPostHeaders(access2),body,fetchImpl)}function parseRepairNullableString(value){return typeof value=="string"?value:null}function parseRepairBase(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw new ConductorBridgeApiError("server");let p=parsed;if(typeof p.epic_run_id!="string"||typeof p.committed!="boolean"||typeof p.event_recorded!="boolean")throw new ConductorBridgeApiError("server");return p}function parseScopeReparseResult(parsed){let p=parseRepairBase(parsed);if(typeof p.scope_id!="string"||typeof p.parse_scheduled!="boolean")throw new ConductorBridgeApiError("server");return{epic_run_id:p.epic_run_id,scope_id:p.scope_id,before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,outcome:typeof p.outcome=="string"?p.outcome:"",parse_scheduled:p.parse_scheduled}}function parseDispatchRemediationResult(parsed){let p=parseRepairBase(parsed);if(typeof p.ticket_key!="string")throw new ConductorBridgeApiError("server");let jobId=p.job_id;return{epic_run_id:p.epic_run_id,ticket_key:p.ticket_key,before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,job_type:typeof p.job_type=="string"?p.job_type:"",job_id:typeof jobId=="number"&&Number.isSafeInteger(jobId)?jobId:null}}function parseMergeChildRouteResponse(parsed){let p=parseRepairBase(parsed),prNumber=p.pr_number;if(typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0)throw new ConductorBridgeApiError("server");let localExecution=null,local=p.local_execution;if(local&&typeof local=="object"&&!Array.isArray(local)){let l=local,sha=normalizeSha(l.expected_head_sha);if(sha===null)throw new ConductorBridgeApiError("server");localExecution={expected_head_sha:sha,merge_method:parseRepairNullableString(l.merge_method)}}return{epic_run_id:p.epic_run_id,pr_number:prNumber,ticket_key:typeof p.ticket_key=="string"?p.ticket_key:"",before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,reason:parseRepairNullableString(p.reason),local_execution:localExecution}}async function postRepairRequest(access2,apiPath,body,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("POST",buildConductorJiraUrl(access2.baseUrl,apiPath),conductorPostHeaders(access2),JSON.stringify({repo_name:access2.repoName,...body}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl,{readErrorField:!0})}async function scopeReparse(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.scopeId);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/scope-reparse`,{scope_id:request.scopeId},fetchImpl);return parseScopeReparseResult(parsed)}async function dispatchRemediation(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.context);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`,{context:request.context},fetchImpl);return parseDispatchRemediationResult(parsed)}async function mergeChild(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requirePositiveSafeInteger(request.prNumber);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/merge-child`,{pr_number:request.prNumber},fetchImpl);return parseMergeChildRouteResponse(parsed)}async function completeMergeChild(access2,request,fetchImpl=globalThis.fetch){if(requireNonEmptyString(request.epicRunId),requirePositiveSafeInteger(request.prNumber),!["merged","already_merged","failed"].includes(request.result))throw new ConductorBridgeApiError("invalid-input");let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/merge-child/complete`,{pr_number:request.prNumber,result:request.result,...request.reason!==void 0?{reason:request.reason}:{}},fetchImpl);return parseMergeChildRouteResponse(parsed)}async function createEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireEpicTicketStatusValue(request.status),requireNonNegativeSafeInteger(request.planVersion),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets`),body=JSON.stringify({repo_name:access2.repoName,ticket_key:request.ticketKey,status:request.status,plan_version:request.planVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}});await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseEpicDispatchResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row2=p.row;if(!row2||typeof row2!="object")throw new ConductorBridgeApiError("server");let dispatch=row2;if(p.claimed===!0)return{ok:!0,kind:"claimed",dispatch};if(p.claimed===!1){let reason=p.reason;if(reason==="already_spawned")return{ok:!0,kind:"already-spawned",dispatch};if(reason==="already_exists")return{ok:!0,kind:"already-exists",dispatch};if(reason==="terminal")return{ok:!0,kind:"terminal",terminal:!0,dispatch};if(reason==="lease_held")return{ok:!1,kind:"lease-held",dispatch}}throw new ConductorBridgeApiError("server")}async function recordEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.leaseOwner),requireNonNegativeSafeInteger(request.planVersion),requirePositiveSafeInteger(request.ttlSeconds);let dispatchKey=buildEpicDispatchKey(request.epicKey,request.ticketKey,request.planVersion,request.attempt??0)+(request.reviewRole?":review":""),url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/dispatch/claim`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epicKey,ticket_key:request.ticketKey,plan_version:request.planVersion,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds,dispatch_key:dispatchKey}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicDispatchResult(parsed)}async function transitionEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.dispatchKey),requireNoSlashPathSegment(request.dispatchKey),requireEpicDispatchTransitionStatus(request.nextStatus),request.nextStatus==="run_spawned"&&requireNonEmptyString(request.runId);let path59=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path59),body=request.nextStatus==="run_spawned"?JSON.stringify({repo_name:access2.repoName,run_id:request.runId}):JSON.stringify({repo_name:access2.repoName});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseShadowMergeReconcileResult(parsed){let obj=parsed??{};return{applies:obj.applies===!0,scheduled:obj.scheduled===!0,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:"unknown",shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,requiredCommitSha:typeof obj.required_commit_sha=="string"?obj.required_commit_sha:null,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null}}function parseShadowDispatchFreshnessResult(parsed){let obj=parsed??{},rawVerdict=typeof obj.verdict=="string"?obj.verdict:"",verdict=SHADOW_FRESHNESS_VERDICTS.has(rawVerdict)?rawVerdict:"stale";return{verdict,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:verdict,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null,indexedCommitSha:typeof obj.indexed_commit_sha=="string"?obj.indexed_commit_sha:null,shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,lastError:typeof obj.last_error=="string"?obj.last_error:null,deadlineExpired:obj.deadline_expired===!0,blockedAdvanceReason:typeof obj.blocked_advance_reason=="string"&&obj.blocked_advance_reason.length>0?obj.blocked_advance_reason:null}}async function reconcileShadowMerge(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/shadow/merge-reconcile`),body=JSON.stringify({repo_name:access2.repoName,merged_ticket_key:request.mergedTicketKey??null}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowMergeReconcileResult(parsed)}async function fetchShadowDispatchFreshness(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNoSlashPathSegment(request.ticketKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}/shadow-freshness`),body=JSON.stringify({repo_name:access2.repoName}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowDispatchFreshnessResult(parsed)}async function validateEpicPlan(access2,request,fetchImpl=globalThis.fetch){requirePositiveSafeInteger(request.planVersion);let blobVersion=request.planBlob.plan_version;if(blobVersion!==void 0&&blobVersion!==request.planVersion)throw new ConductorValidationError(`planVersion mismatch: request.planVersion=${request.planVersion} but planBlob.plan_version=${blobVersion}`);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/plan/validate`),body={repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob};request.epicKey!==void 0&&(body.epic_key=request.epicKey),request.trackedPaths!==void 0&&(body.tracked_paths=request.trackedPaths);let parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseValidateEpicPlanResult(parsed)}function parseValidateEpicPlanResult(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw new ConductorBridgeApiError("server");let p=parsed,planHash=p.plan_hash,serializationEnabled=p.serialization_enabled,insertedEdges=p.inserted_edges,valid=p.valid;if(typeof valid!="boolean"||typeof planHash!="string"||planHash.trim()===""||typeof serializationEnabled!="boolean"||typeof insertedEdges!="number"||!Number.isSafeInteger(insertedEdges)||insertedEdges<0)throw new ConductorBridgeApiError("server");let rejections=parsePlanLintRejections(p.rejections),notices=parsePlanLintNotices(p.notices);if(!valid&&rejections.length===0)throw new ConductorBridgeApiError("server");return{valid,rejections,notices,evaluationComplete:typeof p.evaluation_complete=="boolean"?p.evaluation_complete:!0,planHash,serializationEnabled,insertedEdges,overlappingPairsFound:safeCount(p.overlapping_pairs_found),undeclaredNodes:safeCount(p.undeclared_nodes),undeclaredPairsSkipped:safeCount(p.undeclared_pairs_skipped),coverageScope:typeof p.coverage_scope=="string"&&p.coverage_scope.trim()!==""?p.coverage_scope:"unreported"}}function parsePlanLintRejections(value){if(!Array.isArray(value))return[];let out=[];for(let raw of value){if(!raw||typeof raw!="object"||Array.isArray(raw))continue;let entry=raw,reasonCode=entry.reason_code,explanation=entry.explanation;if(typeof reasonCode!="string"||reasonCode.trim()===""||typeof explanation!="string"||explanation.trim()==="")continue;let rejection={reasonCode,explanation,ticketKeys:safeStringArray(entry.ticket_keys)};typeof entry.path=="string"&&entry.path!==""&&(rejection.path=entry.path),typeof entry.migration_id=="string"&&entry.migration_id!==""&&(rejection.migrationId=entry.migration_id),out.push(rejection)}return out}function parsePlanLintNotices(value){if(!Array.isArray(value))return[];let out=[];for(let raw of value){if(!raw||typeof raw!="object"||Array.isArray(raw))continue;let entry=raw,noticeCode=entry.notice_code,explanation=entry.explanation;typeof noticeCode!="string"||noticeCode.trim()===""||typeof explanation!="string"||explanation.trim()===""||out.push({noticeCode,explanation,ticketKeys:safeStringArray(entry.ticket_keys),paths:safeStringArray(entry.paths)})}return out}function safeStringArray(value){return Array.isArray(value)?value.filter(v=>typeof v=="string"&&v!==""):[]}function safeCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0?value:0}async function storeEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion),requireNonEmptyString(request.planHash);let blobVersion=request.planBlob.plan_version;if(blobVersion!==void 0&&blobVersion!==request.planVersion)throw new ConductorValidationError(`planVersion mismatch: request.planVersion=${request.planVersion} but planBlob.plan_version=${blobVersion}`);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/plan`),storeBody={repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob,plan_hash:request.planHash};request.trackedPaths!==void 0&&(storeBody.tracked_paths=request.trackedPaths);let body=JSON.stringify(storeBody);return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function updateApprovedPlanNodeTicketSpec(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.expectedPlanHash),requireNonEmptyString(request.ticketSpec);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/plan/nodes/${encodeURIComponent(request.ticketKey)}/ticket-spec`),body=JSON.stringify({repo_name:access2.repoName,expected_plan_hash:request.expectedPlanHash,ticket_spec:request.ticketSpec});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseApproveEpicPlanSuccess(parsed){let record=parsed??{},result={ok:!0,plan_hash:record.plan_hash},prov=record.feature_branch_provisioning;if(prov&&typeof prov=="object"&&!Array.isArray(prov)){let p=prov;(p.status==="created"||p.status==="already_exists")&&typeof p.feature_branch=="string"&&typeof p.source_branch=="string"&&typeof p.source_sha=="string"&&typeof p.remote_head_sha=="string"&&(result.featureBranchProvisioning={status:p.status,feature_branch:p.feature_branch,source_branch:p.source_branch,source_sha:p.source_sha,remote_head_sha:p.remote_head_sha})}return result}async function approveEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/approve-plan`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion});try{let parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseApproveEpicPlanSuccess(parsed)}catch(error){if(error instanceof ConductorBridgeApiError&&error.status===409){let preview=error.bodyPreview??"";return/multiple active runs/i.test(preview)?{ok:!1,kind:"conflict",reason:"multiple_active_runs"}:{ok:!1,kind:"conflict",reason:"superseded"}}throw error}}async function getEpicPlan(access2,epicKey,planVersion,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/plan`,{repo_name:access2.repoName,plan_version:String(planVersion)});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchParseStatus(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-status",{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function triggerRepositoryParse(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-repository"),body=JSON.stringify({repo_name:access2.repoName});return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function deletePullRequestBranch(access2,prNumber,expectedHeadSha,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/branch?repo_name=${encodeURIComponent(access2.repoName)}&expected_head_sha=${encodeURIComponent(expectedHeadSha)}`),controller=new AbortController,timer=setTimeout(()=>controller.abort(),CONDUCTOR_FETCH_TIMEOUT_MS);try{let resp;try{resp=await fetchImpl(url,{method:"DELETE",headers:{"X-API-Key":access2.apiKey},body:"",signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(resp.status===404)return{deleted:!1,branch:null,reason:"not_found"};if(!resp.ok)throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status):resp.status>=500?new ConductorBridgeApiError("server",resp.status):new ConductorBridgeApiError("http",resp.status);try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function transitionJiraStatus(access2,ticketNumber,targetStatus="auto",fetchImpl=globalThis.fetch){if(!ticketNumber)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,`/tickets/${encodeURIComponent(ticketNumber)}/jira-status`),body=JSON.stringify({repo_name:access2.repoName,target_status:targetStatus});try{return await fetchConductorJsonPutWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl),{status:"transitioned"}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===400)return{status:"skipped"};throw err}}function readinessShapeError(){return new ConductorBridgeApiError("invalid-input",void 0,{errorCode:"READINESS_SHAPE_INVALID"})}function requireObject(value){if(typeof value!="object"||value===null||Array.isArray(value))throw readinessShapeError();return value}function requireBool(o,key){let v=o[key];if(typeof v!="boolean")throw readinessShapeError();return v}function requireInt(o,key){let v=o[key];if(typeof v!="number"||!Number.isInteger(v))throw readinessShapeError();return v}function requireNullableInt(o,key){let v=o[key];if(v==null)return null;if(typeof v!="number"||!Number.isInteger(v))throw readinessShapeError();return v}function requireNullableString(o,key){let v=o[key];if(v==null)return null;if(typeof v!="string")throw readinessShapeError();return v}function requireNullableBool(o,key){let v=o[key];if(v==null)return null;if(typeof v!="boolean")throw readinessShapeError();return v}function parseReviewPolicyAlignment(o){let raw=o.review_policy_alignment;if(raw==null)return null;let a=requireObject(raw),explanation=a.explanation;if(typeof explanation!="string")throw readinessShapeError();return{status:requireEnum(a,"status",CONDUCTOR_REVIEW_ALIGNMENT_STATUSES),repo_review_signal:requireNullableString(a,"repo_review_signal"),done_gate_review_signal:requireNullableString(a,"done_gate_review_signal"),explanation}}function requireEnum(o,key,allowed){let v=o[key];if(typeof v!="string"||!allowed.has(v))throw readinessShapeError();return v}function parseConductorReadinessResponse(body){let root=requireObject(body),repoName=root.repo_name;if(typeof repoName!="string"||repoName.length===0)throw readinessShapeError();let sup=requireObject(root.supervisor),gh=requireObject(root.github),rec=requireObject(root.reconciler),exec=requireObject(root.executor),thr=requireObject(root.thresholds);return{repo_name:repoName,supervisor:{setup_present:requireBool(sup,"setup_present"),setup_source:requireEnum(sup,"setup_source",READINESS_SOURCES),setup_created_at:requireNullableString(sup,"setup_created_at"),setup_updated_at:requireNullableString(sup,"setup_updated_at"),config_present:requireBool(sup,"config_present"),config_source:requireEnum(sup,"config_source",READINESS_SOURCES),config_created_at:requireNullableString(sup,"config_created_at"),config_updated_at:requireNullableString(sup,"config_updated_at"),required_checks_count:requireInt(sup,"required_checks_count"),required_checks_empty:requireBool(sup,"required_checks_empty"),auto_merge_enabled:requireBool(sup,"auto_merge_enabled"),merge_approval_required_set:requireBool(sup,"merge_approval_required_set"),review_policy_alignment:parseReviewPolicyAlignment(sup),review_policy_present:sup.review_policy_present===!0},github:{credentials_readable:requireBool(gh,"credentials_readable"),owner_resolved:requireBool(gh,"owner_resolved"),repo_id_resolved:requireBool(gh,"repo_id_resolved"),installation_id_resolved:requireBool(gh,"installation_id_resolved"),credentials_complete:requireBool(gh,"credentials_complete"),actions_probe_succeeded:requireBool(gh,"actions_probe_succeeded"),actions_permission_present:requireBool(gh,"actions_permission_present"),actions_permission_level:requireEnum(gh,"actions_permission_level",ACTIONS_LEVELS),actions_write:requireBool(gh,"actions_write")},reconciler:{liveness_readable:requireBool(rec,"liveness_readable"),last_tick_at:requireNullableString(rec,"last_tick_at"),last_tick_age_seconds:requireNullableInt(rec,"last_tick_age_seconds"),stale:requireBool(rec,"stale"),active_run_count:requireInt(rec,"active_run_count"),expired_lease_count:requireInt(rec,"expired_lease_count")},executor:{liveness_readable:requireBool(exec,"liveness_readable"),last_seen_at:requireNullableString(exec,"last_seen_at"),last_seen_age_seconds:requireNullableInt(exec,"last_seen_age_seconds"),ready:requireNullableBool(exec,"ready")},thresholds:{reconciler_stale_after_seconds:requireInt(thr,"reconciler_stale_after_seconds"),executor_stale_after_seconds:requireInt(thr,"executor_stale_after_seconds")},review_workflow:parseReviewWorkflow(root),conductor_ci_workflow:parseConductorCiWorkflow(root),unattended:parseUnattended(root),bring_up:parseBringUp(root)}}function parseBringUp(body){let raw=body.bring_up;if(raw==null)return null;let o=requireObject(raw),freshCount=requireInt(o,"fresh_reconciler_count");if(freshCount<0||freshCount>READINESS_FRESH_RECONCILER_COUNT_CAP)throw readinessShapeError();return{migration_readable:requireBool(o,"migration_readable"),database_migration_current:requireBool(o,"database_migration_current"),heartbeat_readable:requireBool(o,"heartbeat_readable"),worker_present:requireBool(o,"worker_present"),fresh_reconciler_count:freshCount,single_transition_authority:requireBool(o,"single_transition_authority")}}function parseReviewWorkflow(body){let raw=body.review_workflow;if(raw==null)return null;let o=requireObject(raw);return{probe_succeeded:requireBool(o,"probe_succeeded"),workflow_present:requireBool(o,"workflow_present"),emits_run_id:requireBool(o,"emits_run_id"),emits_reviewed_sha:requireBool(o,"emits_reviewed_sha")}}function parseConductorCiWorkflow(body){let raw=body.conductor_ci_workflow;if(raw==null)return null;let o=requireObject(raw);return{probe_succeeded:requireBool(o,"probe_succeeded"),workflow_present:requireBool(o,"workflow_present"),migration_guard_present:requireBool(o,"migration_guard_present")}}function parseUnattended(body){let raw=body.unattended;if(raw==null)return null;let o=requireObject(raw);return{conductor_allowed:requireBool(o,"conductor_allowed"),notify_webhook_default_declared:requireBool(o,"notify_webhook_default_declared"),notify_webhook_default_verified:o.notify_webhook_default_verified===void 0?null:requireNullableBool(o,"notify_webhook_default_verified"),repository_readiness_confirmed:requireBool(o,"repository_readiness_confirmed")}}async function fetchConductorReadiness(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/conductor-readiness`,{repo_name:access2.repoName}),body=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseConductorReadinessResponse(body)}function parseConductorSupervisorBootstrapResponse(body){let o=requireObject(body),repoName=o.repo_name;if(typeof repoName!="string"||repoName.length===0)throw readinessShapeError();let names=o.audited_field_names;if(!Array.isArray(names)||names.some(n=>typeof n!="string"))throw readinessShapeError();return{repo_name:repoName,setup_written:requireBool(o,"setup_written"),config_written:requireBool(o,"config_written"),setup_source:requireEnum(o,"setup_source",READINESS_SOURCES),config_source:requireEnum(o,"config_source",READINESS_SOURCES),setup_created_at:requireNullableString(o,"setup_created_at"),setup_updated_at:requireNullableString(o,"setup_updated_at"),config_created_at:requireNullableString(o,"config_created_at"),config_updated_at:requireNullableString(o,"config_updated_at"),required_checks_count:requireInt(o,"required_checks_count"),audited_field_names:names}}async function bootstrapConductorSupervisorDefaults(access2,request,fetchImpl){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap`,{repo_name:access2.repoName}),body=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(request),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseConductorSupervisorBootstrapResponse(body)}var CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS,CONDUCTOR_BRIDGE_API_ERROR_KINDS,CONDUCTOR_ERROR_PREVIEW_MAX,CONDUCTOR_VALIDATION_LOC_MAX_PARTS,ConductorBridgeApiError,EPIC_TICKET_STATUS_VALUES,EPIC_DISPATCH_TRANSITION_STATUSES,EPIC_RUNS_API_PREFIX,EPIC_RUN_STATUS_VALUES,SHADOW_FRESHNESS_VERDICTS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,READINESS_FRESH_RECONCILER_COUNT_CAP,READINESS_SOURCES,ACTIONS_LEVELS,init_bridge_api_client=__esm({"src/conductor/bridge-api-client.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_git_ci_types();init_errors();init_run_branch();CONDUCTOR_DEFAULT_BASE_URL="https://bridgegpt-api.com",CONDUCTOR_FETCH_TIMEOUT_MS=3e4;CONDUCTOR_BRIDGE_API_ERROR_KINDS=["invalid-input","network","timeout","unauthorized","server","http"],CONDUCTOR_ERROR_PREVIEW_MAX=200;CONDUCTOR_VALIDATION_LOC_MAX_PARTS=8;ConductorBridgeApiError=class extends Error{kind;status;errorCode;bodyPreview;currentRowVersion;constructor(kindOrMessage,status,diagnostics){let isKnownKind=CONDUCTOR_BRIDGE_API_ERROR_KINDS.includes(kindOrMessage),errorCode5=diagnostics?.errorCode,bodyPreview=diagnostics?.bodyPreview;if(isKnownKind){let parts=[`Conductor Bridge API request failed (${kindOrMessage}${typeof status=="number"?`, status ${status}`:""})`];errorCode5&&parts.push(`code=${errorCode5}`),bodyPreview&&parts.push(bodyPreview),super(parts.join(": "))}else super(kindOrMessage);this.name="ConductorBridgeApiError",this.kind=isKnownKind?kindOrMessage:"http",typeof status=="number"&&(this.status=status),errorCode5&&(this.errorCode=errorCode5),bodyPreview&&(this.bodyPreview=bodyPreview),typeof diagnostics?.currentRowVersion=="number"&&(this.currentRowVersion=diagnostics.currentRowVersion)}};EPIC_TICKET_STATUS_VALUES=["planned","ready","dispatched","running","blocked","abandoned","done","ready_for_review","reviewing","parse_pending"];EPIC_DISPATCH_TRANSITION_STATUSES=["run_spawned","terminal"];EPIC_RUNS_API_PREFIX="/epic-runs";EPIC_RUN_STATUS_VALUES=["planning","pending_approval","active","blocked","abandoned","done"];SHADOW_FRESHNESS_VERDICTS=new Set(["not_applicable","covered","stale","failed"]);CONDUCTOR_REVIEW_ALIGNMENT_STATUSES=new Set(["aligned","divergent","not_configured","invalid"]),READINESS_FRESH_RECONCILER_COUNT_CAP=2,READINESS_SOURCES=new Set(["epic","project_default","none"]),ACTIONS_LEVELS=new Set(["write","read","none","unknown"])}});function buildPrBaseContractLaunchInstruction(){return'PR base contract: when you open the pull request for this ticket you MUST run gh pr create --base "$BAPI_BASE_BRANCH" so the PR targets the run base branch. Do not infer the base from the current branch ancestry or from the repository default branch. If a pull request for this branch already exists, verify that its base equals $BAPI_BASE_BRANCH and report the mismatch rather than retargeting the PR or rebuilding the branch yourself.'}var PR_BASE_BRANCH_ENV_VAR,WORKER_BRANCH_ENV_VAR,init_pr_base_contract=__esm({"src/pr-base-contract.ts"(){"use strict";PR_BASE_BRANCH_ENV_VAR="BAPI_BASE_BRANCH",WORKER_BRANCH_ENV_VAR="BAPI_WORKER_BRANCH"}});function validateOptionalIndexScope(value){if(value!=null){if(typeof value!="string")throw new IndexScopeConfigurationError;if(value.trim().length!==0){if(!INDEX_SCOPE_PATTERN.test(value))throw new IndexScopeConfigurationError;return value}}}var INDEX_SCOPE_ENV_VAR,INDEX_SCOPE_HEADER,INDEX_SCOPE_PATTERN,INDEX_SCOPE_CONFIGURATION_ERROR,IndexScopeConfigurationError,init_index_scope_contract=__esm({"src/index-scope-contract.ts"(){"use strict";INDEX_SCOPE_ENV_VAR="BAPI_INDEX_SCOPE",INDEX_SCOPE_HEADER="X-Bapi-Index-Scope",INDEX_SCOPE_PATTERN=/^[0-9a-f]{32}$/,INDEX_SCOPE_CONFIGURATION_ERROR=`${INDEX_SCOPE_ENV_VAR} is not a valid index-scope declaration. Expected a server-minted scope identity; the value was not logged.`,IndexScopeConfigurationError=class extends Error{constructor(){super(INDEX_SCOPE_CONFIGURATION_ERROR),this.name="IndexScopeConfigurationError"}}}});import path18 from"path";function resolveBranchForTicket(key,overrides){return Object.prototype.hasOwnProperty.call(overrides,key)?overrides[key]:`feature/${key}`}async function branchExists(deps,branch){let result=await deps.runCommand("git",["show-ref","--verify","--quiet",`refs/heads/${branch}`],{cwd:deps.cwd});return commandSucceeded(result)}function buildWtSwitchArgs(branch,exists,baseStartPoint="main"){return exists?["switch","-y",branch,"--format=json"]:["switch","--create","-y",branch,"-b",baseStartPoint,"--format=json"]}function pathApiForPlatform3(platform){return platform==="win32"?path18.win32:path18.posix}function extractWorktreePath(stdout,cwd,platform=process.platform){let parsed;try{parsed=JSON.parse(stdout)}catch{throw new Error(`Could not parse Worktrunk JSON output: ${stdout.slice(0,200)}`)}let candidate=pickWorktreePathField(parsed);if(!candidate)throw new Error(`Worktrunk JSON did not include a worktree path: ${stdout.slice(0,200)}`);let pathApi=pathApiForPlatform3(platform);return pathApi.isAbsolute(candidate)?candidate:pathApi.resolve(cwd,candidate)}function pickWorktreePathField(parsed){if(!parsed||typeof parsed!="object")return;let obj=parsed;if(typeof obj.path=="string")return obj.path;if(typeof obj.worktree_path=="string")return obj.worktree_path;if(typeof obj.directory=="string")return obj.directory;if(obj.worktree&&typeof obj.worktree=="object"){let nested=obj.worktree;if(typeof nested.path=="string")return nested.path}}function staleLeftoverRemedy(branch,baseRef){return`it carries commits not on the resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree \u2014 delete it (git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`}function unattachedSuccessRemedy(branch){return`it carries the commits of a SUCCEEDED implement whose pull request was never attached (implement gate observation: pr_not_attached). Refusing to reuse the worktree \u2014 but this branch is finished work, not a leftover. Open or attach a pull request from '${branch}' and let the reconciler bind it. Do NOT remove or re-write the branch: either would destroy a completed implementation.`}async function isExistingBranchSafeToReuse(deps,branch,baseStartPoint,classification="unknown"){let baseRef=baseStartPoint,originRef=`origin/${baseStartPoint}`,originExists=await deps.runCommand("git",["rev-parse","--verify","--quiet",originRef],{cwd:deps.cwd});commandSucceeded(originExists)&&(baseRef=originRef);let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",branch,baseRef],{cwd:deps.cwd});if(commandSucceeded(ancestor))return{safe:!0};let remedy=classification==="succeeded_pr_not_attached"?unattachedSuccessRemedy(branch):staleLeftoverRemedy(branch,baseRef);return{safe:!1,reason:`existing branch '${branch}' is not an ancestor of ${baseRef}; ${remedy}`}}async function hardResetWorktree(deps,worktreePath,ref){let resetArgs=["reset","--hard",ref],reset=await deps.runCommand("git",resetArgs,{cwd:worktreePath});if(!commandSucceeded(reset)){let reason=(reset.stderr||reset.stdout||"").trim();return`git ${resetArgs.join(" ")} failed${reason?`: ${reason}`:""}`}return null}async function cleanUntrackedWorktree(deps,worktreePath){let emit=deps.onCleanupDiagnostic,report=message=>{emit&&emit(message)};try{let result=await deps.runCommand("git",["clean","-fd"],{cwd:worktreePath});for(let line of result.stdout.split(/\r?\n/)){if(!line.startsWith("Removing "))continue;let removed=line.slice(9).trim();removed.length>0&&report(`removed untracked path: ${removed}`)}commandSucceeded(result)||report(CLEANUP_FAILURE_DIAGNOSTIC)}catch{report(CLEANUP_FAILURE_DIAGNOSTIC)}}async function verifyWorktreeHead(deps,worktreePath,expected){let headRes=await deps.runCommand("git",["rev-parse","--verify","HEAD^{commit}"],{cwd:worktreePath});if(!commandSucceeded(headRes))return"failed to resolve worktree HEAD after creation (git rev-parse HEAD^{commit} failed).";let expectedRes=await deps.runCommand("git",["rev-parse","--verify",`${expected}^{commit}`],{cwd:worktreePath});if(!commandSucceeded(expectedRes))return"failed to resolve the expected base commit after creation (git rev-parse --verify failed).";let head=headRes.stdout.trim(),want=expectedRes.stdout.trim();return head!==want?`worktree head ${head.slice(0,12)} does not match the pinned base ${want.slice(0,12)}; Worktrunk seeded the worktree from an unexpected start point. Refusing to hand a mis-seeded worktree to a worker.`:null}async function createWorktreeForTicket(deps,key,branchOverrides,worktrunkBinary,baseStartPoint="main",guardStaleWorktree=!1,behavior={}){let branch=resolveBranchForTicket(key,branchOverrides);try{let exists=await branchExists(deps,branch);if(exists&&guardStaleWorktree){let safety=await isExistingBranchSafeToReuse(deps,branch,baseStartPoint,behavior.staleBranchClassification??"unknown");if(!safety.safe)return{key,branch,status:"create-failed",error:`stale worktree guard: ${safety.reason}`}}let args=buildWtSwitchArgs(branch,exists,baseStartPoint),result=await deps.runCommand(worktrunkBinary,args,{cwd:deps.cwd});if(!commandSucceeded(result)){let reason=(result.stderr||result.stdout||"").trim();return{key,branch,status:"create-failed",error:`${worktrunkBinary} ${args.join(" ")} failed${reason?`: ${reason}`:""}`}}let worktreePath=extractWorktreePath(result.stdout,deps.cwd,deps.platform);if(exists&&behavior.freshenFromOrigin){let resetError=await hardResetWorktree(deps,worktreePath,behavior.freshenFromOrigin);if(resetError)return{key,branch,status:"create-failed",error:resetError};await cleanUntrackedWorktree(deps,worktreePath)}if(exists&&behavior.alignExistingBranchTo){let resetError=await hardResetWorktree(deps,worktreePath,behavior.alignExistingBranchTo);if(resetError)return{key,branch,status:"create-failed",error:resetError};await cleanUntrackedWorktree(deps,worktreePath)}if(behavior.verifyHeadMatches){let verifyError=await verifyWorktreeHead(deps,worktreePath,behavior.verifyHeadMatches);if(verifyError)return{key,branch,status:"create-failed",error:verifyError}}return{key,branch,status:"created",path:worktreePath}}catch(err){let message=err instanceof Error?err.message:String(err);return{key,branch,status:"create-failed",error:message}}}var CLEANUP_FAILURE_DIAGNOSTIC,init_worktree_core=__esm({"src/worktree-core.ts"(){"use strict";init_start_tickets_prereqs();CLEANUP_FAILURE_DIAGNOSTIC="untracked-file cleanup did not complete; continuing with the reset worktree"}});function symlinkUnsupportedWarning(name){return{category:"symlink_unsupported",message:`worktree ${name}: this platform could not create a symbolic link, so the file was copied instead. The worktree now holds a second copy of that environment file at rest. On Windows, enable Developer Mode (or run elevated) to get links instead.`}}function linkFailedWarning(name){return{category:"link_failed",message:`worktree ${name}: could not be provided from the main checkout, so this worktree has no copy of it. Commands that need it will fail until it is linked by hand.`}}function errorCode(err){if(!err||typeof err!="object")return;let code=err.code;return typeof code=="string"?code:void 0}function sameFile(a,b){return!a||!b?!1:a.dev===b.dev&&a.ino===b.ino}async function linkOperatorEnvFiles(mainCheckout,worktreePath,deps){let pathApi=pathApiForPlatform3(deps.platform??process.platform),result={linked:[],copied:[],unchanged:[],warnings:[]};for(let name of LINKED_OPERATOR_ENV_FILES){let source=pathApi.join(mainCheckout,name),destination=pathApi.join(worktreePath,name);try{let sourceStats=await deps.stat(source);if(!sourceStats||!sourceStats.isFile())continue;let existing=await deps.lstat(destination);if(existing){if(existing.isSymbolicLink()&&sameFile(await deps.stat(destination),sourceStats)){result.unchanged.push(name);continue}await deps.unlink(destination)}try{await deps.symlink(source,destination),result.linked.push(name)}catch(err){if(!SYMLINK_UNSUPPORTED_CODES.has(errorCode(err)??""))throw err;await deps.copyFile(source,destination),result.copied.push(name),result.warnings.push(symlinkUnsupportedWarning(name))}}catch{result.warnings.push(linkFailedWarning(name))}}return result}var LINKED_OPERATOR_ENV_FILES,SYMLINK_UNSUPPORTED_CODES,init_env_file_link=__esm({"src/env-file-link.ts"(){"use strict";init_worktree_core();LINKED_OPERATOR_ENV_FILES=[".env",".env.test"];SYMLINK_UNSUPPORTED_CODES=new Set(["EPERM","ENOSYS","EOPNOTSUPP","ENOTSUP","UNKNOWN"])}});import path19 from"path";function validateBranchName(branch){if(branch.trim().length===0)return"branch name must not be empty.";if([...branch].length>255)return"branch name must be 255 characters or fewer.";if(branch.startsWith("-"))return"branch name must not start with '-'.";if(branch.includes(".."))return"branch name must not contain '..'.";if(branch.endsWith(".lock"))return"branch name must not end with '.lock'.";for(let ch of branch){let code=ch.codePointAt(0);if(code<=31||code===127)return"branch name must not contain control characters."}return null}function normalizeRepoKey(cwd){return path19.resolve(cwd)}async function withRepoFetchLock(repoKey,fn){let previous=repoFetchLocks.get(repoKey)??Promise.resolve(),releaseCurrent,current=new Promise(resolve2=>{releaseCurrent=resolve2}),chained=previous.then(()=>current);repoFetchLocks.set(repoKey,chained),await previous.catch(()=>{});try{return await fn()}finally{releaseCurrent(),repoFetchLocks.get(repoKey)===chained&&repoFetchLocks.delete(repoKey)}}async function fetchAndResolveBaseSha(deps,baseBranch){let validationError2=validateBranchName(baseBranch);if(validationError2)return{ok:!1,error:`Invalid base branch '${baseBranch}': ${validationError2}`};let repoKey=normalizeRepoKey(deps.cwd);return withRepoFetchLock(repoKey,async()=>{let defaultBranch=await resolveRepositoryDefaultBranch(deps),extraRefs=defaultBranch&&defaultBranch!==baseBranch&&validateBranchName(defaultBranch)===null?[defaultBranch]:[],fetch2=await deps.runCommand("git",["fetch","origin",baseBranch,...extraRefs],{cwd:deps.cwd});if(!commandSucceeded(fetch2)&&extraRefs.length>0&&(fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd})),!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`};let resolve2=await deps.runCommand("git",["rev-parse","--verify",`origin/${baseBranch}^{commit}`],{cwd:deps.cwd});return commandSucceeded(resolve2)?{ok:!0,base_sha:resolve2.stdout.trim()}:{ok:!1,error:`Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`}})}var repoFetchLocks,init_base_ref=__esm({"src/base-ref.ts"(){"use strict";init_claude_review_workflow_drift_probe();init_start_tickets_prereqs();init_claude_review_workflow_drift_probe();repoFetchLocks=new Map}});import{execFile}from"child_process";import{readFile as readFile5,writeFile as writeFile3,mkdir as mkdir3,mkdtemp,stat as stat3,lstat,symlink,unlink,copyFile,readdir as readdir2,rm as rm2}from"fs/promises";import os5 from"node:os";import{createHash as createHash4}from"node:crypto";import path20 from"path";import{existsSync as existsSync2}from"node:fs";import{tmpdir}from"node:os";function appendSummaryRowWarning(row2,warning){return{...row2,warnings:[...row2.warnings??[],warning]}}function getStartTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,"","Flags:"," --agent claude|cursor-agent|codex Agent command to launch in each worktree (overrides --host; default: claude)"," --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions"," Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate."," --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases."," --tier cheap|basic|premium Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available."," --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement"," --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only"," --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use"," --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)"," --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)"," --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility"," --max-parallel N Max worktrees to create concurrently (default: 3)"," --conductor Enable the Conductor system: per-worker hook injection + BAPI_CONDUCTOR_* env, lifecycle ledger events, and a supervisor peer tab (default: off \u2014 a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>'). Implies --guard-stale-branch: a pre-existing ticket branch carrying commits beyond the resolved base is refused, not silently reused."," --guard-stale-branch Refuse to reuse a pre-existing feature/KEY branch whose tip is NOT an ancestor of the resolved base (it carries commits the base does not). The ticket is reported create-failed with the stale-worktree remedy instead of spawning a worker on leftover code. Implied by --conductor; pass it explicitly on the LLM-conductor pilot cut path. Off by default, so a plain interactive run keeps reusing a same-named branch."," -h, --help Show this help","","Environment:",` ${WORKTRUNK_BINARY_OVERRIDE_ENV} Override the Worktrunk executable name/path for nonstandard installs`,` ${TMUX_SESSION_OVERRIDE_ENV} Override the tmux session-name prefix on Linux (default: ${DEFAULT_TMUX_SESSION_PREFIX})`," BAPI_CONDUCTOR_GATE_NAME Conductor gate name for this run (default: implement-ticket)"," BAPI_CONDUCTOR_SUPERVISOR_MODE Conductor supervisor mode (default: auto when --auto, else interactive)"," BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE Set 1/true to also register a PreToolUse conductor hook","","Conductor observability (opt-in via --conductor):"," With --conductor, real Claude Code workers launched by start-tickets receive"," per-worktree conductor hook injection (into .claude/settings.local.json) and emit"," local lifecycle events into the conductor ledger. Each such run mints one run_id"," and attributes worker events by worker_id, ticket key, and worktree path, and a"," supervisor peer tab is opened. Without --conductor none of this happens. Inspect"," the ledger with the `conductor` CLI. The BAPI_CONDUCTOR_* env vars above apply"," only when --conductor is set.","","Prerequisites:"," macOS wt, git, osascript"," Windows git-wt, Git for Windows / Git Bash, Windows Terminal or PowerShell"," Linux wt, git, tmux","",TICKET_KEY_USAGE_SUMMARY].join(`
3745
+ LIMIT @limit`),toDelivered=db.prepare("UPDATE messages SET state = 'delivered', updated_at = datetime('now') WHERE seq = @seq AND state = 'pending'"),toAcked=db.prepare("UPDATE messages SET state = 'acked', acked_at = datetime('now'), updated_at = datetime('now') WHERE seq = @seq AND state = 'delivered'"),reread=db.prepare("SELECT * FROM messages WHERE seq = ?"),delivered=[];return db.transaction(()=>{let pending=selectPending.all({run_id:runId,worker_id:workerId,limit});for(let row2 of pending){if(toDelivered.run({seq:row2.seq}).changes!==1)continue;insertRelayAuditEvent(db,{id:`message.delivered:${row2.id}`,source:"conductor-worker",type:"message.delivered",run_id:row2.run_id,worker_id:row2.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message delivered to worker",status:"delivered",details:{message_id:row2.id,message_type:row2.type}}}),toAcked.run({seq:row2.seq}),insertRelayAuditEvent(db,{id:`message.acked:${row2.id}`,source:"conductor-worker",type:"message.acked",run_id:row2.run_id,worker_id:row2.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message acknowledged by worker",status:"acked",details:{message_id:row2.id,message_type:row2.type}}});let finalRow=reread.get(row2.seq);delivered.push(rowToConductorWorkerMessage(finalRow))}}).immediate(),{messages:delivered,count:delivered.length,acked_count:delivered.length}}finally{db.close()}}var ConductorPersistenceUnavailableError,LEDGER_NATIVE_MODULE_NAME,ConductorNativeModuleLoadError,databaseModulePromise,dbLoadFailure,dbLoadDiagnosticEmitted,BUSY_TIMEOUT_DEFAULT,BUSY_TIMEOUT_MIN,BUSY_TIMEOUT_MAX,RETENTION_DAYS_DEFAULT,RETENTION_DAYS_MAX,RETENTION_MAX_ROWS_DEFAULT,RETENTION_MAX_ROWS_MIN,RETENTION_MAX_ROWS_MAX,POLL_LIMIT_DEFAULT,POLL_LIMIT_MAX,MESSAGE_COOLDOWN_DEFAULT_MS,MESSAGE_COOLDOWN_MIN_MS,MESSAGE_COOLDOWN_MAX_MS,CHECK_MESSAGES_LIMIT_DEFAULT,CHECK_MESSAGES_LIMIT_MAX,WAIT_TIMEOUT_MAX_MS,WAIT_POLL_INTERVAL_MS,SUMMARY_FIELD_MAX_CHARS,CURRENT_CONDUCTOR_SCHEMA_VERSION,MESSAGE_TYPE_PATTERN,init_store=__esm({"src/conductor/store.ts"(){"use strict";init_taxonomy();init_errors();init_data_normalization();init_paths();ConductorPersistenceUnavailableError=class extends Error{constructor(message="Conductor persistence is unavailable: the optional 'better-sqlite3' native module could not be loaded."){super(message),this.name="ConductorPersistenceUnavailableError"}},LEDGER_NATIVE_MODULE_NAME="better-sqlite3",ConductorNativeModuleLoadError=class extends ConductorPersistenceUnavailableError{details;failureKind;constructor(failureKind,details){super("Conductor ledger native module failed to load for this Node runtime."),this.name="ConductorNativeModuleLoadError",this.failureKind=failureKind,this.details=details}};databaseModulePromise=null,dbLoadFailure=null,dbLoadDiagnosticEmitted=!1;BUSY_TIMEOUT_DEFAULT=1e4,BUSY_TIMEOUT_MIN=250,BUSY_TIMEOUT_MAX=12e4,RETENTION_DAYS_DEFAULT=30,RETENTION_DAYS_MAX=3650,RETENTION_MAX_ROWS_DEFAULT=5e4,RETENTION_MAX_ROWS_MIN=100,RETENTION_MAX_ROWS_MAX=1e7,POLL_LIMIT_DEFAULT=100,POLL_LIMIT_MAX=1e3,MESSAGE_COOLDOWN_DEFAULT_MS=3e5,MESSAGE_COOLDOWN_MIN_MS=1e3,MESSAGE_COOLDOWN_MAX_MS=864e5,CHECK_MESSAGES_LIMIT_DEFAULT=10,CHECK_MESSAGES_LIMIT_MAX=100,WAIT_TIMEOUT_MAX_MS=12e4,WAIT_POLL_INTERVAL_MS=500,SUMMARY_FIELD_MAX_CHARS=500;CURRENT_CONDUCTOR_SCHEMA_VERSION=8;MESSAGE_TYPE_PATTERN=/^[A-Za-z0-9._:-]{1,100}$/}});import{randomBytes}from"node:crypto";import{fileURLToPath}from"node:url";function randomCorrelationFragment(){return randomBytes(4).toString("hex")}function sanitizeIdSegment(value){return value.replace(/[^A-Za-z0-9_-]/g,"-")}function mintStartTicketsRunId(keys,fragment=randomCorrelationFragment()){return`${keys.length>0?keys[0]:"start-tickets"}-start-tickets-${fragment}`}function mintStartTicketsWorkerId(ticketKey,agentName,fragment=randomCorrelationFragment()){return`${ticketKey}-${sanitizeIdSegment(agentName)}-${fragment}`}function buildEpicIdentityEnv(epic){let env={BAPI_CONDUCTOR_EPIC_KEY:epic.epic_key,BAPI_CONDUCTOR_EPIC_RUN_ID:epic.epic_run_id,BAPI_CONDUCTOR_PLAN_VERSION:String(epic.plan_version)},declared=normalizeDeclaredTouchedFiles(epic.declared_touched_files);return declared.length>0&&(env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON=JSON.stringify(declared)),env}function defaultResolveBinPath(filename){return fileURLToPath(new URL(`./${filename}`,import.meta.url))}function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}async function createStartTicketsConductorContext(options,agent,deps){let resolveRepoName2=deps.resolveRepoName??resolveStartTicketsRepoName,repoName=null;try{repoName=await resolveRepoName2({env:deps.env,cwd:deps.cwd,readFile:deps.readFile})}catch{repoName=null}let gateName=nonEmpty(deps.env.BAPI_CONDUCTOR_GATE_NAME)?deps.env.BAPI_CONDUCTOR_GATE_NAME.trim():DEFAULT_CONDUCTOR_GATE_NAME,supervisorMode=nonEmpty(deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE)?deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE.trim():options.autoApprove?"auto":"interactive",resolveBinPath=deps.resolveBinPath??defaultResolveBinPath,context={runId:mintStartTicketsRunId(options.keys,deps.fragment),repoName,gateName,supervisorMode,agentName:agent.name,cliFile:resolveBinPath("epic-implementer-bin.js"),conductorNodePath:deps.execPath??process.execPath,hookBinPath:resolveBinPath("epic-implementer-claude-hook-bin.js")};return options.epic&&(context.epic=options.epic),context}function isConductorFlagEnabled(value){if(typeof value!="string")return!1;let v=value.trim().toLowerCase();return v==="1"||v==="true"}function buildConductorWorkerEnv(context,worker,parentEnv){let parentActiveGroups=new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));parentActiveGroups.add("conductor");let env={BAPI_CONDUCTOR_ENABLED:"1",BRIDGE_MCP_PROFILE:Array.from(parentActiveGroups).join(","),BAPI_CONDUCTOR_RUN_ID:context.runId,BAPI_CONDUCTOR_WORKER_ID:worker.workerId,BAPI_CONDUCTOR_TICKET_KEY:worker.ticketKey,BAPI_CONDUCTOR_WORKTREE_PATH:worker.worktreePath,BAPI_CONDUCTOR_GATE_NAME:context.gateName,BAPI_CONDUCTOR_SUPERVISOR_MODE:context.supervisorMode,BAPI_CONDUCTOR_CLI_FILE:context.cliFile,CONDUCTOR_NODE_PATH:context.conductorNodePath};if(context.repoName&&(env.BAPI_CONDUCTOR_REPO_NAME=context.repoName),context.epic){let epicEnv=buildEpicIdentityEnv(context.epic);for(let[k,v]of Object.entries(epicEnv))env[k]=v}isConductorFlagEnabled(parentEnv.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE)&&(env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE="1");for(let key of CONDUCTOR_TUNING_ENV_KEYS)nonEmpty(parentEnv[key])&&(env[key]=parentEnv[key].trim());return env}function shellQuotePath(value){return`'${value.replace(/'/g,"'\\''")}'`}function resolveConductorHookCommand(env,hookBinPath,execPath=process.execPath){return nonEmpty(env.BAPI_CONDUCTOR_HOOK_COMMAND)?env.BAPI_CONDUCTOR_HOOK_COMMAND:`${shellQuotePath(execPath)} ${shellQuotePath(hookBinPath)}`}function mergeClaudeSettingsWithConductorHook(settings,command,options={}){return mergeClaudeSettingsWithCommandHook(settings,command,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,options)}async function provisionConductorHookForWorktree(worktreePath,command,options,deps){return provisionClaudeSettingsForWorktree(worktreePath,existing=>mergeClaudeSettingsWithConductorHook(existing,command,{enablePreToolUse:options.enablePreToolUse,preToolUseMatcher:options.preToolUseMatcher??detectExistingPreToolUseMatcher(existing)}),deps)}async function provisionConductorHooksForRows(rows,context,deps){let isClaude=context.agentName==="claude",enablePreToolUse=isConductorFlagEnabled(deps.env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE),command=resolveConductorHookCommand(deps.env,context.hookBinPath,deps.execPath),fragment=deps.workerFragment,out=[];for(let row2 of rows){let base={...row2,runId:context.runId};if(row2.status!=="created"||!row2.path){out.push(base);continue}if(!isClaude){out.push(base);continue}let workerId=mintStartTicketsWorkerId(row2.key,context.agentName,fragment?fragment():randomCorrelationFragment()),conductorEnv=buildConductorWorkerEnv(context,{workerId,ticketKey:row2.key,worktreePath:row2.path},deps.env),result=await provisionConductorHookForWorktree(row2.path,command,{enablePreToolUse},deps);if(!result.ok){out.push({...base,workerId,warnings:[...base.warnings??[],`conductor hook not injected: ${result.error}`]});continue}out.push({...base,workerId,conductorEnv,conductorHookInjected:!0})}return out}function buildStartTicketsRunStartedEventInput(context,rows,options){let worktreeRows=rows.filter(r=>typeof r.path=="string"&&r.path.length>0),workers=worktreeRows.map(r=>({ticket_key:r.key,worker_id:r.workerId??null,worktree_path:r.path??null,status:r.status})),subject=context.repoName??(options.keys.length>0?options.keys[0]:"start-tickets");return{source:"start-tickets",type:"run.started",run_id:context.runId,producer:"bridge-api-mcp-server",observed_via:"start-tickets",subject,data:{summary:"start-tickets run started",status:"started",details:{repo:context.repoName,requested_ticket_keys:options.keys,ticket_keys:worktreeRows.map(r=>r.key),worktree_paths:worktreeRows.map(r=>r.path),workers,gate_name:context.gateName,supervisor_mode:context.supervisorMode,dry_run:options.dryRun,agent:context.agentName,...context.epic?{epic_key:context.epic.epic_key,epic_run_id:context.epic.epic_run_id,plan_version:context.epic.plan_version}:{}}}}}async function emitStartTicketsRunStarted(context,rows,options,deps={}){let event=buildStartTicketsRunStartedEventInput(context,rows,options);try{if(deps.emit)deps.emit(event);else{let{emitConductorEvent:emitConductorEvent2}=await Promise.resolve().then(()=>(init_store(),store_exports));emitConductorEvent2(event)}return rows}catch{if(rows.length===0)return rows;let[first,...rest]=rows;return[{...first,warnings:[...first.warnings??[],CONDUCTOR_RUN_START_EMIT_FAILED_WARNING]},...rest]}}function posixSingleQuote(value){return`'${value.replace(/'/g,"'\\''")}'`}function powershellSingleQuote(value){return`'${value.replace(/'/g,"''")}'`}function injectConductorEnvIntoShellCommand(platform,shellCommand,env){if(!env)return shellCommand;let entries=Object.entries(env).filter(([key])=>ENV_KEY_PATTERN.test(key));if(entries.length===0)return shellCommand;let isWindows=platform==="win32";return`${entries.map(([key,value])=>isWindows?`$env:${key}=${powershellSingleQuote(value)};`:`export ${key}=${posixSingleQuote(value)};`).join(" ")} ${shellCommand}`}function buildSupervisorTabCommand(context,platform,nodeExecPath=process.execPath){let quote=platform==="win32"?powershellSingleQuote:posixSingleQuote;return`${quote(nodeExecPath)} ${quote(context.cliFile)} supervise --run-id ${quote(context.runId)}`}function isSupervisorLaunchEnabled(context){return context.supervisorMode.trim().toLowerCase()!=="off"}function supervisorSpawnKey(keys){return`${keys.length>0?keys[0]:"start-tickets"}-${SUPERVISOR_SPAWN_KEY_SUFFIX}`}var DEFAULT_CONDUCTOR_GATE_NAME,CONDUCTOR_TUNING_ENV_KEYS,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,CONDUCTOR_RUN_START_EMIT_FAILED_WARNING,ENV_KEY_PATTERN,SUPERVISOR_SPAWN_KEY_SUFFIX,init_start_tickets_conductor=__esm({"src/start-tickets-conductor.ts"(){"use strict";init_claude_settings();init_file_scope_guard();init_mcp_profile();init_start_tickets_repo();DEFAULT_CONDUCTOR_GATE_NAME="implement-ticket";CONDUCTOR_TUNING_ENV_KEYS=["BAPI_CONDUCTOR_BUSY_TIMEOUT_MS","BAPI_CONDUCTOR_RETENTION_DAYS","BAPI_CONDUCTOR_RETENTION_MAX_ROWS","BAPI_CONDUCTOR_MESSAGE_COOLDOWN_MS"];CONDUCTOR_HOOK_LIFECYCLE_EVENTS=["SessionStart","SessionEnd","Notification"];CONDUCTOR_RUN_START_EMIT_FAILED_WARNING="conductor run-start emit failed (continuing without run-level event)";ENV_KEY_PATTERN=/^[A-Z_][A-Z0-9_]*$/;SUPERVISOR_SPAWN_KEY_SUFFIX="supervisor"}});import{createHash as createHash3}from"node:crypto";function normalizeRepoName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function normalizeSha(value){if(typeof value!="string")return null;let lowered=value.trim().toLowerCase();return SHA_RE.test(lowered)?lowered:null}function normalizePrNumber(value){return typeof value!="number"||!Number.isSafeInteger(value)||value<=0?null:value}function normalizeCheckName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function canonicalize(value){if(Array.isArray(value))return value.map(item=>canonicalize(item));if(value!==null&&typeof value=="object"){let record=value,sortedKeys=Object.keys(record).sort(),out={};for(let key of sortedKeys)out[key]=canonicalize(record[key]);return out}return value}function stableJsonHash(value){let canonical=canonicalize(value),json=JSON.stringify(canonical)??"null";return createHash3("sha256").update(json).digest("hex")}var GIT_CI_PRODUCER,GIT_HOOK_PRODUCER,REQUIRED_CI_CHECKS_GREEN,REVIEW_STATE,DEFAULT_GATE_NAME,CONTROL_CHAR_RE,SHA_RE,VERDICTLESS_DISPOSITION_PARK,VERDICTLESS_DISPOSITION_FAIL_OPEN,VERDICTLESS_DISPOSITIONS,MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON,init_git_ci_types=__esm({"src/conductor/git-ci-types.ts"(){"use strict";GIT_CI_PRODUCER="git-pr-ci-producer",GIT_HOOK_PRODUCER="git-hook",REQUIRED_CI_CHECKS_GREEN="required_ci_checks_green",REVIEW_STATE="review_state",DEFAULT_GATE_NAME="done",CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/,SHA_RE=/^[0-9a-f]{40}$|^[0-9a-f]{64}$/;VERDICTLESS_DISPOSITION_PARK="park",VERDICTLESS_DISPOSITION_FAIL_OPEN="fail_open",VERDICTLESS_DISPOSITIONS=[VERDICTLESS_DISPOSITION_PARK,VERDICTLESS_DISPOSITION_FAIL_OPEN],MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON="review_waived_verdictless_fail_open"}});function nonblank(candidate){if(typeof candidate!="string")return;let trimmed=candidate.trim();return trimmed.length>0?trimmed:void 0}function resolveDeclaredRunBaseBranch(policyJson){if(policyJson===null||typeof policyJson!="object"||Array.isArray(policyJson))return;let policy=policyJson;for(let key of POLICY_BASE_BRANCH_KEYS){if(!Object.prototype.hasOwnProperty.call(policy,key))continue;let resolved=nonblank(policy[key]);if(resolved!==void 0)return resolved}}var POLICY_BASE_BRANCH_KEYS,init_run_branch=__esm({"src/conductor/run-branch.ts"(){"use strict";POLICY_BASE_BRANCH_KEYS=["base_branch","baseBranch"]}});var bridge_api_client_exports={};__export(bridge_api_client_exports,{CONDUCTOR_DEFAULT_BASE_URL:()=>CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS:()=>CONDUCTOR_FETCH_TIMEOUT_MS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES:()=>CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,ConductorBridgeApiError:()=>ConductorBridgeApiError,EPIC_RUN_STATUS_VALUES:()=>EPIC_RUN_STATUS_VALUES,READINESS_FRESH_RECONCILER_COUNT_CAP:()=>READINESS_FRESH_RECONCILER_COUNT_CAP,adoptCurrentHeadAndUnparkTicket:()=>adoptCurrentHeadAndUnparkTicket,advanceEpicTicketStatus:()=>advanceEpicTicketStatus,approveEpicPlan:()=>approveEpicPlan,bootstrapConductorSupervisorDefaults:()=>bootstrapConductorSupervisorDefaults,buildConductorJiraUrl:()=>buildConductorJiraUrl,buildConductorVcsUrl:()=>buildConductorVcsUrl,buildEpicDispatchKey:()=>buildEpicDispatchKey,claimEpicSupervisionLease:()=>claimEpicSupervisionLease,completeMergeChild:()=>completeMergeChild,createEpicRun:()=>createEpicRun,createEpicRunWithDisposition:()=>createEpicRunWithDisposition,createEpicTicketStatus:()=>createEpicTicketStatus,deletePullRequestBranch:()=>deletePullRequestBranch,dispatchRemediation:()=>dispatchRemediation,extractSanitizedErrorDiagnostics:()=>extractSanitizedErrorDiagnostics,fetchActiveEpicRuns:()=>fetchActiveEpicRuns,fetchConductorConfigField:()=>fetchConductorConfigField,fetchConductorJsonPatchWithTimeout:()=>fetchConductorJsonPatchWithTimeout,fetchConductorJsonPostWithTimeout:()=>fetchConductorJsonPostWithTimeout,fetchConductorJsonPutWithTimeout:()=>fetchConductorJsonPutWithTimeout,fetchConductorJsonWithTimeout:()=>fetchConductorJsonWithTimeout,fetchConductorReadiness:()=>fetchConductorReadiness,fetchEffectiveSupervisorConfig:()=>fetchEffectiveSupervisorConfig,fetchEffectiveSupervisorSetup:()=>fetchEffectiveSupervisorSetup,fetchEpicRunState:()=>fetchEpicRunState,fetchExplainRun:()=>fetchExplainRun,fetchParseStatus:()=>fetchParseStatus,fetchPrReviewStatus:()=>fetchPrReviewStatus,fetchShadowDispatchFreshness:()=>fetchShadowDispatchFreshness,getEpicPlan:()=>getEpicPlan,mergeChild:()=>mergeChild,mergePullRequestForGate:()=>mergePullRequestForGate,parseConductorReadinessResponse:()=>parseConductorReadinessResponse,parseConductorSupervisorBootstrapResponse:()=>parseConductorSupervisorBootstrapResponse,pollCiChecksForCommit:()=>pollCiChecksForCommit,readEpicRunCompletionState:()=>readEpicRunCompletionState,reconcileShadowMerge:()=>reconcileShadowMerge,recordEpicDispatch:()=>recordEpicDispatch,remediateEpicTicket:()=>remediateEpicTicket,replaceEpicRunPolicy:()=>replaceEpicRunPolicy,resolveConductorBridgeApiAccess:()=>resolveConductorBridgeApiAccess,safeDiagnosticMessage:()=>safeDiagnosticMessage,scopeReparse:()=>scopeReparse,stopEpicRun:()=>stopEpicRun,storeEpicPlan:()=>storeEpicPlan,transitionEpicDispatch:()=>transitionEpicDispatch,transitionJiraStatus:()=>transitionJiraStatus,triggerRepositoryParse:()=>triggerRepositoryParse,unparkEpicTicket:()=>unparkEpicTicket,updateApprovedPlanNodeTicketSpec:()=>updateApprovedPlanNodeTicketSpec,updateEpicRunStatus:()=>updateEpicRunStatus,validateEpicPlan:()=>validateEpicPlan});import os4 from"node:os";import{readFile as readFile4,stat as stat2}from"node:fs/promises";async function resolveConductorBridgeApiAccess(deps={}){let env=deps.env??process.env,cwd=deps.cwd??process.cwd(),homedir=deps.homedir??os4.homedir,platform=deps.platform??process.platform,readFileImpl=deps.readFile??(p=>readFile4(p,"utf-8")),statImpl=deps.stat??(p=>stat2(p)),repoName=deps.repoName?.trim()||await resolveStartTicketsRepoName({env,cwd,readFile:readFileImpl});if(!repoName)return{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"};let credResult;try{credResult=await resolveBapiCredentials(repoName,{env,homedir,platform,readFile:readFileImpl,stat:statImpl})}catch{return{ok:!1,kind:"credentials-unavailable",error:"failed to resolve Bridge API credentials"}}if(!credResult.ok)return{ok:!1,kind:"credentials-unavailable",error:"Bridge API credentials unavailable"};let baseUrlRaw=env.BAPI_BASE_URL,baseUrl=typeof baseUrlRaw=="string"&&baseUrlRaw.trim().length>0?baseUrlRaw.trim():CONDUCTOR_DEFAULT_BASE_URL;return{ok:!0,access:{repoName,apiKey:credResult.credentials.apiKey,baseUrl}}}function buildConductorJiraUrl(baseUrl,apiPath,params={}){let trimmed=baseUrl.replace(/\/+$/,""),url=new URL(`${trimmed}/jira${apiPath}`);for(let[k,v]of Object.entries(params))url.searchParams.set(k,v);return url.toString()}function redactErrorPreview(text3){return text3.replace(/sk-[A-Za-z0-9_-]{8,}/g,"[REDACTED]").replace(/(Bearer|X-API-Key|api[_-]?key)\b\s*[:=]?\s*\S+/gi,"$1 [REDACTED]").replace(/(webhook[_-]?url)\b["']?\s*[:=]?\s*["']?https?:\/\/\S+/gi,"$1 [REDACTED]").replace(/https?:\/\/[^\s/@]+:[^\s/@]+@\S+/gi,"[REDACTED]")}function stripWebhookUrlsDeep(value,depth=0){if(depth>12)return;if(Array.isArray(value))return value.map(item=>stripWebhookUrlsDeep(item,depth+1));if(!value||typeof value!="object")return value;let out={};for(let[key,item]of Object.entries(value))key!=="webhook_url"&&(out[key]=stripWebhookUrlsDeep(item,depth+1));return out}function boundedErrorPreview(text3){let redacted=redactErrorPreview(text3).replace(/\s+/g," ").trim();return redacted.length>CONDUCTOR_ERROR_PREVIEW_MAX?`${redacted.slice(0,CONDUCTOR_ERROR_PREVIEW_MAX)}\u2026`:redacted}function formatValidationDetailItem(item){if(!item||typeof item!="object")return;let record=item,msg=record.msg;if(typeof msg!="string"||!msg.trim())return;let loc=record.loc,parts=Array.isArray(loc)?loc.filter(part=>typeof part=="string"||typeof part=="number"):[],path59=(parts[0]==="body"?parts.slice(1):parts).slice(0,CONDUCTOR_VALIDATION_LOC_MAX_PARTS).join(".");return path59?`${path59}: ${msg}`:msg}function extractSanitizedErrorDiagnostics(body){if(typeof body=="string"){let trimmed=body.trim();return trimmed?{bodyPreview:boundedErrorPreview(trimmed)}:{}}if(!body||typeof body!="object")return{};let record=body,detail=record.detail,errorCode5,message;if(Array.isArray(detail))message=formatValidationDetailItem(detail[0]);else if(detail&&typeof detail=="object"){let d=detail;typeof d.error_code=="string"&&(errorCode5=d.error_code),typeof d.message=="string"&&(message=d.message)}else typeof detail=="string"&&(message=detail);!errorCode5&&typeof record.error_code=="string"&&(errorCode5=record.error_code),!message&&typeof record.message=="string"&&(message=record.message);let diagnostics={};errorCode5&&(diagnostics.errorCode=boundedErrorPreview(errorCode5)),message&&(diagnostics.bodyPreview=boundedErrorPreview(message));let rawCurrentRowVersion=detail&&typeof detail=="object"&&!Array.isArray(detail)?detail.current_row_version:record.current_row_version;return typeof rawCurrentRowVersion=="number"&&Number.isSafeInteger(rawCurrentRowVersion)&&rawCurrentRowVersion>=0&&(diagnostics.currentRowVersion=rawCurrentRowVersion),diagnostics}function redactDiagnosticValues(diagnostics,secrets){let scrub=text3=>{let out2=text3;for(let secret of secrets)secret&&secret.length>=4&&(out2=out2.split(secret).join("[REDACTED]"));return out2},out={};return diagnostics.errorCode&&(out.errorCode=scrub(diagnostics.errorCode)),diagnostics.bodyPreview&&(out.bodyPreview=scrub(diagnostics.bodyPreview)),typeof diagnostics.currentRowVersion=="number"&&(out.currentRowVersion=diagnostics.currentRowVersion),out}async function readSanitizedErrorDiagnostics(resp,headers={},options={}){try{let parsed=stripWebhookUrlsDeep(await resp.json()),diagnostics=extractSanitizedErrorDiagnostics(parsed);if(options.readErrorField&&!diagnostics.bodyPreview){let detail=parsed&&typeof parsed=="object"&&!Array.isArray(parsed)?parsed.detail:void 0,error=detail&&typeof detail=="object"&&!Array.isArray(detail)?detail.error:void 0;typeof error=="string"&&error.trim()&&(diagnostics.bodyPreview=boundedErrorPreview(error.trim()))}let secrets=Object.entries(headers).filter(([k])=>/key|authorization|token/i.test(k)).map(([,v])=>v);return redactDiagnosticValues(diagnostics,secrets)}catch{return{}}}function safeDiagnosticMessage(err,fallback){if(err instanceof ConductorBridgeApiError){let parts=[`kind=${err.kind}`];return typeof err.status=="number"&&parts.push(`status=${err.status}`),err.errorCode&&parts.push(`code=${err.errorCode}`),err.bodyPreview&&parts.push(err.bodyPreview),parts.join(" ")}return err instanceof Error?err.constructor.name:fallback}function conductorGetHeaders(access2){return{"X-API-Key":access2.apiKey}}async function fetchConductorJsonWithTimeout(url,headers,timeoutMs,fetchImpl=globalThis.fetch){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{headers,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorConfigField(access2,fieldName,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`/config-field/${encodeURIComponent(fieldName)}`,{repo_name:access2.repoName}),body=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(body&&typeof body=="object"&&"value"in body)return body.value}async function fetchEffectiveSupervisorSetup(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-setup/`:`${EPIC_RUNS_API_PREFIX}/supervisor-setup/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchEffectiveSupervisorConfig(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-config/`:`${EPIC_RUNS_API_PREFIX}/supervisor-config/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function pollCiChecksForCommit(access2,commitRef,fetchImpl=globalThis.fetch){let sha=normalizeSha(commitRef);if(sha===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,"/poll-ci-checks",{repo_name:access2.repoName,commit_ref:sha});return fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchPrReviewStatus(access2,prNumber,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/reviews/status`),fullUrl=new URL(url);return fullUrl.searchParams.set("repo_name",access2.repoName),fetchConductorJsonWithTimeout(fullUrl.toString(),conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function buildConductorVcsUrl(baseUrl,apiPath){let trimmed=baseUrl.replace(/\/+$/,""),path59=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path59}`).toString()}function conductorPostHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}async function fetchConductorJsonWithMethodAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions={}){let{body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions);return parsed}async function fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl,errorOptions={}){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{method,headers,body,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers,errorOptions);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return{status:resp.status,body:await resp.json()}}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorJsonPostWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("POST",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPatchWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PATCH",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPutWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PUT",url,headers,body,timeoutMs,fetchImpl)}async function mergePullRequestForGate(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number),sha=normalizeSha(request.expected_head_sha);if(pr===null||sha===null||!request.action_key||!request.repo_name)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/merge`),body=JSON.stringify({repo_name:request.repo_name,expected_head_sha:sha,gate:request.gate,action_key:request.action_key,...request.gate_event?{gate_event:request.gate_event}:{}});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function remediateEpicTicket(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number);if(pr===null||!request.epic_run_id||!request.ticket_key||!request.head_sha||!request.idempotency_key)throw new ConductorBridgeApiError("invalid-input");if(requireNonNegativeSafeInteger(request.expected_row_version),request.attempt_kind!=="nudge"&&request.attempt_kind!=="redispatch")throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/remediate`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epic_run_id,ticket_key:request.ticket_key,expected_row_version:request.expected_row_version,head_sha:request.head_sha,idempotency_key:request.idempotency_key,attempt_kind:request.attempt_kind,...request.reason?{reason:request.reason}:{}});try{return{ok:!0,conflict:!1,response:await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===409)return{ok:!0,conflict:!0};throw err}}function requireNonEmptyString(value){if(typeof value!="string"||value.trim().length===0)throw new ConductorBridgeApiError("invalid-input")}function requirePositiveSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<=0)throw new ConductorBridgeApiError("invalid-input")}function requireNonNegativeSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<0)throw new ConductorBridgeApiError("invalid-input")}function requireNoSlashPathSegment(value){if(value.includes("/"))throw new ConductorBridgeApiError("invalid-input")}function requireEpicTicketStatusValue(value){if(typeof value!="string"||!EPIC_TICKET_STATUS_VALUES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function requireEpicDispatchTransitionStatus(value){if(typeof value!="string"||!EPIC_DISPATCH_TRANSITION_STATUSES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function epicRunApiPath(epicKey){return`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}`}function epicDispatchTransitionApiPath(dispatchKey,nextStatus){return nextStatus==="run_spawned"?`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/run-spawned`:`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/terminal`}function buildEpicDispatchKey(epicKey,ticketKey,planVersion,attempt=0){requireNonEmptyString(epicKey),requireNonEmptyString(ticketKey),requireNonNegativeSafeInteger(planVersion),requireNonNegativeSafeInteger(attempt);let base=`dispatch:${epicKey}:${ticketKey}:${planVersion}`;return attempt>0?`${base}:r${attempt}`:base}function parseEpicSupervisionLeaseResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row2=p.row;if(p.claimed===!0&&row2&&typeof row2=="object")return{ok:!0,kind:"acquired-or-renewed",row:row2};if(p.claimed===!1&&p.reason==="lease_held"&&row2&&typeof row2=="object")return{ok:!1,kind:"held-by-other",reason:"lease_held",row:row2};if(p.claimed===!1&&p.reason==="terminal"&&row2&&typeof row2=="object")return{ok:!1,kind:"terminal",reason:"terminal",row:row2};throw new ConductorBridgeApiError("server")}async function claimEpicSupervisionLease(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.leaseOwner),requirePositiveSafeInteger(request.ttlSeconds);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/lease/claim`),body=JSON.stringify({repo_name:access2.repoName,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicSupervisionLeaseResult(parsed)}async function fetchEpicRunState(access2,epicKey,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/state`,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchExplainRun(access2,epicRunId,ticketKey,fetchImpl=globalThis.fetch){requireNonEmptyString(epicRunId);let apiPath=`${epicRunApiPath(epicRunId)}/explain`;ticketKey!==void 0&&(requireNonEmptyString(ticketKey),requireNoSlashPathSegment(ticketKey),apiPath=`${epicRunApiPath(epicRunId)}/tickets/${encodeURIComponent(ticketKey)}/explain`);let url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function isEpicRunStatus(value){return typeof value=="string"&&EPIC_RUN_STATUS_VALUES.includes(value)}async function readEpicRunCompletionState(access2,epicRunId,fetchImpl=globalThis.fetch){if(typeof epicRunId!="string"||epicRunId.trim().length===0)return{ok:!1,reason:"invalid-input"};let parsed;try{parsed=await fetchEpicRunState(access2,epicRunId,fetchImpl)}catch(err){return err instanceof ConductorBridgeApiError?err.kind==="http"&&err.status===404?{ok:!1,reason:"not-found"}:err.kind==="unauthorized"?{ok:!1,reason:"unauthorized"}:err.kind==="timeout"?{ok:!1,reason:"timeout"}:err.kind==="server"?{ok:!1,reason:"server"}:err.kind==="network"?{ok:!1,reason:"network"}:err.kind==="invalid-input"?{ok:!1,reason:"invalid-input"}:{ok:!1,reason:"malformed"}:{ok:!1,reason:"network"}}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed"};let epicRun=parsed.epic_run;if(!epicRun||typeof epicRun!="object"||Array.isArray(epicRun))return{ok:!1,reason:"malformed"};let status=epicRun.status;if(!isEpicRunStatus(status))return{ok:!1,reason:"malformed"};let policyJson=epicRun.policy_json,featureBranch=resolveDeclaredRunBaseBranch(policyJson);return{ok:!0,state:featureBranch!==void 0?{status,featureBranch}:{status}}}async function fetchActiveEpicRuns(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`,{repo_name:access2.repoName,status:"active",limit:"20"}),parsed=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(parsed&&typeof parsed=="object"){let runs=parsed.runs;if(Array.isArray(runs))return runs}return[]}async function createEpicRun(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let body={repo_name:access2.repoName,epic_key:request.epicKey,status:request.status??"planning",current_plan_version:request.currentPlanVersion??0};request.policyJson!==void 0&&(body.policy_json=request.policyJson),request.budgetWallClockSeconds!==void 0&&(body.budget_wall_clock_seconds=request.budgetWallClockSeconds),request.budgetCostCents!==void 0&&(body.budget_cost_cents=request.budgetCostCents);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`);return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function createEpicRunWithDisposition(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let body={repo_name:access2.repoName,epic_key:request.epicKey,status:request.status??"planning",current_plan_version:request.currentPlanVersion??0};request.policyJson!==void 0&&(body.policy_json=request.policyJson),request.budgetWallClockSeconds!==void 0&&(body.budget_wall_clock_seconds=request.budgetWallClockSeconds),request.budgetCostCents!==void 0&&(body.budget_cost_cents=request.budgetCostCents);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`),{status,body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout("POST",url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return{run:parsed,created:status===201}}async function replaceEpicRunPolicy(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId);let url=buildConductorJiraUrl(access2.baseUrl,epicRunApiPath(request.epicRunId)),body=JSON.stringify({repo_name:access2.repoName,policy_json:request.policyJson});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function updateEpicRunStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,epicRunApiPath(request.epicKey)),body=JSON.stringify({repo_name:access2.repoName,status:request.status,...request.expectedStatus?{expected_status:request.expectedStatus}:{}});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function stopEpicRun(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/stop`),body=JSON.stringify({repo_name:access2.repoName,...request.reason!==void 0?{reason:request.reason}:{},...request.expectedGeneration!==void 0?{expected_generation:request.expectedGeneration}:{}});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseAdvanceEpicTicketStatusResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed;if(p.ok===!1&&p.kind==="cas-conflict")return{ok:!1,kind:"cas-conflict",...typeof p.current_row_version=="number"?{current_row_version:p.current_row_version}:{},...p.ticket_status&&typeof p.ticket_status=="object"?{ticket_status:p.ticket_status}:{}};if(p.ok===!0&&p.ticket_status&&typeof p.ticket_status=="object")return{ok:!0,ticket_status:p.ticket_status};if("ticket_key"in p&&"status"in p&&"row_version"in p)return{ok:!0,ticket_status:parsed};throw new ConductorBridgeApiError("server")}async function advanceEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion),requireNonNegativeSafeInteger(request.planVersion),requireEpicTicketStatusValue(request.nextStatus),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let CAS_ENDPOINT_PATH=`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}`,url=buildConductorJiraUrl(access2.baseUrl,CAS_ENDPOINT_PATH),body=JSON.stringify({repo_name:access2.repoName,status:request.nextStatus,plan_version:request.planVersion,expected_row_version:request.expectedRowVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}}),parsed=await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseAdvanceEpicTicketStatusResult(parsed)}async function postUnparkLikeRequest(url,headers,body,fetchImpl){try{let parsed=await fetchConductorJsonPostWithTimeout(url,headers,body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseAdvanceEpicTicketStatusResult(parsed)}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===400&&typeof err.currentRowVersion=="number")return parseAdvanceEpicTicketStatusResult({ok:!1,kind:"cas-conflict",current_row_version:err.currentRowVersion});throw err}}async function unparkEpicTicket(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/unpark`),body=JSON.stringify({repo_name:access2.repoName,expected_row_version:request.expectedRowVersion,...request.idempotencyKey!==void 0?{idempotency_key:request.idempotencyKey}:{},...request.reason!==void 0?{reason:request.reason}:{}});return postUnparkLikeRequest(url,conductorPostHeaders(access2),body,fetchImpl)}async function adoptCurrentHeadAndUnparkTicket(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/adopt-current-head-and-unpark`),body=JSON.stringify({repo_name:access2.repoName,expected_row_version:request.expectedRowVersion,...request.idempotencyKey!==void 0?{idempotency_key:request.idempotencyKey}:{},...request.reason!==void 0?{reason:request.reason}:{}});return postUnparkLikeRequest(url,conductorPostHeaders(access2),body,fetchImpl)}function parseRepairNullableString(value){return typeof value=="string"?value:null}function parseRepairBase(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw new ConductorBridgeApiError("server");let p=parsed;if(typeof p.epic_run_id!="string"||typeof p.committed!="boolean"||typeof p.event_recorded!="boolean")throw new ConductorBridgeApiError("server");return p}function parseScopeReparseResult(parsed){let p=parseRepairBase(parsed);if(typeof p.scope_id!="string"||typeof p.parse_scheduled!="boolean")throw new ConductorBridgeApiError("server");return{epic_run_id:p.epic_run_id,scope_id:p.scope_id,before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,outcome:typeof p.outcome=="string"?p.outcome:"",parse_scheduled:p.parse_scheduled}}function parseDispatchRemediationResult(parsed){let p=parseRepairBase(parsed);if(typeof p.ticket_key!="string")throw new ConductorBridgeApiError("server");let jobId=p.job_id;return{epic_run_id:p.epic_run_id,ticket_key:p.ticket_key,before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,job_type:typeof p.job_type=="string"?p.job_type:"",job_id:typeof jobId=="number"&&Number.isSafeInteger(jobId)?jobId:null}}function parseMergeChildRouteResponse(parsed){let p=parseRepairBase(parsed),prNumber=p.pr_number;if(typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0)throw new ConductorBridgeApiError("server");let localExecution=null,local=p.local_execution;if(local&&typeof local=="object"&&!Array.isArray(local)){let l=local,sha=normalizeSha(l.expected_head_sha);if(sha===null)throw new ConductorBridgeApiError("server");localExecution={expected_head_sha:sha,merge_method:parseRepairNullableString(l.merge_method)}}return{epic_run_id:p.epic_run_id,pr_number:prNumber,ticket_key:typeof p.ticket_key=="string"?p.ticket_key:"",before:parseRepairNullableString(p.before),after:parseRepairNullableString(p.after),committed:p.committed,event_recorded:p.event_recorded,reason:parseRepairNullableString(p.reason),local_execution:localExecution}}async function postRepairRequest(access2,apiPath,body,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("POST",buildConductorJiraUrl(access2.baseUrl,apiPath),conductorPostHeaders(access2),JSON.stringify({repo_name:access2.repoName,...body}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl,{readErrorField:!0})}async function scopeReparse(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.scopeId);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/scope-reparse`,{scope_id:request.scopeId},fetchImpl);return parseScopeReparseResult(parsed)}async function dispatchRemediation(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.context);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`,{context:request.context},fetchImpl);return parseDispatchRemediationResult(parsed)}async function mergeChild(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicRunId),requirePositiveSafeInteger(request.prNumber);let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/merge-child`,{pr_number:request.prNumber},fetchImpl);return parseMergeChildRouteResponse(parsed)}async function completeMergeChild(access2,request,fetchImpl=globalThis.fetch){if(requireNonEmptyString(request.epicRunId),requirePositiveSafeInteger(request.prNumber),!["merged","already_merged","failed"].includes(request.result))throw new ConductorBridgeApiError("invalid-input");let parsed=await postRepairRequest(access2,`${epicRunApiPath(request.epicRunId)}/merge-child/complete`,{pr_number:request.prNumber,result:request.result,...request.reason!==void 0?{reason:request.reason}:{}},fetchImpl);return parseMergeChildRouteResponse(parsed)}async function createEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireEpicTicketStatusValue(request.status),requireNonNegativeSafeInteger(request.planVersion),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets`),body=JSON.stringify({repo_name:access2.repoName,ticket_key:request.ticketKey,status:request.status,plan_version:request.planVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}});await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseEpicDispatchResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row2=p.row;if(!row2||typeof row2!="object")throw new ConductorBridgeApiError("server");let dispatch=row2;if(p.claimed===!0)return{ok:!0,kind:"claimed",dispatch};if(p.claimed===!1){let reason=p.reason;if(reason==="already_spawned")return{ok:!0,kind:"already-spawned",dispatch};if(reason==="already_exists")return{ok:!0,kind:"already-exists",dispatch};if(reason==="terminal")return{ok:!0,kind:"terminal",terminal:!0,dispatch};if(reason==="lease_held")return{ok:!1,kind:"lease-held",dispatch}}throw new ConductorBridgeApiError("server")}async function recordEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.leaseOwner),requireNonNegativeSafeInteger(request.planVersion),requirePositiveSafeInteger(request.ttlSeconds);let dispatchKey=buildEpicDispatchKey(request.epicKey,request.ticketKey,request.planVersion,request.attempt??0)+(request.reviewRole?":review":""),url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/dispatch/claim`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epicKey,ticket_key:request.ticketKey,plan_version:request.planVersion,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds,dispatch_key:dispatchKey}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicDispatchResult(parsed)}async function transitionEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.dispatchKey),requireNoSlashPathSegment(request.dispatchKey),requireEpicDispatchTransitionStatus(request.nextStatus),request.nextStatus==="run_spawned"&&requireNonEmptyString(request.runId);let path59=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path59),body=request.nextStatus==="run_spawned"?JSON.stringify({repo_name:access2.repoName,run_id:request.runId}):JSON.stringify({repo_name:access2.repoName});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseShadowMergeReconcileResult(parsed){let obj=parsed??{};return{applies:obj.applies===!0,scheduled:obj.scheduled===!0,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:"unknown",shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,requiredCommitSha:typeof obj.required_commit_sha=="string"?obj.required_commit_sha:null,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null}}function parseShadowDispatchFreshnessResult(parsed){let obj=parsed??{},rawVerdict=typeof obj.verdict=="string"?obj.verdict:"",verdict=SHADOW_FRESHNESS_VERDICTS.has(rawVerdict)?rawVerdict:"stale";return{verdict,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:verdict,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null,indexedCommitSha:typeof obj.indexed_commit_sha=="string"?obj.indexed_commit_sha:null,shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,lastError:typeof obj.last_error=="string"?obj.last_error:null,deadlineExpired:obj.deadline_expired===!0,blockedAdvanceReason:typeof obj.blocked_advance_reason=="string"&&obj.blocked_advance_reason.length>0?obj.blocked_advance_reason:null}}async function reconcileShadowMerge(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/shadow/merge-reconcile`),body=JSON.stringify({repo_name:access2.repoName,merged_ticket_key:request.mergedTicketKey??null}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowMergeReconcileResult(parsed)}async function fetchShadowDispatchFreshness(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNoSlashPathSegment(request.ticketKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}/shadow-freshness`),body=JSON.stringify({repo_name:access2.repoName}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowDispatchFreshnessResult(parsed)}async function validateEpicPlan(access2,request,fetchImpl=globalThis.fetch){requirePositiveSafeInteger(request.planVersion);let blobVersion=request.planBlob.plan_version;if(blobVersion!==void 0&&blobVersion!==request.planVersion)throw new ConductorValidationError(`planVersion mismatch: request.planVersion=${request.planVersion} but planBlob.plan_version=${blobVersion}`);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/plan/validate`),body={repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob};request.epicKey!==void 0&&(body.epic_key=request.epicKey),request.trackedPaths!==void 0&&(body.tracked_paths=request.trackedPaths);let parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseValidateEpicPlanResult(parsed)}function parseValidateEpicPlanResult(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw new ConductorBridgeApiError("server");let p=parsed,planHash=p.plan_hash,serializationEnabled=p.serialization_enabled,insertedEdges=p.inserted_edges,valid=p.valid;if(typeof valid!="boolean"||typeof planHash!="string"||planHash.trim()===""||typeof serializationEnabled!="boolean"||typeof insertedEdges!="number"||!Number.isSafeInteger(insertedEdges)||insertedEdges<0)throw new ConductorBridgeApiError("server");let rejections=parsePlanLintRejections(p.rejections),notices=parsePlanLintNotices(p.notices);if(!valid&&rejections.length===0)throw new ConductorBridgeApiError("server");return{valid,rejections,notices,evaluationComplete:typeof p.evaluation_complete=="boolean"?p.evaluation_complete:!0,planHash,serializationEnabled,insertedEdges,overlappingPairsFound:safeCount(p.overlapping_pairs_found),undeclaredNodes:safeCount(p.undeclared_nodes),undeclaredPairsSkipped:safeCount(p.undeclared_pairs_skipped),coverageScope:typeof p.coverage_scope=="string"&&p.coverage_scope.trim()!==""?p.coverage_scope:"unreported"}}function parsePlanLintRejections(value){if(!Array.isArray(value))return[];let out=[];for(let raw of value){if(!raw||typeof raw!="object"||Array.isArray(raw))continue;let entry=raw,reasonCode=entry.reason_code,explanation=entry.explanation;if(typeof reasonCode!="string"||reasonCode.trim()===""||typeof explanation!="string"||explanation.trim()==="")continue;let rejection={reasonCode,explanation,ticketKeys:safeStringArray(entry.ticket_keys)};typeof entry.path=="string"&&entry.path!==""&&(rejection.path=entry.path),typeof entry.migration_id=="string"&&entry.migration_id!==""&&(rejection.migrationId=entry.migration_id),out.push(rejection)}return out}function parsePlanLintNotices(value){if(!Array.isArray(value))return[];let out=[];for(let raw of value){if(!raw||typeof raw!="object"||Array.isArray(raw))continue;let entry=raw,noticeCode=entry.notice_code,explanation=entry.explanation;typeof noticeCode!="string"||noticeCode.trim()===""||typeof explanation!="string"||explanation.trim()===""||out.push({noticeCode,explanation,ticketKeys:safeStringArray(entry.ticket_keys),paths:safeStringArray(entry.paths)})}return out}function safeStringArray(value){return Array.isArray(value)?value.filter(v=>typeof v=="string"&&v!==""):[]}function safeCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0?value:0}async function storeEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion),requireNonEmptyString(request.planHash);let blobVersion=request.planBlob.plan_version;if(blobVersion!==void 0&&blobVersion!==request.planVersion)throw new ConductorValidationError(`planVersion mismatch: request.planVersion=${request.planVersion} but planBlob.plan_version=${blobVersion}`);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/plan`),storeBody={repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob,plan_hash:request.planHash};request.trackedPaths!==void 0&&(storeBody.tracked_paths=request.trackedPaths);let body=JSON.stringify(storeBody);return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function updateApprovedPlanNodeTicketSpec(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.expectedPlanHash),requireNonEmptyString(request.ticketSpec);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/plan/nodes/${encodeURIComponent(request.ticketKey)}/ticket-spec`),body=JSON.stringify({repo_name:access2.repoName,expected_plan_hash:request.expectedPlanHash,ticket_spec:request.ticketSpec});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseApproveEpicPlanSuccess(parsed){let record=parsed??{},result={ok:!0,plan_hash:record.plan_hash},prov=record.feature_branch_provisioning;if(prov&&typeof prov=="object"&&!Array.isArray(prov)){let p=prov;(p.status==="created"||p.status==="already_exists")&&typeof p.feature_branch=="string"&&typeof p.source_branch=="string"&&typeof p.source_sha=="string"&&typeof p.remote_head_sha=="string"&&(result.featureBranchProvisioning={status:p.status,feature_branch:p.feature_branch,source_branch:p.source_branch,source_sha:p.source_sha,remote_head_sha:p.remote_head_sha})}return result}async function approveEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/approve-plan`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion});try{let parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseApproveEpicPlanSuccess(parsed)}catch(error){if(error instanceof ConductorBridgeApiError&&error.status===409){let preview=error.bodyPreview??"";return/multiple active runs/i.test(preview)?{ok:!1,kind:"conflict",reason:"multiple_active_runs"}:{ok:!1,kind:"conflict",reason:"superseded"}}throw error}}async function getEpicPlan(access2,epicKey,planVersion,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/plan`,{repo_name:access2.repoName,plan_version:String(planVersion)});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchParseStatus(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-status",{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function triggerRepositoryParse(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-repository"),body=JSON.stringify({repo_name:access2.repoName});return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function deletePullRequestBranch(access2,prNumber,expectedHeadSha,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/branch?repo_name=${encodeURIComponent(access2.repoName)}&expected_head_sha=${encodeURIComponent(expectedHeadSha)}`),controller=new AbortController,timer=setTimeout(()=>controller.abort(),CONDUCTOR_FETCH_TIMEOUT_MS);try{let resp;try{resp=await fetchImpl(url,{method:"DELETE",headers:{"X-API-Key":access2.apiKey},body:"",signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(resp.status===404)return{deleted:!1,branch:null,reason:"not_found"};if(!resp.ok)throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status):resp.status>=500?new ConductorBridgeApiError("server",resp.status):new ConductorBridgeApiError("http",resp.status);try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function transitionJiraStatus(access2,ticketNumber,targetStatus="auto",fetchImpl=globalThis.fetch){if(!ticketNumber)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,`/tickets/${encodeURIComponent(ticketNumber)}/jira-status`),body=JSON.stringify({repo_name:access2.repoName,target_status:targetStatus});try{return await fetchConductorJsonPutWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl),{status:"transitioned"}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===400)return{status:"skipped"};throw err}}function readinessShapeError(){return new ConductorBridgeApiError("invalid-input",void 0,{errorCode:"READINESS_SHAPE_INVALID"})}function requireObject(value){if(typeof value!="object"||value===null||Array.isArray(value))throw readinessShapeError();return value}function requireBool(o,key){let v=o[key];if(typeof v!="boolean")throw readinessShapeError();return v}function requireInt(o,key){let v=o[key];if(typeof v!="number"||!Number.isInteger(v))throw readinessShapeError();return v}function requireNullableInt(o,key){let v=o[key];if(v==null)return null;if(typeof v!="number"||!Number.isInteger(v))throw readinessShapeError();return v}function requireNullableString(o,key){let v=o[key];if(v==null)return null;if(typeof v!="string")throw readinessShapeError();return v}function requireNullableBool(o,key){let v=o[key];if(v==null)return null;if(typeof v!="boolean")throw readinessShapeError();return v}function parseReviewPolicyAlignment(o){let raw=o.review_policy_alignment;if(raw==null)return null;let a=requireObject(raw),explanation=a.explanation;if(typeof explanation!="string")throw readinessShapeError();return{status:requireEnum(a,"status",CONDUCTOR_REVIEW_ALIGNMENT_STATUSES),repo_review_signal:requireNullableString(a,"repo_review_signal"),done_gate_review_signal:requireNullableString(a,"done_gate_review_signal"),explanation}}function requireEnum(o,key,allowed){let v=o[key];if(typeof v!="string"||!allowed.has(v))throw readinessShapeError();return v}function parseConductorReadinessResponse(body){let root=requireObject(body),repoName=root.repo_name;if(typeof repoName!="string"||repoName.length===0)throw readinessShapeError();let sup=requireObject(root.supervisor),gh=requireObject(root.github),rec=requireObject(root.reconciler),exec=requireObject(root.executor),thr=requireObject(root.thresholds);return{repo_name:repoName,supervisor:{setup_present:requireBool(sup,"setup_present"),setup_source:requireEnum(sup,"setup_source",READINESS_SOURCES),setup_created_at:requireNullableString(sup,"setup_created_at"),setup_updated_at:requireNullableString(sup,"setup_updated_at"),config_present:requireBool(sup,"config_present"),config_source:requireEnum(sup,"config_source",READINESS_SOURCES),config_created_at:requireNullableString(sup,"config_created_at"),config_updated_at:requireNullableString(sup,"config_updated_at"),required_checks_count:requireInt(sup,"required_checks_count"),required_checks_empty:requireBool(sup,"required_checks_empty"),auto_merge_enabled:requireBool(sup,"auto_merge_enabled"),merge_approval_required_set:requireBool(sup,"merge_approval_required_set"),review_policy_alignment:parseReviewPolicyAlignment(sup),review_policy_present:sup.review_policy_present===!0},github:{credentials_readable:requireBool(gh,"credentials_readable"),owner_resolved:requireBool(gh,"owner_resolved"),repo_id_resolved:requireBool(gh,"repo_id_resolved"),installation_id_resolved:requireBool(gh,"installation_id_resolved"),credentials_complete:requireBool(gh,"credentials_complete"),actions_probe_succeeded:requireBool(gh,"actions_probe_succeeded"),actions_permission_present:requireBool(gh,"actions_permission_present"),actions_permission_level:requireEnum(gh,"actions_permission_level",ACTIONS_LEVELS),actions_write:requireBool(gh,"actions_write")},reconciler:{liveness_readable:requireBool(rec,"liveness_readable"),last_tick_at:requireNullableString(rec,"last_tick_at"),last_tick_age_seconds:requireNullableInt(rec,"last_tick_age_seconds"),stale:requireBool(rec,"stale"),active_run_count:requireInt(rec,"active_run_count"),expired_lease_count:requireInt(rec,"expired_lease_count")},executor:{liveness_readable:requireBool(exec,"liveness_readable"),last_seen_at:requireNullableString(exec,"last_seen_at"),last_seen_age_seconds:requireNullableInt(exec,"last_seen_age_seconds"),ready:requireNullableBool(exec,"ready")},thresholds:{reconciler_stale_after_seconds:requireInt(thr,"reconciler_stale_after_seconds"),executor_stale_after_seconds:requireInt(thr,"executor_stale_after_seconds")},review_workflow:parseReviewWorkflow(root),conductor_ci_workflow:parseConductorCiWorkflow(root),unattended:parseUnattended(root),bring_up:parseBringUp(root)}}function parseBringUp(body){let raw=body.bring_up;if(raw==null)return null;let o=requireObject(raw),freshCount=requireInt(o,"fresh_reconciler_count");if(freshCount<0||freshCount>READINESS_FRESH_RECONCILER_COUNT_CAP)throw readinessShapeError();return{migration_readable:requireBool(o,"migration_readable"),database_migration_current:requireBool(o,"database_migration_current"),heartbeat_readable:requireBool(o,"heartbeat_readable"),worker_present:requireBool(o,"worker_present"),fresh_reconciler_count:freshCount,single_transition_authority:requireBool(o,"single_transition_authority")}}function parseReviewWorkflow(body){let raw=body.review_workflow;if(raw==null)return null;let o=requireObject(raw);return{probe_succeeded:requireBool(o,"probe_succeeded"),workflow_present:requireBool(o,"workflow_present"),emits_run_id:requireBool(o,"emits_run_id"),emits_reviewed_sha:requireBool(o,"emits_reviewed_sha")}}function parseConductorCiWorkflow(body){let raw=body.conductor_ci_workflow;if(raw==null)return null;let o=requireObject(raw);return{probe_succeeded:requireBool(o,"probe_succeeded"),workflow_present:requireBool(o,"workflow_present"),migration_guard_present:requireBool(o,"migration_guard_present")}}function parseUnattended(body){let raw=body.unattended;if(raw==null)return null;let o=requireObject(raw);return{conductor_allowed:requireBool(o,"conductor_allowed"),notify_webhook_default_declared:requireBool(o,"notify_webhook_default_declared"),notify_webhook_default_verified:o.notify_webhook_default_verified===void 0?null:requireNullableBool(o,"notify_webhook_default_verified"),repository_readiness_confirmed:requireBool(o,"repository_readiness_confirmed")}}async function fetchConductorReadiness(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/conductor-readiness`,{repo_name:access2.repoName}),body=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseConductorReadinessResponse(body)}function parseConductorSupervisorBootstrapResponse(body){let o=requireObject(body),repoName=o.repo_name;if(typeof repoName!="string"||repoName.length===0)throw readinessShapeError();let names=o.audited_field_names;if(!Array.isArray(names)||names.some(n=>typeof n!="string"))throw readinessShapeError();return{repo_name:repoName,setup_written:requireBool(o,"setup_written"),config_written:requireBool(o,"config_written"),setup_source:requireEnum(o,"setup_source",READINESS_SOURCES),config_source:requireEnum(o,"config_source",READINESS_SOURCES),setup_created_at:requireNullableString(o,"setup_created_at"),setup_updated_at:requireNullableString(o,"setup_updated_at"),config_created_at:requireNullableString(o,"config_created_at"),config_updated_at:requireNullableString(o,"config_updated_at"),required_checks_count:requireInt(o,"required_checks_count"),audited_field_names:names}}async function bootstrapConductorSupervisorDefaults(access2,request,fetchImpl){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap`,{repo_name:access2.repoName}),body=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(request),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseConductorSupervisorBootstrapResponse(body)}var CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS,CONDUCTOR_BRIDGE_API_ERROR_KINDS,CONDUCTOR_ERROR_PREVIEW_MAX,CONDUCTOR_VALIDATION_LOC_MAX_PARTS,ConductorBridgeApiError,EPIC_TICKET_STATUS_VALUES,EPIC_DISPATCH_TRANSITION_STATUSES,EPIC_RUNS_API_PREFIX,EPIC_RUN_STATUS_VALUES,SHADOW_FRESHNESS_VERDICTS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,READINESS_FRESH_RECONCILER_COUNT_CAP,READINESS_SOURCES,ACTIONS_LEVELS,init_bridge_api_client=__esm({"src/conductor/bridge-api-client.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_git_ci_types();init_errors();init_run_branch();CONDUCTOR_DEFAULT_BASE_URL="https://bridgegpt-api.com",CONDUCTOR_FETCH_TIMEOUT_MS=3e4;CONDUCTOR_BRIDGE_API_ERROR_KINDS=["invalid-input","network","timeout","unauthorized","server","http"],CONDUCTOR_ERROR_PREVIEW_MAX=200;CONDUCTOR_VALIDATION_LOC_MAX_PARTS=8;ConductorBridgeApiError=class extends Error{kind;status;errorCode;bodyPreview;currentRowVersion;constructor(kindOrMessage,status,diagnostics){let isKnownKind=CONDUCTOR_BRIDGE_API_ERROR_KINDS.includes(kindOrMessage),errorCode5=diagnostics?.errorCode,bodyPreview=diagnostics?.bodyPreview;if(isKnownKind){let parts=[`Conductor Bridge API request failed (${kindOrMessage}${typeof status=="number"?`, status ${status}`:""})`];errorCode5&&parts.push(`code=${errorCode5}`),bodyPreview&&parts.push(bodyPreview),super(parts.join(": "))}else super(kindOrMessage);this.name="ConductorBridgeApiError",this.kind=isKnownKind?kindOrMessage:"http",typeof status=="number"&&(this.status=status),errorCode5&&(this.errorCode=errorCode5),bodyPreview&&(this.bodyPreview=bodyPreview),typeof diagnostics?.currentRowVersion=="number"&&(this.currentRowVersion=diagnostics.currentRowVersion)}};EPIC_TICKET_STATUS_VALUES=["planned","ready","dispatched","running","blocked","abandoned","done","ready_for_review","reviewing","parse_pending"];EPIC_DISPATCH_TRANSITION_STATUSES=["run_spawned","terminal"];EPIC_RUNS_API_PREFIX="/epic-runs";EPIC_RUN_STATUS_VALUES=["planning","pending_approval","active","blocked","abandoned","done"];SHADOW_FRESHNESS_VERDICTS=new Set(["not_applicable","covered","stale","failed"]);CONDUCTOR_REVIEW_ALIGNMENT_STATUSES=new Set(["aligned","divergent","not_configured","invalid"]),READINESS_FRESH_RECONCILER_COUNT_CAP=2,READINESS_SOURCES=new Set(["epic","project_default","none"]),ACTIONS_LEVELS=new Set(["write","read","none","unknown"])}});function buildPrBaseContractLaunchInstruction(){return'PR base contract: when you open the pull request for this ticket you MUST run gh pr create --base "$BAPI_BASE_BRANCH" so the PR targets the run base branch. Do not infer the base from the current branch ancestry or from the repository default branch. If a pull request for this branch already exists, verify that its base equals $BAPI_BASE_BRANCH and report the mismatch rather than retargeting the PR or rebuilding the branch yourself.'}var PR_BASE_BRANCH_ENV_VAR,WORKER_BRANCH_ENV_VAR,init_pr_base_contract=__esm({"src/pr-base-contract.ts"(){"use strict";PR_BASE_BRANCH_ENV_VAR="BAPI_BASE_BRANCH",WORKER_BRANCH_ENV_VAR="BAPI_WORKER_BRANCH"}});function validateOptionalIndexScope(value){if(value!=null){if(typeof value!="string")throw new IndexScopeConfigurationError;if(value.trim().length!==0){if(!INDEX_SCOPE_PATTERN.test(value))throw new IndexScopeConfigurationError;return value}}}var INDEX_SCOPE_ENV_VAR,INDEX_SCOPE_HEADER,INDEX_SCOPE_PATTERN,INDEX_SCOPE_CONFIGURATION_ERROR,IndexScopeConfigurationError,init_index_scope_contract=__esm({"src/index-scope-contract.ts"(){"use strict";INDEX_SCOPE_ENV_VAR="BAPI_INDEX_SCOPE",INDEX_SCOPE_HEADER="X-Bapi-Index-Scope",INDEX_SCOPE_PATTERN=/^[0-9a-f]{32}$/,INDEX_SCOPE_CONFIGURATION_ERROR=`${INDEX_SCOPE_ENV_VAR} is not a valid index-scope declaration. Expected a server-minted scope identity; the value was not logged.`,IndexScopeConfigurationError=class extends Error{constructor(){super(INDEX_SCOPE_CONFIGURATION_ERROR),this.name="IndexScopeConfigurationError"}}}});import path18 from"path";function resolveBranchForTicket(key,overrides){return Object.prototype.hasOwnProperty.call(overrides,key)?overrides[key]:`feature/${key}`}async function branchExists(deps,branch){let result=await deps.runCommand("git",["show-ref","--verify","--quiet",`refs/heads/${branch}`],{cwd:deps.cwd});return commandSucceeded(result)}function buildWtSwitchArgs(branch,exists,baseStartPoint="main"){return exists?["switch","-y",branch,"--format=json"]:["switch","--create","-y",branch,"-b",baseStartPoint,"--format=json"]}function pathApiForPlatform3(platform){return platform==="win32"?path18.win32:path18.posix}function extractWorktreePath(stdout,cwd,platform=process.platform){let parsed;try{parsed=JSON.parse(stdout)}catch{throw new Error(`Could not parse Worktrunk JSON output: ${stdout.slice(0,200)}`)}let candidate=pickWorktreePathField(parsed);if(!candidate)throw new Error(`Worktrunk JSON did not include a worktree path: ${stdout.slice(0,200)}`);let pathApi=pathApiForPlatform3(platform);return pathApi.isAbsolute(candidate)?candidate:pathApi.resolve(cwd,candidate)}function pickWorktreePathField(parsed){if(!parsed||typeof parsed!="object")return;let obj=parsed;if(typeof obj.path=="string")return obj.path;if(typeof obj.worktree_path=="string")return obj.worktree_path;if(typeof obj.directory=="string")return obj.directory;if(obj.worktree&&typeof obj.worktree=="object"){let nested=obj.worktree;if(typeof nested.path=="string")return nested.path}}function staleLeftoverRemedy(branch,baseRef){return`it carries commits not on the resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree \u2014 delete it (git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`}function unattachedSuccessRemedy(branch){return`it carries the commits of a SUCCEEDED implement whose pull request was never attached (implement gate observation: pr_not_attached). Refusing to reuse the worktree \u2014 but this branch is finished work, not a leftover. Open or attach a pull request from '${branch}' and let the reconciler bind it. Do NOT remove or re-write the branch: either would destroy a completed implementation.`}async function isExistingBranchSafeToReuse(deps,branch,baseStartPoint,classification="unknown"){let baseRef=baseStartPoint,originRef=`origin/${baseStartPoint}`,originExists=await deps.runCommand("git",["rev-parse","--verify","--quiet",originRef],{cwd:deps.cwd});commandSucceeded(originExists)&&(baseRef=originRef);let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",branch,baseRef],{cwd:deps.cwd});if(commandSucceeded(ancestor))return{safe:!0};let remedy=classification==="succeeded_pr_not_attached"?unattachedSuccessRemedy(branch):staleLeftoverRemedy(branch,baseRef);return{safe:!1,reason:`existing branch '${branch}' is not an ancestor of ${baseRef}; ${remedy}`}}async function hardResetWorktree(deps,worktreePath,ref){let resetArgs=["reset","--hard",ref],reset=await deps.runCommand("git",resetArgs,{cwd:worktreePath});if(!commandSucceeded(reset)){let reason=(reset.stderr||reset.stdout||"").trim();return`git ${resetArgs.join(" ")} failed${reason?`: ${reason}`:""}`}return null}async function cleanUntrackedWorktree(deps,worktreePath){let emit=deps.onCleanupDiagnostic,report=message=>{emit&&emit(message)};try{let result=await deps.runCommand("git",["clean","-fd"],{cwd:worktreePath});for(let line of result.stdout.split(/\r?\n/)){if(!line.startsWith("Removing "))continue;let removed=line.slice(9).trim();removed.length>0&&report(`removed untracked path: ${removed}`)}commandSucceeded(result)||report(CLEANUP_FAILURE_DIAGNOSTIC)}catch{report(CLEANUP_FAILURE_DIAGNOSTIC)}}async function verifyWorktreeHead(deps,worktreePath,expected){let headRes=await deps.runCommand("git",["rev-parse","--verify","HEAD^{commit}"],{cwd:worktreePath});if(!commandSucceeded(headRes))return"failed to resolve worktree HEAD after creation (git rev-parse HEAD^{commit} failed).";let expectedRes=await deps.runCommand("git",["rev-parse","--verify",`${expected}^{commit}`],{cwd:worktreePath});if(!commandSucceeded(expectedRes))return"failed to resolve the expected base commit after creation (git rev-parse --verify failed).";let head=headRes.stdout.trim(),want=expectedRes.stdout.trim();return head!==want?`worktree head ${head.slice(0,12)} does not match the pinned base ${want.slice(0,12)}; Worktrunk seeded the worktree from an unexpected start point. Refusing to hand a mis-seeded worktree to a worker.`:null}async function createWorktreeForTicket(deps,key,branchOverrides,worktrunkBinary,baseStartPoint="main",guardStaleWorktree=!1,behavior={}){let branch=resolveBranchForTicket(key,branchOverrides);try{let exists=await branchExists(deps,branch);if(exists&&guardStaleWorktree){let safety=await isExistingBranchSafeToReuse(deps,branch,baseStartPoint,behavior.staleBranchClassification??"unknown");if(!safety.safe)return{key,branch,status:"create-failed",error:`stale worktree guard: ${safety.reason}`}}let args=buildWtSwitchArgs(branch,exists,baseStartPoint),result=await deps.runCommand(worktrunkBinary,args,{cwd:deps.cwd});if(!commandSucceeded(result)){let reason=(result.stderr||result.stdout||"").trim();return{key,branch,status:"create-failed",error:`${worktrunkBinary} ${args.join(" ")} failed${reason?`: ${reason}`:""}`}}let worktreePath=extractWorktreePath(result.stdout,deps.cwd,deps.platform);if(exists&&behavior.freshenFromOrigin){let resetError=await hardResetWorktree(deps,worktreePath,behavior.freshenFromOrigin);if(resetError)return{key,branch,status:"create-failed",error:resetError};await cleanUntrackedWorktree(deps,worktreePath)}if(exists&&behavior.alignExistingBranchTo){let resetError=await hardResetWorktree(deps,worktreePath,behavior.alignExistingBranchTo);if(resetError)return{key,branch,status:"create-failed",error:resetError};await cleanUntrackedWorktree(deps,worktreePath)}if(behavior.verifyHeadMatches){let verifyError=await verifyWorktreeHead(deps,worktreePath,behavior.verifyHeadMatches);if(verifyError)return{key,branch,status:"create-failed",error:verifyError}}return{key,branch,status:"created",path:worktreePath}}catch(err){let message=err instanceof Error?err.message:String(err);return{key,branch,status:"create-failed",error:message}}}var CLEANUP_FAILURE_DIAGNOSTIC,init_worktree_core=__esm({"src/worktree-core.ts"(){"use strict";init_start_tickets_prereqs();CLEANUP_FAILURE_DIAGNOSTIC="untracked-file cleanup did not complete; continuing with the reset worktree"}});function symlinkUnsupportedWarning(name){return{category:"symlink_unsupported",message:`worktree ${name}: this platform could not create a symbolic link, so the file was copied instead. The worktree now holds a second copy of that environment file at rest. On Windows, enable Developer Mode (or run elevated) to get links instead.`}}function linkFailedWarning(name){return{category:"link_failed",message:`worktree ${name}: could not be provided from the main checkout, so this worktree has no copy of it. Commands that need it will fail until it is linked by hand.`}}function errorCode(err){if(!err||typeof err!="object")return;let code=err.code;return typeof code=="string"?code:void 0}function sameFile(a,b){return!a||!b?!1:a.dev===b.dev&&a.ino===b.ino}async function linkOperatorEnvFiles(mainCheckout,worktreePath,deps){let pathApi=pathApiForPlatform3(deps.platform??process.platform),result={linked:[],copied:[],unchanged:[],warnings:[]};for(let name of LINKED_OPERATOR_ENV_FILES){let source=pathApi.join(mainCheckout,name),destination=pathApi.join(worktreePath,name);try{let sourceStats=await deps.stat(source);if(!sourceStats||!sourceStats.isFile())continue;let existing=await deps.lstat(destination);if(existing){if(existing.isSymbolicLink()&&sameFile(await deps.stat(destination),sourceStats)){result.unchanged.push(name);continue}await deps.unlink(destination)}try{await deps.symlink(source,destination),result.linked.push(name)}catch(err){if(!SYMLINK_UNSUPPORTED_CODES.has(errorCode(err)??""))throw err;await deps.copyFile(source,destination),result.copied.push(name),result.warnings.push(symlinkUnsupportedWarning(name))}}catch{result.warnings.push(linkFailedWarning(name))}}return result}var LINKED_OPERATOR_ENV_FILES,SYMLINK_UNSUPPORTED_CODES,init_env_file_link=__esm({"src/env-file-link.ts"(){"use strict";init_worktree_core();LINKED_OPERATOR_ENV_FILES=[".env",".env.test"];SYMLINK_UNSUPPORTED_CODES=new Set(["EPERM","ENOSYS","EOPNOTSUPP","ENOTSUP","UNKNOWN"])}});import path19 from"path";function validateBranchName(branch){if(branch.trim().length===0)return"branch name must not be empty.";if([...branch].length>255)return"branch name must be 255 characters or fewer.";if(branch.startsWith("-"))return"branch name must not start with '-'.";if(branch.includes(".."))return"branch name must not contain '..'.";if(branch.endsWith(".lock"))return"branch name must not end with '.lock'.";for(let ch of branch){let code=ch.codePointAt(0);if(code<=31||code===127)return"branch name must not contain control characters."}return null}function normalizeRepoKey(cwd){return path19.resolve(cwd)}async function withRepoFetchLock(repoKey,fn){let previous=repoFetchLocks.get(repoKey)??Promise.resolve(),releaseCurrent,current=new Promise(resolve2=>{releaseCurrent=resolve2}),chained=previous.then(()=>current);repoFetchLocks.set(repoKey,chained),await previous.catch(()=>{});try{return await fn()}finally{releaseCurrent(),repoFetchLocks.get(repoKey)===chained&&repoFetchLocks.delete(repoKey)}}async function fetchAndResolveBaseSha(deps,baseBranch){let validationError2=validateBranchName(baseBranch);if(validationError2)return{ok:!1,error:`Invalid base branch '${baseBranch}': ${validationError2}`};let repoKey=normalizeRepoKey(deps.cwd);return withRepoFetchLock(repoKey,async()=>{let defaultBranch=await resolveRepositoryDefaultBranch(deps),extraRefs=defaultBranch&&defaultBranch!==baseBranch&&validateBranchName(defaultBranch)===null?[defaultBranch]:[],fetch2=await deps.runCommand("git",["fetch","origin",baseBranch,...extraRefs],{cwd:deps.cwd});if(!commandSucceeded(fetch2)&&extraRefs.length>0&&(fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd})),!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`};let resolve2=await deps.runCommand("git",["rev-parse","--verify",`origin/${baseBranch}^{commit}`],{cwd:deps.cwd});return commandSucceeded(resolve2)?{ok:!0,base_sha:resolve2.stdout.trim()}:{ok:!1,error:`Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`}})}var repoFetchLocks,init_base_ref=__esm({"src/base-ref.ts"(){"use strict";init_claude_review_workflow_drift_probe();init_start_tickets_prereqs();init_claude_review_workflow_drift_probe();repoFetchLocks=new Map}});import{execFile}from"child_process";import{readFile as readFile5,writeFile as writeFile3,mkdir as mkdir3,mkdtemp,stat as stat3,lstat,symlink,unlink,copyFile,readdir as readdir2,rm as rm2}from"fs/promises";import os5 from"node:os";import{createHash as createHash4}from"node:crypto";import path20 from"path";import{fileURLToPath as fileURLToPath2}from"node:url";import{existsSync as existsSync2}from"node:fs";import{tmpdir}from"node:os";function appendSummaryRowWarning(row2,warning){return{...row2,warnings:[...row2.warnings??[],warning]}}function getStartTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,"","Flags:"," --agent claude|cursor-agent|codex Agent command to launch in each worktree (overrides --host; default: claude)"," --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions"," Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate."," --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases."," --tier cheap|basic|premium Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available."," --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement"," --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only"," --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use"," --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)"," --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)"," --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility"," --max-parallel N Max worktrees to create concurrently (default: 3)"," --conductor Enable the Conductor system: per-worker hook injection + BAPI_CONDUCTOR_* env, lifecycle ledger events, and a supervisor peer tab (default: off \u2014 a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>'). Implies --guard-stale-branch: a pre-existing ticket branch carrying commits beyond the resolved base is refused, not silently reused."," --guard-stale-branch Refuse to reuse a pre-existing feature/KEY branch whose tip is NOT an ancestor of the resolved base (it carries commits the base does not). The ticket is reported create-failed with the stale-worktree remedy instead of spawning a worker on leftover code. Implied by --conductor; pass it explicitly on the LLM-conductor pilot cut path. Off by default, so a plain interactive run keeps reusing a same-named branch."," -h, --help Show this help","","Environment:",` ${WORKTRUNK_BINARY_OVERRIDE_ENV} Override the Worktrunk executable name/path for nonstandard installs`,` ${TMUX_SESSION_OVERRIDE_ENV} Override the tmux session-name prefix on Linux (default: ${DEFAULT_TMUX_SESSION_PREFIX})`," BAPI_CONDUCTOR_GATE_NAME Conductor gate name for this run (default: implement-ticket)"," BAPI_CONDUCTOR_SUPERVISOR_MODE Conductor supervisor mode (default: auto when --auto, else interactive)"," BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE Set 1/true to also register a PreToolUse conductor hook","","Conductor observability (opt-in via --conductor):"," With --conductor, real Claude Code workers launched by start-tickets receive"," per-worktree conductor hook injection (into .claude/settings.local.json) and emit"," local lifecycle events into the conductor ledger. Each such run mints one run_id"," and attributes worker events by worker_id, ticket key, and worktree path, and a"," supervisor peer tab is opened. Without --conductor none of this happens. Inspect"," the ledger with the `conductor` CLI. The BAPI_CONDUCTOR_* env vars above apply"," only when --conductor is set.","","Prerequisites:"," macOS wt, git, osascript"," Windows git-wt, Git for Windows / Git Bash, Windows Terminal or PowerShell"," Linux wt, git, tmux","",TICKET_KEY_USAGE_SUMMARY].join(`
3746
3746
  `)}function formatStartTicketsAgentNames(){return getWorktreeHostTargets().filter(isRunnableHostTarget).map(target=>target.launchAgent).join(", ")}function parseStartTicketsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getStartTicketsUsage()};let terminal,dryRun=!1,autoApprove=!1,refreshMain=!0,maxParallelRaw,agentName,hostPlatform,baseBranch="main",conductorEnabled=!1,guardStaleBranch=!1,workflow="implement",reviewRoundsRaw,injectedTier,branchEntries=[],keys=[];for(let i=0;i<argv.length;i++){let arg=argv[i],takeValue3=()=>{if(!(i+1>=argv.length))return i+=1,argv[i]};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--agent requires a value (an agent name)."};if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (launchable agents: ${formatStartTicketsAgentNames()}).`};agentName=value;continue}if(arg==="--host"||arg.startsWith("--host=")){let value=arg.startsWith("--host=")?arg.slice(7):takeValue3();if(!value||!isHostPlatformId(value))return{status:"error",message:`Invalid --host value: '${value??""}'.`};hostPlatform=value;continue}if(arg==="--workflow"||arg.startsWith("--workflow=")){let value;if(arg.startsWith("--workflow="))value=arg.slice(11);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--workflow requires a value (allowed values: implement, review-and-implement)."};if(value!=="implement"&&value!=="review-and-implement")return{status:"error",message:`Invalid --workflow value: '${value}' (allowed values: implement, review-and-implement).`};workflow=value;continue}if(arg==="--rounds"||arg.startsWith("--rounds=")){let value;if(arg.startsWith("--rounds="))value=arg.slice(9);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--rounds requires a value (allowed values: 1, 2)."};if(value!=="1"&&value!=="2")return{status:"error",message:`Invalid --rounds value: '${value}' (allowed values: 1, 2).`};reviewRoundsRaw=value;continue}if(arg==="--tier"||arg.startsWith("--tier=")){let value;if(arg.startsWith("--tier="))value=arg.slice(7);else{let next=i+1<argv.length?argv[i+1]:void 0;next!==void 0&&!next.startsWith("-")&&!TICKET_KEY_PATTERN3.test(next)&&(value=takeValue3())}injectedTier=isModelTier(value)?value:INJECTED_TIER_UNRESOLVED;continue}if(arg==="--terminal"||arg.startsWith("--terminal=")){let value;if(arg.startsWith("--terminal="))value=arg.slice(11);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--terminal requires a value (terminal or iterm)."};if(value!=="terminal"&&value!=="iterm")return{status:"error",message:`Invalid --terminal value: '${value}' (allowed values: terminal, iterm).`};terminal=value;continue}if(arg==="--max-parallel"||arg.startsWith("--max-parallel=")){if(arg.startsWith("--max-parallel="))maxParallelRaw=arg.slice(15);else{let value=takeValue3();if(value===void 0)return{status:"error",message:"--max-parallel requires a positive integer value."};maxParallelRaw=value}continue}if(arg==="--branch"||arg.startsWith("--branch=")){let value;if(arg.startsWith("--branch="))value=arg.slice(9);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--branch requires a KEY=BRANCH value."};branchEntries.push(value);continue}if(arg==="--base-branch"||arg.startsWith("--base-branch=")){let value;if(arg.startsWith("--base-branch="))value=arg.slice(14);else{let next=i+1<argv.length?argv[i+1]:void 0;if(next===void 0||next.startsWith("-"))return{status:"error",message:"--base-branch requires a value (a branch name)."};value=takeValue3()}let trimmed=(value??"").trim(),error=validateBranchName(trimmed);if(error)return{status:"error",message:`Invalid --base-branch value: ${error}`};baseBranch=trimmed;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--auto"){autoApprove=!0;continue}if(arg==="--conductor"){conductorEnabled=!0;continue}if(arg==="--guard-stale-branch"){guardStaleBranch=!0;continue}if(arg==="--no-refresh-main"){refreshMain=!1;continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};keys.push(arg)}if(keys.length===0)return{status:"error",message:"At least one ticket key is required (e.g., BAPI-248)."};let seen=new Set;for(let key of keys){let keyCheck=validateTicketKey(key);if(!keyCheck.ok)return{status:"error",message:keyCheck.message};if(seen.has(key))return{status:"error",message:`Duplicate ticket key: '${key}'.`};seen.add(key)}let maxParallel=DEFAULT_MAX_PARALLEL;if(maxParallelRaw!==void 0){if(!/^[0-9]+$/.test(maxParallelRaw)||Number(maxParallelRaw)<1)return{status:"error",message:`Invalid --max-parallel value: '${maxParallelRaw}' (must be a positive integer).`};maxParallel=Number(maxParallelRaw)}let branchOverrides={};for(let entry of branchEntries){let sepIndex=entry.indexOf("=");if(sepIndex<=0)return{status:"error",message:`Invalid --branch override: '${entry}' (expected KEY=BRANCH).`};let overrideKey=entry.slice(0,sepIndex),branchName=entry.slice(sepIndex+1),overrideCheck=validateTicketKey(overrideKey,"--branch override key");if(!overrideCheck.ok)return{status:"error",message:overrideCheck.message};if(!seen.has(overrideKey))return{status:"error",message:`--branch override key '${overrideKey}' is not one of the requested tickets.`};let branchError=validateBranchName(branchName);if(branchError)return{status:"error",message:`Invalid branch name for ${overrideKey}: ${branchError}`};branchOverrides[overrideKey]=branchName}let reviewRounds;if(reviewRoundsRaw!==void 0){if(workflow!=="review-and-implement")return{status:"error",message:"--rounds is only valid with --workflow review-and-implement."};reviewRounds=reviewRoundsRaw==="1"?1:2}let target=hostPlatform?getHostTarget(hostPlatform):void 0;if(!agentName&&target&&!isRunnableHostTarget(target))return{status:"error",message:`${target.label} supports MCP configuration but cannot spawn a Bridge session. Continue the workflow manually or pass an explicit --agent override.`};let resolvedAgentName=agentName??(hostPlatform?agentForPlatform(hostPlatform):DEFAULT_AGENT_NAME);return{status:"ok",options:{keys,terminal,dryRun,autoApprove,refreshMain,maxParallel,branchOverrides,agentName:resolvedAgentName,baseBranch,conductorEnabled,workflow,reviewRounds,...guardStaleBranch||conductorEnabled?{guardStaleWorktree:!0}:{},...injectedTier!==void 0?{injectedTier}:{}}}}function detectTerminal(explicit,env){return explicit||((env.TERM_PROGRAM??"").toLowerCase().includes("iterm")?"iterm":"terminal")}function getDefaultSpawnTerminalTabForPlatform(platform){switch(platform){case"darwin":return spawnMacOSTerminalTab;case"win32":return spawnWindowsTerminalTab;case"linux":return spawnLinuxTmuxTerminalTab;default:return spawnUnsupportedPlatformTerminalTab}}function resolveStartTicketsPlatformConfig(deps,agent,autoApprove=!1,conductorEnabled=!1,repoName=null,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,bridgeApiBaseUrlOverride){if(!isSupportedStartTicketsPlatform(deps.platform))return{ok:!1,error:unsupportedPlatformMessage(deps.platform)};let platform=deps.platform,prBaseBranch=resolvePrBaseBranchEnvValue(conductorEnabled,baseBranch),bridgeApiBaseUrl=bridgeApiBaseUrlOverride??resolveSpawnBridgeApiBaseUrl(deps.env);return{ok:!0,config:{platform,worktrunkBinary:resolveWorktrunkBinary(platform,deps.env),buildAgentShellCommand:(key,worktreePath,modelAlias,launchArgs)=>prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,launchArgs,bridgeApiBaseUrl),repoName,platform),prBaseBranch,platform),spawnTerminalTab:deps.spawnTerminalTab}}}function prependRepoNameEnvAssignment(command,repoName,platform="darwin"){return repoName?platform==="win32"?`$env:BAPI_REPO_NAME = ${powershellSquote(repoName)}; ${command}`:`export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`:command}function prependBridgeApiBaseUrlEnvAssignment(command,baseUrl,platform="darwin"){return baseUrl?platform==="win32"?`$env:BAPI_BASE_URL = ${powershellSquote(baseUrl)}; ${command}`:`export BAPI_BASE_URL='${shSquoteInner(baseUrl)}' && ${command}`:command}function resolveSpawnBridgeApiBaseUrl(env){let raw=env.BAPI_BASE_URL;if(!raw||!raw.trim())return;let result=validateHttpBaseUrl(raw);return result.ok?result.baseUrl:void 0}async function resolveStartTicketsBridgeApiBaseUrl(deps,agent){let inherited=resolveSpawnBridgeApiBaseUrl(deps.env);if(inherited)return inherited;let configPath=agent.name==="cursor-agent"?path20.join(deps.cwd,".cursor","mcp.json"):path20.join(deps.cwd,".mcp.json");try{let raw=await(deps.readFile??(filePath=>readFile5(filePath,"utf-8")))(configPath),parsed=JSON.parse(raw),bridge=parsed.mcpServers?.[MCP_SERVER_NAME]??parsed.mcpServers?.[LEGACY_SERVER_NAMES[0]];if(!bridge)return;let args=Array.isArray(bridge.args)?bridge.args:[],flagIndex=args.indexOf("--base-url"),candidate=bridge.env?.BAPI_BASE_URL??(flagIndex>=0?args[flagIndex+1]:void 0),validated=validateHttpBaseUrl(candidate);return validated.ok?validated.baseUrl:void 0}catch{return}}function resolvePrBaseBranchEnvValue(conductorEnabled,baseBranch){return conductorEnabled?baseBranch??null:!baseBranch||!baseBranch.trim()?null:baseBranch}function prependBaseBranchEnvAssignment(command,baseBranch,platform="darwin"){return baseBranch?platform==="win32"?`$env:${PR_BASE_BRANCH_ENV_VAR} = ${powershellSquote(baseBranch)}; ${command}`:`export ${PR_BASE_BRANCH_ENV_VAR}='${shSquoteInner(baseBranch)}' && ${command}`:command}function shSquoteInner(value){return value.replace(/'/g,"'\\''")}function applescriptDquoteInner(value){return value.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function powershellSquoteInner(value){return value.replace(/'/g,"''")}function powershellSquote(value){return`'${powershellSquoteInner(value)}'`}function createDefaultStartTicketsDeps(){return{runCommand:(file,args,options)=>new Promise(resolve2=>{execFile(file,args,{cwd:options?.cwd,maxBuffer:67108864,encoding:"utf-8",timeout:options?.timeoutMs},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})}),platform:process.platform,env:process.env,cwd:process.cwd(),spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),writeWorkerLaunchScript:defaultWriteWorkerLaunchScript}}function combineCommandOutput(result){return[result.stderr,result.stdout].map(s=>s.trim()).filter(Boolean).join(" ")}async function runPreflight(deps,options,warn=message=>console.warn(message)){if(options.dryRun)return{ok:!0};let enforceLiveSourceGuard=options.nonMutatingBase===!0||options.epic!==void 0,result=await enforcePreflightPrerequisites(deps,{enforceLiveSourceGuard});if(result.ok){if(options.agentName==="cursor-agent"&&options.autoApprove){let cursorReady=await preflightHeadlessCursor(deps);if(!cursorReady.ok)return cursorReady}return result.warning&&warn(result.warning),{ok:!0}}return result.reason==="unsupported-platform"?{ok:!1,error:result.error}:{ok:!1,error:appendDoctorHint(result.error)}}async function preflightHeadlessCursor(deps){let help;try{help=await deps.runCommand("cursor-agent",["--help"],{timeoutMs:5e3})}catch{return{ok:!1,error:"Cursor Agent is unavailable. Install cursor-agent and ensure it is on PATH."}}if(help.exitCode!==0)return{ok:!1,error:"Cursor Agent is unavailable. Install cursor-agent and ensure it is on PATH."};for(let flag of["--print","--output-format","--trust","--workspace","--force","--approve-mcps"])if(!help.stdout.includes(flag))return{ok:!1,error:`Cursor Agent does not support ${flag}. Update cursor-agent before using --auto.`};let probeDir;try{probeDir=await mkdtemp(path20.join(tmpdir(),"bridge-cursor-ready-"))}catch{return{ok:!1,error:"Could not create a temporary workspace for the Cursor Agent readiness check."}}try{let probe=await deps.runCommand("cursor-agent",["-p","--output-format","text","--trust","--workspace",probeDir,"Reply with exactly BAPI_CURSOR_READY. Do not use tools."],{cwd:probeDir,timeoutMs:6e4});return probe.exitCode!==0||!probe.stdout.includes("BAPI_CURSOR_READY")?{ok:!1,error:"Cursor Agent could not complete a headless authentication check. Run cursor-agent login or configure CURSOR_API_KEY, then retry."}:{ok:!0}}catch{return{ok:!1,error:"Cursor Agent headless readiness check failed. Verify authentication and retry."}}finally{try{await rm2(probeDir,{recursive:!0,force:!0})}catch{}}}function parseGitWorktreeList(output){let entries=[],current=null;for(let rawLine of output.split(`
3747
- `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&&current){let ref=line.slice(7);current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}function findBaseWorktreePath(entries,baseBranch){for(let entry of entries)if(entry.branch===baseBranch)return entry.path;return null}async function refreshBaseBranch(deps,options){if(!options.refreshMain)return{ok:!0};let baseBranch=options.baseBranch,originRef=`origin/${baseBranch}`,defaultBranch=await resolveRepositoryDefaultBranch(deps),extraRefs=defaultBranch&&defaultBranch!==baseBranch&&validateBranchName(defaultBranch)===null?[defaultBranch]:[],fetch2=await deps.runCommand("git",["fetch","origin",baseBranch,...extraRefs],{cwd:deps.cwd});if(!commandSucceeded(fetch2)&&extraRefs.length>0&&(fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd})),!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`};let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return{ok:!1,error:`git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`};let basePath=findBaseWorktreePath(parseGitWorktreeList(list.stdout),baseBranch);if(basePath){let merge=await deps.runCommand("git",["merge","--ff-only",originRef],{cwd:basePath});return commandSucceeded(merge)?{ok:!0}:{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`}}if(await branchExists(deps,baseBranch)){let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",baseBranch,originRef],{cwd:deps.cwd});if(!commandSucceeded(ancestor))return{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`}}let update=await deps.runCommand("git",["branch","--force",baseBranch,originRef],{cwd:deps.cwd});return commandSucceeded(update)?{ok:!0}:{ok:!1,error:`Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`}}async function runWithConcurrency(items,limit,worker){let results=new Array(items.length),effectiveLimit=Math.max(1,Math.floor(limit)),nextIndex=0;async function runner(){for(;;){let index=nextIndex;if(index>=items.length)return;nextIndex+=1,results[index]=await worker(items[index],index)}}let runners=[],poolSize=Math.min(effectiveLimit,items.length);for(let i=0;i<poolSize;i++)runners.push(runner());return await Promise.all(runners),results}async function emitClaudeReviewWorkflowDriftAdvisory(deps,baseBranch,overrides){let warn=overrides.workflowDriftWarningLog??(m=>console.error(m));try{let probe=overrides.probeWorkflowDrift??probeClaudeReviewWorkflowDrift,defaultRef=await resolveRepositoryDefaultBranch(deps),classification=await probe(deps,{baseRef:baseBranch,defaultRef});if(classification.state==="aligned")return;warn(formatClaudeReviewWorkflowDriftDiagnostic(classification))}catch{}}async function createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{},link=deps.linkOperatorEnvFiles??linkOperatorEnvFiles,linkDeps=buildEnvFileLinkDeps(deps);return runWithConcurrency(options.keys,options.maxParallel,async key=>{let row2=await createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior);if(row2.status!=="created"||typeof row2.path!="string"||row2.path.length===0)return row2;try{return(await link(deps.cwd,row2.path,linkDeps)).warnings.reduce((acc,warning)=>appendSummaryRowWarning(acc,warning.message),row2)}catch{return appendSummaryRowWarning(row2,"worktree environment files: could not be provided from the main checkout, so this worktree has none. Commands that need them will fail until they are linked by hand.")}})}function buildEnvFileLinkDeps(deps){let metadata=probe=>async target=>{try{return await probe(target)}catch{return null}};return{stat:metadata(target=>stat3(target)),lstat:metadata(target=>lstat(target)),symlink:(target,linkPath)=>symlink(target,linkPath),unlink:target=>unlink(target),copyFile:(source,destination)=>copyFile(source,destination),platform:deps.platform}}async function resumeWorktrees(deps,options){let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return options.keys.map(key=>({key,branch:resolveBranchForTicket(key,options.branchOverrides),status:"create-failed",error:"resume mode: git worktree list --porcelain failed; cannot locate existing worktree."}));let entries=parseGitWorktreeList(list.stdout);return options.keys.map(key=>{let branch=resolveBranchForTicket(key,options.branchOverrides),entry=entries.find(e=>e.branch===branch);if(!entry){let needle=key.toLowerCase();entry=entries.find(e=>(e.branch??"").toLowerCase().includes(needle))}return entry?{key,branch:entry.branch??branch,status:"created",path:entry.path}:{key,branch,status:"create-failed",error:`resume mode: no existing worktree found for ticket ${key} (branch '${branch}').`}})}function buildResumeModeRemediationFinalizeInstruction(){return"Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket (a merge conflict, a CI failure, or requested review changes). First rebase against the current base branch and resolve the merge conflicts. A clean textual merge can still break behavior, so inspect for semantic conflicts even when there are no textual conflict markers. Before you push or mark the ticket complete, run the full test suite for the project (the full unit suite, the same gate enforced by the advisory pre-push hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) and do not rely on targeted subsets as your only verification. Push and mark the ticket complete only after the full suite is green. If you cannot make the full suite pass, report the ticket blocked and escalate rather than pushing a green-looking but broken merge."}function buildAgentPrompt(key,opts={}){let workflow=opts.workflow??"implement",commandName=workflow==="review-and-implement"?"review-and-implement":"implement-ticket",command=`${opts.agentName==="codex"?`$bridge-${commandName}`:`/${commandName}`} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.agentName==="cursor-agent"&&(command+=" --agent=cursor-agent"),opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`),opts.agentName==="codex"&&(command+=" --agent codex --host codex"));let parts=[command];return opts.conductorEnabled&&parts.push(buildPrBaseContractLaunchInstruction()),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias,launchArgsOrCursorWorkspace=[]){let argv=[agent.command];return agent.name==="codex"&&Array.isArray(launchArgsOrCursorWorkspace)&&argv.push(...launchArgsOrCursorWorkspace),agent.name==="cursor-agent"&&typeof launchArgsOrCursorWorkspace=="string"&&launchArgsOrCursorWorkspace&&argv.push("-p","--output-format","text","--trust","--approve-mcps","--force","--workspace",launchArgsOrCursorWorkspace),agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias,launchArgsOrCursorWorkspace=[]){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias,launchArgsOrCursorWorkspace),quotedRest=rest.map(quote);return[command,...quotedRest].join(" ")}else{let exhaustive=agent.promptArgStyle;throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`)}}function buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,agentName:agent.name}),p=>`'${shSquoteInner(p)}'`,modelAlias,agent.name==="cursor-agent"&&autoApprove?worktreePath:launchArgs);return`cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation,bridgeApiBaseUrl,"darwin")}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,agentName:agent.name}),powershellSquote,modelAlias,agent.name==="cursor-agent"&&autoApprove?worktreePath:launchArgs);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation,bridgeApiBaseUrl,"win32")}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,launchArgs,bridgeApiBaseUrl):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,launchArgs,bridgeApiBaseUrl)}function buildGenericAgentShellCommand(agent,prompt,cwd,platform="darwin",modelAlias){if(platform==="win32"){let invocation2=buildAgentInvocation(agent,prompt,powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(cwd)}; ${invocation2}`}let invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function terminalTitleForTicket(key){return`${key} Implementation`}function buildTerminalAppleScript(shellCommand,title){let esc=applescriptDquoteInner(shellCommand),titleEsc=applescriptDquoteInner(title);return['tell application "Terminal"'," activate"," if (count of windows) is 0 then",` set spawnedTab to do script "${esc}"`," else",' tell application "System Events" to keystroke "t" using command down'," delay 0.2",` set spawnedTab to do script "${esc}" in selected tab of front window`," end if",` set custom title of spawnedTab to "${titleEsc}"`,"end tell"].join(`
3747
+ `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&&current){let ref=line.slice(7);current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}function findBaseWorktreePath(entries,baseBranch){for(let entry of entries)if(entry.branch===baseBranch)return entry.path;return null}async function refreshBaseBranch(deps,options){if(!options.refreshMain)return{ok:!0};let baseBranch=options.baseBranch,originRef=`origin/${baseBranch}`,defaultBranch=await resolveRepositoryDefaultBranch(deps),extraRefs=defaultBranch&&defaultBranch!==baseBranch&&validateBranchName(defaultBranch)===null?[defaultBranch]:[],fetch2=await deps.runCommand("git",["fetch","origin",baseBranch,...extraRefs],{cwd:deps.cwd});if(!commandSucceeded(fetch2)&&extraRefs.length>0&&(fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd})),!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`};let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return{ok:!1,error:`git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`};let basePath=findBaseWorktreePath(parseGitWorktreeList(list.stdout),baseBranch);if(basePath){let merge=await deps.runCommand("git",["merge","--ff-only",originRef],{cwd:basePath});return commandSucceeded(merge)?{ok:!0}:{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`}}if(await branchExists(deps,baseBranch)){let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",baseBranch,originRef],{cwd:deps.cwd});if(!commandSucceeded(ancestor))return{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`}}let update=await deps.runCommand("git",["branch","--force",baseBranch,originRef],{cwd:deps.cwd});return commandSucceeded(update)?{ok:!0}:{ok:!1,error:`Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`}}async function runWithConcurrency(items,limit,worker){let results=new Array(items.length),effectiveLimit=Math.max(1,Math.floor(limit)),nextIndex=0;async function runner(){for(;;){let index=nextIndex;if(index>=items.length)return;nextIndex+=1,results[index]=await worker(items[index],index)}}let runners=[],poolSize=Math.min(effectiveLimit,items.length);for(let i=0;i<poolSize;i++)runners.push(runner());return await Promise.all(runners),results}async function emitClaudeReviewWorkflowDriftAdvisory(deps,baseBranch,overrides){let warn=overrides.workflowDriftWarningLog??(m=>console.error(m));try{let probe=overrides.probeWorkflowDrift??probeClaudeReviewWorkflowDrift,defaultRef=await resolveRepositoryDefaultBranch(deps),classification=await probe(deps,{baseRef:baseBranch,defaultRef});if(classification.state==="aligned")return;warn(formatClaudeReviewWorkflowDriftDiagnostic(classification))}catch{}}async function createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{},link=deps.linkOperatorEnvFiles??linkOperatorEnvFiles,linkDeps=buildEnvFileLinkDeps(deps);return runWithConcurrency(options.keys,options.maxParallel,async key=>{let row2=await createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior);if(row2.status!=="created"||typeof row2.path!="string"||row2.path.length===0)return row2;try{return(await link(deps.cwd,row2.path,linkDeps)).warnings.reduce((acc,warning)=>appendSummaryRowWarning(acc,warning.message),row2)}catch{return appendSummaryRowWarning(row2,"worktree environment files: could not be provided from the main checkout, so this worktree has none. Commands that need them will fail until they are linked by hand.")}})}function buildEnvFileLinkDeps(deps){let metadata=probe=>async target=>{try{return await probe(target)}catch{return null}};return{stat:metadata(target=>stat3(target)),lstat:metadata(target=>lstat(target)),symlink:(target,linkPath)=>symlink(target,linkPath),unlink:target=>unlink(target),copyFile:(source,destination)=>copyFile(source,destination),platform:deps.platform}}async function resumeWorktrees(deps,options){let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return options.keys.map(key=>({key,branch:resolveBranchForTicket(key,options.branchOverrides),status:"create-failed",error:"resume mode: git worktree list --porcelain failed; cannot locate existing worktree."}));let entries=parseGitWorktreeList(list.stdout);return options.keys.map(key=>{let branch=resolveBranchForTicket(key,options.branchOverrides),entry=entries.find(e=>e.branch===branch);if(!entry){let needle=key.toLowerCase();entry=entries.find(e=>(e.branch??"").toLowerCase().includes(needle))}return entry?{key,branch:entry.branch??branch,status:"created",path:entry.path}:{key,branch,status:"create-failed",error:`resume mode: no existing worktree found for ticket ${key} (branch '${branch}').`}})}function buildResumeModeRemediationFinalizeInstruction(){return"Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket (a merge conflict, a CI failure, or requested review changes). First rebase against the current base branch and resolve the merge conflicts. A clean textual merge can still break behavior, so inspect for semantic conflicts even when there are no textual conflict markers. Before you push or mark the ticket complete, run the full test suite for the project (the full unit suite, the same gate enforced by the advisory pre-push hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) and do not rely on targeted subsets as your only verification. Push and mark the ticket complete only after the full suite is green. If you cannot make the full suite pass, report the ticket blocked and escalate rather than pushing a green-looking but broken merge."}function buildAgentPrompt(key,opts={}){let workflow=opts.workflow??"implement",commandName=workflow==="review-and-implement"?"review-and-implement":"implement-ticket",command=`${opts.agentName==="codex"?`$bridge-${commandName}`:`/${commandName}`} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.agentName==="cursor-agent"&&(command+=" --agent=cursor-agent"),opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`),opts.agentName==="codex"&&(command+=" --agent codex --host codex"));let parts=[command];return opts.conductorEnabled&&parts.push(buildPrBaseContractLaunchInstruction()),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias,launchArgsOrCursorWorkspace=[]){let argv=[agent.command];if(agent.name==="codex"&&Array.isArray(launchArgsOrCursorWorkspace)&&argv.push(...launchArgsOrCursorWorkspace),agent.name==="cursor-agent"&&typeof launchArgsOrCursorWorkspace=="string"&&launchArgsOrCursorWorkspace){let progressScript=fileURLToPath2(new URL("./cursor-agent-progress.js",import.meta.url));argv.splice(0,1,"node",progressScript,"--",agent.command),argv.push("-p","--output-format","stream-json","--trust","--approve-mcps","--force","--workspace",launchArgsOrCursorWorkspace)}return agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias,launchArgsOrCursorWorkspace=[]){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias,launchArgsOrCursorWorkspace),quotedRest=rest.map(quote);return[command,...quotedRest].join(" ")}else{let exhaustive=agent.promptArgStyle;throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`)}}function buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,agentName:agent.name}),p=>`'${shSquoteInner(p)}'`,modelAlias,agent.name==="cursor-agent"&&autoApprove?worktreePath:launchArgs);return`cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation,bridgeApiBaseUrl,"darwin")}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,agentName:agent.name}),powershellSquote,modelAlias,agent.name==="cursor-agent"&&autoApprove?worktreePath:launchArgs);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation,bridgeApiBaseUrl,"win32")}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch,launchArgs=[],bridgeApiBaseUrl){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,launchArgs,bridgeApiBaseUrl):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch,launchArgs,bridgeApiBaseUrl)}function buildGenericAgentShellCommand(agent,prompt,cwd,platform="darwin",modelAlias){if(platform==="win32"){let invocation2=buildAgentInvocation(agent,prompt,powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(cwd)}; ${invocation2}`}let invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function terminalTitleForTicket(key){return`${key} Implementation`}function buildTerminalAppleScript(shellCommand,title){let esc=applescriptDquoteInner(shellCommand),titleEsc=applescriptDquoteInner(title);return['tell application "Terminal"'," activate"," if (count of windows) is 0 then",` set spawnedTab to do script "${esc}"`," else",' tell application "System Events" to keystroke "t" using command down'," delay 0.2",` set spawnedTab to do script "${esc}" in selected tab of front window`," end if",` set custom title of spawnedTab to "${titleEsc}"`,"end tell"].join(`
3748
3748
  `)}function itermBadgeShellCommand(badgeText){return`printf '\\033]1337;SetBadgeFormat=%s\\007' '${Buffer.from(badgeText,"utf8").toString("base64")}'`}function buildITermAppleScript(shellCommand,title,badgeText){let esc=applescriptDquoteInner(shellCommand),lines=['tell application "iTerm"'," activate"," if (count of windows) = 0 then"," set spawnedSession to current session of (create window with default profile)"," else"," tell current window to set spawnedSession to (current session of (create tab with default profile))"," end if"," tell spawnedSession",` set name to "${applescriptDquoteInner(title)}"`];if(badgeText){let badgeEsc=applescriptDquoteInner(itermBadgeShellCommand(badgeText));lines.push(` write text "${badgeEsc}"`)}return lines.push(` write text "${esc}"`),lines.push(" end tell"),lines.push("end tell"),lines.join(`
3749
3749
  `)}async function spawnMacOSTerminalTab(deps,terminal,shellCommand,context){let title=context?.title??terminalTitleForTicket(context?.key??""),badgeText=context?.title??(context?.key||void 0),script=terminal==="iterm"?buildITermAppleScript(shellCommand,title,badgeText):buildTerminalAppleScript(shellCommand,title),result=await deps.runCommand("osascript",["-e",script]);if(commandSucceeded(result))return{ok:!0};let reason=(result.stderr||result.stdout||"").trim();return{ok:!1,error:`osascript failed to open a ${terminal} tab${reason?`: ${reason}`:""}`}}function buildWindowsTerminalArgs(worktreePath,shellCommand,title){let wtEscapedCommand=shellCommand.replace(/;/g,"\\;");return["new-tab","--title",title,"--suppressApplicationTitle","-d",worktreePath,"powershell.exe","-ExecutionPolicy","Bypass","-NoExit","-Command",wtEscapedCommand]}function buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title){let titledCommand=`$host.UI.RawUI.WindowTitle = ${powershellSquote(title)}; ${shellCommand}`,argumentList=`@('-ExecutionPolicy', 'Bypass', '-NoExit', '-Command', ${powershellSquote(titledCommand)})`;return`Start-Process -FilePath 'powershell.exe' -WorkingDirectory ${powershellSquote(worktreePath)} -ArgumentList ${argumentList}`}async function spawnWindowsTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath;if(!worktreePath)return{ok:!1,error:"Windows spawner requires a worktreePath context to open a tab."};let title=context?.title??terminalTitleForTicket(context?.key??"");if(await isCommandOnPath(deps,WINDOWS_TERMINAL_COMMAND)){let args=buildWindowsTerminalArgs(worktreePath,shellCommand,title),result2=await deps.runCommand(WINDOWS_TERMINAL_COMMAND,args);if(commandSucceeded(result2))return{ok:!0};let reason2=combineCommandOutput(result2);return{ok:!1,error:`wt.exe failed to open a Windows Terminal tab${reason2?`: ${reason2}`:""}`}}let powershell=await resolveFirstCommandOnPath(deps,WINDOWS_POWERSHELL_CANDIDATES);if(!powershell)return{ok:!1,error:"Windows Terminal (wt.exe) or PowerShell is required to open a tab, but neither was found on PATH."};let fallback=buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title),result=await deps.runCommand(powershell,["-NoProfile","-ExecutionPolicy","Bypass","-Command",fallback]);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`PowerShell failed to open a window via Start-Process${reason?`: ${reason}`:""}`}}function sanitizeTmuxName(value){let cleaned=value.replace(/[^A-Za-z0-9_-]+/g,"-").replace(/^-+/,"").replace(/-+$/,"");return cleaned.length>0?cleaned:"ticket"}function tmuxWindowNameForTicket(key){return terminalTitleForTicket(sanitizeTmuxName(key))}function tmuxWindowLabelFromTitle(title){let cleaned=title.replace(/[.:]+/g," ").replace(/\s+/g," ").trim();return cleaned.length>0?cleaned:"session"}function tmuxSessionPrefix(deps){let override=deps.env[TMUX_SESSION_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return DEFAULT_TMUX_SESSION_PREFIX}function tmuxSessionNameForTicket(deps,key){return`${tmuxSessionPrefix(deps)}-${sanitizeTmuxName(key)}`}function buildTmuxPaneCommand(shellCommand){return`${shellCommand}; exec $SHELL`}function buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand){return["new-session","-d","-s",session,"-n",window,"-c",worktreePath,paneCommand]}function buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand){return["new-window","-t",session,"-n",window,"-c",worktreePath,paneCommand]}async function spawnLinuxTmuxTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath,key=context?.key;if(!worktreePath||!key)return{ok:!1,error:"Linux tmux spawner requires a worktreePath context to open a session."};if(!await isCommandOnPath(deps,TMUX_COMMAND))return{ok:!1,error:"tmux is required to spawn Linux sessions but was not found on PATH. Install tmux and retry."};let session=tmuxSessionNameForTicket(deps,key),window=context?.title?tmuxWindowLabelFromTitle(context.title):tmuxWindowNameForTicket(key),paneCommand=buildTmuxPaneCommand(shellCommand),hasSession=await deps.runCommand(TMUX_COMMAND,["has-session","-t",session]),args=commandSucceeded(hasSession)?buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand):buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand),result=await deps.runCommand(TMUX_COMMAND,args);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`tmux failed to create a session/window${reason?`: ${reason}`:""}`}}async function spawnUnsupportedPlatformTerminalTab(deps,_terminal,_shellCommand,_context){return{ok:!1,error:unsupportedPlatformMessage(deps.platform)}}function sanitizeKeyForLaunchScript(key){let cleaned=key.replace(/[^A-Za-z0-9._-]/g,"_");return cleaned.length>0?cleaned:"worker"}function buildLaunchScriptContent(platform,fullCommand){return platform==="win32"?`${fullCommand}
3750
3750
  `:`#!/usr/bin/env bash
3751
3751
  ${fullCommand}
3752
3752
  `}function buildLaunchScriptRunnerCommand(platform,scriptPath){return platform==="win32"?`. ${powershellSquote(scriptPath)}`:`. '${shSquoteInner(scriptPath)}'`}async function pruneStaleLaunchScripts(deps=defaultPruneStaleLaunchScriptsDeps){try{let parent=path20.join(os5.tmpdir(),"bridge-start-tickets"),entries;try{entries=await deps.readdir(parent)}catch{return}let cutoff=deps.now()-STALE_LAUNCH_SCRIPT_MAX_AGE_MS;for(let entry of entries){if(!entry.startsWith("w-"))continue;let full=path20.join(parent,entry);try{(await deps.stat(full)).mtimeMs<cutoff&&await deps.rm(full,{recursive:!0,force:!0})}catch{}}}catch{}}async function materializeWorkerLaunchCommand(deps,key,fullCommand){if(!deps.writeWorkerLaunchScript)return{ok:!0,command:fullCommand};try{let scriptPath=await deps.writeWorkerLaunchScript({platform:deps.platform,key,content:buildLaunchScriptContent(deps.platform,fullCommand)});return{ok:!0,command:buildLaunchScriptRunnerCommand(deps.platform,scriptPath)}}catch{return Buffer.byteLength(fullCommand,"utf8")<=MAX_TERMINAL_COMMAND_BYTES?{ok:!0,command:fullCommand}:{ok:!1,reason:"launch-script-write-failed-oversized-command",error:"Could not write the temporary launch script, and the full command is too long to send to the terminal directly. Check that the system temporary directory is writable."}}}function buildRowSpawnEnv(row2){if(!row2.conductorEnv&&!row2.indexScope)return;let env={...row2.conductorEnv};return row2.indexScope&&(env[INDEX_SCOPE_ENV_VAR]=row2.indexScope),env}function stampIndexScopeOnRows(rows,indexScope){return indexScope?rows.map(row2=>row2.status==="created"&&row2.path?{...row2,indexScope}:row2):rows}async function spawnTabsForCreatedWorktrees(deps,rows,terminal,buildShellCommand){let out=[];for(let row2 of rows){if(row2.status!=="created"||!row2.path){out.push(row2);continue}let baseShellCommand=buildShellCommand(row2.key,row2.path,row2.modelAlias??null,row2.codexLaunchArgs),shellCommand=injectConductorEnvIntoShellCommand(deps.platform,baseShellCommand,buildRowSpawnEnv(row2)),materialized=await materializeWorkerLaunchCommand(deps,row2.key,shellCommand);if(!materialized.ok){out.push({...row2,status:"spawn-failed",error:materialized.error});continue}let result=await deps.spawnTerminalTab(deps,terminal,materialized.command,{key:row2.key,worktreePath:row2.path});result.ok?out.push({...row2,status:"spawned"}):out.push({...row2,status:"spawn-failed",error:result.error})}return out}function buildDryRunResults(keys,overrides){return keys.map(key=>({key,branch:resolveBranchForTicket(key,overrides),status:"dry-run"}))}function getDryRunPlatformDetails(agent,platform=process.platform,env=process.env,autoApprove=!1,conductorEnabled=!1,repoName=null,workflow="implement",reviewRounds,baseBranch){return{worktrunkBinary:resolveWorktrunkBinary(platform,env),buildAgentShellCommand:(key,worktreePath,modelAlias,launchArgs)=>prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,!1,workflow,reviewRounds,baseBranch,launchArgs,resolveSpawnBridgeApiBaseUrl(env)),repoName,platform),resolvePrBaseBranchEnvValue(conductorEnabled,baseBranch),platform)}}function buildDryRunMcpProvisioningLines(worktreePath,platform=process.platform,mcpServerInvocation,bridgeApiBaseUrl){let api=platform==="win32"?path20.win32:path20.posix,mcpJson=api.join(worktreePath,".mcp.json"),cursorJson=api.join(worktreePath,".cursor","mcp.json"),invocation=mcpServerInvocation??{form:"npm-channel",command:"npx",packageSpec:`${MCP_PACKAGE_NAME}@latest`},built=buildMcpShimCommand(invocation,"<target>",worktreePath),shim=`${built.command} ${built.args.join(" ")}`;return["DRY-RUN: MCP provisioning (target-driven from .bridge/config \u2014 bapi plus any","DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim","DRY-RUN: entry per target to",`DRY-RUN: ${mcpJson}`,`DRY-RUN: ${cursorJson}`,`DRY-RUN: ${shim}`,...bridgeApiBaseUrl?[`DRY-RUN: bapi shim adds --base-url ${bridgeApiBaseUrl}`]:[]]}function buildDryRunDetailLines(agent,key,branch,platform=process.platform,env=process.env,baseBranch="main",autoApprove=!1,modelAlias=null,conductorEnabled=!1,repoName=null,mcpServerInvocation,workflow="implement",reviewRounds){let{worktrunkBinary,buildAgentShellCommand:build}=getDryRunPlatformDetails(agent,platform,env,autoApprove,conductorEnabled,repoName,workflow,reviewRounds,baseBranch),wtArgs=buildWtSwitchArgs(branch,!1,baseBranch),agentInvocation=build(key,"<worktree-path>",modelAlias),prBaseBranch=resolvePrBaseBranchEnvValue(conductorEnabled,baseBranch);return[`DRY-RUN: ${key} -> branch=${branch}`,`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,`DRY-RUN: ${agentInvocation}`,...prBaseBranch?[`DRY-RUN: PR base: ${prBaseBranch}`]:[],...buildDryRunMcpProvisioningLines("<worktree-path>",platform,mcpServerInvocation,resolveSpawnBridgeApiBaseUrl(env))]}function formatSummaryReport(rows){let lines=["Summary:"],runId=rows.find(r=>r.runId)?.runId;runId&&lines.push(`run_id=${runId}`);let supervisorStatus=rows.find(r=>r.supervisorStatus)?.supervisorStatus;supervisorStatus&&lines.push(`supervisor=${supervisorStatus}`);for(let row2 of rows){let line=`${row2.key} branch=${row2.branch} status=${row2.status}`;row2.path&&(line+=` path=${row2.path}`),row2.workerId&&(line+=` worker_id=${row2.workerId}`),row2.mcpRegistrationForm&&(line+=` mcp_registration=${row2.mcpRegistrationForm}`),lines.push(line)}let warningLines=[];for(let row2 of rows){let messages=[];(row2.status==="create-failed"||row2.status==="spawn-failed")&&messages.push(row2.error??row2.status);for(let warning of row2.warnings??[])messages.push(warning);let seen=new Set;for(let message of messages)seen.has(message)||(seen.add(message),warningLines.push(` ${row2.key}: ${message}`))}return warningLines.length>0&&(lines.push(""),lines.push("Warnings:"),lines.push(...warningLines)),lines.join(`
3753
3753
  `)}function buildMcpProvisioningDeps(deps,bridgeApiBaseUrlOverride){return{env:deps.env,readFile:deps.readFile??(filePath=>readFile5(filePath,"utf-8")),writeFile:deps.writeFile??((filePath,data)=>writeFile3(filePath,data,"utf-8")),mkdir:deps.mkdir??((dirPath,options)=>mkdir3(dirPath,options)),platform:deps.platform,cwd:deps.cwd,bridgeApiBaseUrl:bridgeApiBaseUrlOverride??resolveSpawnBridgeApiBaseUrl(deps.env),mcpServerInvocation:resolveMcpShimInvocationForRuntime({moduleUrl:import.meta.url,nodeExecutable:"node",argv1:process.argv[1],fileExists:existsSync2})}}function buildCommandProvisioningDeps(deps){return{readFile:deps.readFile??(filePath=>readFile5(filePath,"utf-8")),writeFile:deps.writeFile??((filePath,data)=>writeFile3(filePath,data,"utf-8")),mkdir:deps.mkdir??((dirPath,options)=>mkdir3(dirPath,options)),runCommand:deps.runCommand,platform:deps.platform,cwd:deps.cwd}}async function materializeFileCredentialsForCreatedWorktrees(rows,_deps){return rows}function diagnosticReportingPolicyForKind(kind){switch(kind){case"server":case"http":return"stderr-and-sentry";default:return"stderr-only"}}function makeModelRoutingDiagnostic(fields){return{...fields,reportingPolicy:diagnosticReportingPolicyForKind(fields.kind)}}function formatModelRoutingDiagnosticLine(d){let repo=d.repoName??"<unknown>",target=d.storeTarget??(d.repoName?`bapi:${d.repoName}`:"bapi:<repo>"),storePath=d.storePath??"~/.config/bridge/credentials.json",failOpen=d.agentName==="codex"?"Codex continues with its configured default model.":"Assuming hard ticket; defaulting to premium/Opus when available.",ticketSuffix=d.ticketKey?` for ${d.ticketKey}`:"",statusSuffix=typeof d.status=="number"?` (HTTP ${d.status})`:"";switch(d.kind){case"repo-missing":return`model routing skipped: could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config). ${failOpen}`;case"credential-not-found":case"credential-missing-key":return`model routing skipped: no BAPI_API_KEY for repo ${repo}. Run /install-bridge again, or add BAPI_API_KEY under ${target} in ${storePath}. ${failOpen}`;case"credential-read-error":return`model routing skipped: could not read the credential store at ${storePath} for ${target}. Fix file permissions or rerun /install-bridge. ${failOpen}`;case"credential-parse-error":return`model routing skipped: the credential store at ${storePath} is malformed (cannot parse ${target}). Repair it or rerun /install-bridge. ${failOpen}`;case"unauthorized":return`model routing degraded: a BAPI_API_KEY for ${target} was found but rejected by Bridge API${statusSuffix}. Rerun /install-bridge or rotate the stored ${target} key. ${failOpen}`;case"network":return`model routing degraded: could not reach Bridge API (network error or timeout). Ticket spawning continues fail-open. ${failOpen}`;case"server":return`model routing degraded: Bridge API returned a server error${statusSuffix}. Ticket spawning continues fail-open. ${failOpen}`;case"http":return`model routing degraded: unexpected Bridge API response${statusSuffix}. Ticket spawning continues fail-open. ${failOpen}`;case"config-unavailable":return`model routing degraded: could not fetch routing config from Bridge API. Ticket spawning continues fail-open. ${failOpen}`;case"tier-fetch-unavailable":return`model routing degraded: could not fetch the model tier${ticketSuffix} from Bridge API. Ticket spawning continues fail-open. ${failOpen}`;case"no-tier":return`model routing: Bridge API returned no usable tier${ticketSuffix}. Ticket spawning continues fail-open. ${failOpen}`;case"catalog-unavailable":return`model routing degraded: Codex model catalog unavailable. ${failOpen}`;case"catalog-invalid":return`model routing degraded: Codex model catalog failed validation. ${failOpen}`;case"codex-entitlement-unverified":return"model routing: Bridge permits the selected model; local Codex account entitlement cannot be verified non-interactively.";default:return`model routing degraded: ${d.message} ${failOpen}`}}function startTicketsGetHeaders(access2){return{"X-API-Key":access2.apiKey,"X-Bridge-MCP-Version":VERSION}}async function resolveStartTicketsRepoName2(deps){return resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:filePath=>readFile5(filePath,"utf-8")})}async function resolveStartTicketsBridgeApiAccess(deps){let repoResult=await resolveRequiredStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:p=>readFile5(p,"utf-8")});if(!repoResult.ok)return{ok:!1,warning:"model routing: could not resolve repo name (set BAPI_REPO_NAME or .bridge/config); using agent default model",diagnostic:makeModelRoutingDiagnostic({kind:"repo-missing",message:"could not resolve repo name for model routing"})};let repoName=repoResult.repoName,storeTarget=`bapi:${repoName}`,storePath=getPrimaryCredentialStorePath({env:deps.env,homedir:os5.homedir}),credResult;try{credResult=await resolveBapiCredentials(repoName,{env:deps.env,homedir:os5.homedir,platform:deps.platform,readFile:p=>readFile5(p,"utf-8"),stat:p=>stat3(p)})}catch{return{ok:!1,warning:"model routing: failed to resolve Bridge API credentials; using agent default model",diagnostic:makeModelRoutingDiagnostic({kind:"credential-read-error",repoName,storeTarget,storePath,message:"failed to resolve Bridge API credentials"})}}if(!credResult.ok){let kind=credResult.kind==="not-found"?"credential-not-found":credResult.kind==="read-error"?"credential-read-error":credResult.kind==="parse-error"?"credential-parse-error":"credential-missing-key";return{ok:!1,warning:"model routing: Bridge API credentials unavailable; using agent default model",diagnostic:makeModelRoutingDiagnostic({kind,repoName,storeTarget,storePath,message:"Bridge API credentials unavailable"})}}let baseUrlRaw=deps.env.BAPI_BASE_URL,baseUrl=typeof baseUrlRaw=="string"&&baseUrlRaw.trim().length>0?baseUrlRaw.trim():START_TICKETS_DEFAULT_BASE_URL;return{ok:!0,access:{repoName,apiKey:credResult.credentials.apiKey,baseUrl}}}function buildStartTicketsJiraUrl(baseUrl,apiPath,params={}){let trimmed=baseUrl.replace(/\/+$/,""),url=new URL(`${trimmed}/jira${apiPath}`);for(let[k,v]of Object.entries(params))url.searchParams.set(k,v);return url.toString()}function diagnosticFromFetchError(err,opts){let base={repoName:opts.repoName,storeTarget:opts.repoName?`bapi:${opts.repoName}`:void 0,ticketKey:opts.ticketKey,message:opts.message};return err instanceof StartTicketsHttpError?makeModelRoutingDiagnostic({...base,kind:err.kind,status:err.status}):makeModelRoutingDiagnostic({...base,kind:opts.fallbackKind})}async function fetchJsonWithTimeout(url,headers,timeoutMs){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetch(url,{headers,signal:controller.signal})}catch{throw new StartTicketsHttpError("network")}if(!resp.ok)throw resp.status===401||resp.status===403?new StartTicketsHttpError("unauthorized",resp.status):resp.status>=500?new StartTicketsHttpError("server",resp.status):new StartTicketsHttpError("http",resp.status);try{return await resp.json()}catch{throw new StartTicketsHttpError("network")}}finally{clearTimeout(timer)}}function normalizeDifficultyModelTierOverrides(value){if(value==null)return{};if(typeof value!="object"||Array.isArray(value))return{};let out={};for(let[key,raw]of Object.entries(value)){if(!isModelTier(key)||typeof raw!="string")continue;let trimmed=raw.trim();trimmed.length!==0&&(out[key]=trimmed)}return out}async function fetchStartTicketsConfigField(access2,fieldName){let url=buildStartTicketsJiraUrl(access2.baseUrl,`/config-field/${encodeURIComponent(fieldName)}`,{repo_name:access2.repoName}),body=await fetchJsonWithTimeout(url,startTicketsGetHeaders(access2),CONFIG_FIELD_FETCH_TIMEOUT_MS);if(body&&typeof body=="object"&&"value"in body)return body.value}async function fetchDifficultyModelRoutingConfig(access2,includeOverrides=!0){try{let enabledValue=await fetchStartTicketsConfigField(access2,"difficulty_model_routing_enabled"),overridesValue=includeOverrides?await fetchStartTicketsConfigField(access2,"difficulty_model_tier_overrides"):void 0;return{ok:!0,config:{enabled:enabledValue!==!1,overrides:normalizeDifficultyModelTierOverrides(overridesValue)}}}catch(err){return{ok:!1,warning:"model routing: failed to fetch routing config; using agent default model",diagnostic:diagnosticFromFetchError(err,{repoName:access2.repoName,fallbackKind:"config-unavailable",message:"failed to fetch routing config"})}}}async function fetchCodexModelCatalog(access2){let body;try{let url=buildStartTicketsJiraUrl(access2.baseUrl,"/codex-model-catalog",{repo_name:access2.repoName});body=await fetchJsonWithTimeout(url,startTicketsGetHeaders(access2),CODEX_CATALOG_FETCH_TIMEOUT_MS)}catch(err){return{ok:!1,warning:"model routing: Codex catalog unavailable; using configured default",diagnostic:{...diagnosticFromFetchError(err,{repoName:access2.repoName,fallbackKind:"catalog-unavailable",message:"Codex model catalog unavailable"}),agentName:"codex"}}}let invalid=()=>({ok:!1,warning:"model routing: Codex catalog invalid; using configured default",diagnostic:makeModelRoutingDiagnostic({kind:"catalog-invalid",repoName:access2.repoName,agentName:"codex",message:"Codex model catalog failed validation"})});if(!body||typeof body!="object"||Array.isArray(body))return invalid();let obj=body;if(!obj.models||typeof obj.models!="object"||Array.isArray(obj.models))return invalid();let models=obj.models,tiers=["cheap","basic","premium"];if(Object.keys(models).length!==tiers.length||!tiers.every(tier=>typeof models[tier]=="string"&&isValidModelAlias(models[tier])))return invalid();let canonical=tiers.map(tier=>`${tier}=${models[tier]}`).join(`
3754
- `),revision=`sha256:${createHash4("sha256").update(canonical,"utf8").digest("hex")}`;return obj.revision!==revision?invalid():{ok:!0,catalog:{models,revision}}}async function fetchTicketModelTierForStartTickets(access2,ticket){try{let url=buildStartTicketsJiraUrl(access2.baseUrl,`/tickets/${encodeURIComponent(ticket)}/model-tier`,{repo_name:access2.repoName}),obj=await fetchJsonWithTimeout(url,startTicketsGetHeaders(access2),TICKET_MODEL_TIER_FETCH_TIMEOUT_MS)??{},tier=isModelTier(obj.tier)?obj.tier:null,difficulty=typeof obj.difficulty=="number"?obj.difficulty:null,source=obj.source==="cached"||obj.source==="computed"||obj.source==="fallback"?obj.source:"fallback";return{ok:!0,value:{difficulty,tier,source}}}catch(err){return{ok:!1,warning:`model routing: tier lookup failed for ${ticket}; using agent default model`,diagnostic:diagnosticFromFetchError(err,{repoName:access2.repoName,ticketKey:ticket,fallbackKind:"tier-fetch-unavailable",message:`tier lookup failed for ${ticket}`})}}}async function fetchTicketModelTiersForRows(access2,rows,maxParallel,isEligible=r=>r.status==="created"&&!!r.path){let eligible=rows.filter(isEligible),limit=Math.min(maxParallel,3),results=await runWithConcurrency(eligible,limit,row2=>fetchTicketModelTierForStartTickets(access2,row2.key).then(res=>[row2.key,res]));return new Map(results)}function parseAdvertisedAgentModels(stdout){let out=new Set;for(let token of stdout.split(/[\s,]+/)){let cleaned=token.replace(/^[^A-Za-z0-9._:-]+/,"").replace(/[^A-Za-z0-9._:-]+$/,"");cleaned.length>0&&/[A-Za-z0-9]/.test(cleaned)&&isValidModelAlias(cleaned)&&out.add(cleaned)}return out}async function fetchCursorAgentAdvertisedModels(deps,agent){let tryCommand=async args=>{try{let result=await deps.runCommand(agent.command,args,{cwd:deps.cwd,timeoutMs:CURSOR_MODEL_LIST_TIMEOUT_MS});if(!commandSucceeded(result))return null;let models=parseAdvertisedAgentModels(result.stdout);return models.size>0?models:null}catch{return null}},primary=await tryCommand(["--list-models"]);return primary||tryCommand(["models"])}async function validateResolvedModelAliasForAgent(deps,agent,candidate){if(agent.name!=="cursor-agent")return{ok:!0,alias:candidate};let advertised=await fetchCursorAgentAdvertisedModels(deps,agent);return advertised?advertised.has(candidate)?{ok:!0,alias:candidate}:{ok:!1,warning:`model routing: cursor-agent model '${candidate}' not advertised; omitting --model`}:{ok:!1,warning:`model routing: could not verify cursor-agent models; omitting --model for '${candidate}'`}}function applyDefaultModelRoutingMetadata(row2,reason,diagnostic){return{...row2,difficulty:row2.difficulty??null,modelTier:null,modelAlias:null,modelRoutingSource:row2.modelRoutingSource??"default",modelRoutingReason:reason,...diagnostic?{modelRoutingDiagnostic:diagnostic}:{}}}function resolveRoutingAlias(agent,tier,overrides,catalog){return agent.name==="codex"?catalog?.models[tier]??null:resolveModelAlias(agent,tier,overrides)}function codexEntitlementDiagnostic(repoName){return makeModelRoutingDiagnostic({kind:"codex-entitlement-unverified",agentName:"codex",repoName,message:"Local Codex account entitlement cannot be verified non-interactively"})}function codexDefaultRows(rows,isEligible,warning,diagnostic){return rows.map(row2=>isEligible(row2)?appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row2,warning,diagnostic?{...diagnostic,agentName:"codex"}:void 0),warning):row2)}async function makePremiumFallbackApplier(deps,agent,overrides,catalog=null,repoName){let alias=resolveRoutingAlias(agent,"premium",overrides,catalog),validation=alias?await validateResolvedModelAliasForAgent(deps,agent,alias):null;return(row2,warning,diagnostic)=>{if(validation&&validation.ok){let reason=agent.name==="codex"?`${warning}; defaulting to premium (model=${validation.alias})`:`${warning}; defaulting to premium/Opus (model=${validation.alias})`;return appendSummaryRowWarning({...row2,difficulty:row2.difficulty??null,modelTier:"premium",modelAlias:validation.alias,modelRoutingSource:"fallback",modelRoutingReason:reason,...diagnostic?{modelRoutingDiagnostic:agent.name==="codex"?{...diagnostic,agentName:"codex"}:diagnostic}:agent.name==="codex"&&repoName?{modelRoutingDiagnostic:codexEntitlementDiagnostic(repoName)}:{}},warning)}return appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row2,warning,diagnostic),warning)}}async function applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,overrides,warning,diagnostic){let apply=await makePremiumFallbackApplier(deps,agent,overrides);return rows.map(r=>isEligible(r)?apply(r,warning,diagnostic):r)}async function applyInjectedTierRoutingToEligibleRows(deps,rows,isEligible,agent,overrides,tier,catalog=null,repoName){let stampInjected=row2=>({...row2,difficulty:null,modelTier:tier,modelRoutingSource:"injected"}),alias=resolveRoutingAlias(agent,tier,overrides,catalog);if(!alias){let warning=`model routing: no valid alias for injected tier=${tier}; using agent default`;return rows.map(r=>isEligible(r)?appendSummaryRowWarning({...stampInjected(r),modelAlias:null,modelRoutingReason:warning},warning):r)}let validation=await validateResolvedModelAliasForAgent(deps,agent,alias);return validation.ok?rows.map(r=>isEligible(r)?{...stampInjected(r),modelAlias:validation.alias,modelRoutingReason:`injected tier=${tier} model=${validation.alias}`,...agent.name==="codex"&&repoName?{modelRoutingDiagnostic:codexEntitlementDiagnostic(repoName)}:{}}:r):rows.map(r=>isEligible(r)?appendSummaryRowWarning({...stampInjected(r),modelAlias:null,modelRoutingReason:validation.warning},validation.warning):r)}async function resolveModelRoutingForEligible(deps,rows,options,agent,isEligible){let eligible=rows.filter(isEligible);if(eligible.length===0)return rows;if(!agent.supportsModelOverride){let reason=`model routing: agent '${agent.name}' does not support --model; using agent default`;return rows.map(r=>isEligible(r)?applyDefaultModelRoutingMetadata(r,reason):r)}let accessResult=await resolveStartTicketsBridgeApiAccess(deps);if(!accessResult.ok)return agent.name==="codex"?codexDefaultRows(rows,isEligible,accessResult.warning,accessResult.diagnostic):applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,null,accessResult.warning,accessResult.diagnostic);let access2=accessResult.access,configResult=await fetchDifficultyModelRoutingConfig(access2,agent.name!=="codex");if(!configResult.ok)return agent.name==="codex"?codexDefaultRows(rows,isEligible,configResult.warning,configResult.diagnostic):applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,null,configResult.warning,configResult.diagnostic);let{enabled,overrides}=configResult.config;if(!enabled){let reason="model routing: disabled for this repo; using agent default";return rows.map(r=>isEligible(r)?applyDefaultModelRoutingMetadata(r,reason):r)}let catalog=null;if(agent.name==="codex"){let result=await fetchCodexModelCatalog(access2);if(!result.ok)return codexDefaultRows(rows,isEligible,result.warning,result.diagnostic);catalog=result.catalog}let injected=options.injectedTier;if(injected===INJECTED_TIER_UNRESOLVED){let warning="model routing: --tier was not one of cheap|basic|premium; defaulting to premium routing",apply=await makePremiumFallbackApplier(deps,agent,overrides,catalog,access2.repoName);return rows.map(r=>isEligible(r)?apply(r,warning):r)}if(injected)return applyInjectedTierRoutingToEligibleRows(deps,rows,isEligible,agent,overrides,injected,catalog,access2.repoName);let tierMap=await fetchTicketModelTiersForRows(access2,eligible,options.maxParallel,isEligible),premiumApplier=null,applyPremiumFallback=async(row2,warning,diagnostic)=>(premiumApplier||(premiumApplier=await makePremiumFallbackApplier(deps,agent,overrides,catalog,access2.repoName)),premiumApplier(row2,warning,diagnostic)),out=[];for(let row2 of rows){if(!isEligible(row2)){out.push(row2);continue}let tierResult=tierMap.get(row2.key);if(!tierResult||!tierResult.ok){let warning=tierResult&&!tierResult.ok?tierResult.warning:`model routing: no tier resolved for ${row2.key}; assuming hard ticket`,diagnostic=tierResult&&!tierResult.ok?tierResult.diagnostic:makeModelRoutingDiagnostic({kind:"tier-fetch-unavailable",repoName:access2.repoName,storeTarget:`bapi:${access2.repoName}`,ticketKey:row2.key,message:`no tier resolved for ${row2.key}`});if(agent.name==="codex"){out.push(codexDefaultRows([row2],()=>!0,warning,diagnostic)[0]);continue}out.push(await applyPremiumFallback(row2,warning,diagnostic));continue}let{difficulty,tier,source}=tierResult.value,baseRow={...row2,difficulty:difficulty??null,modelTier:tier??null,modelRoutingSource:source};if(!tier){let warning=`model routing: ${row2.key} resolved no tier (source=${source}); assuming hard ticket`,diagnostic=makeModelRoutingDiagnostic({kind:"no-tier",repoName:access2.repoName,storeTarget:`bapi:${access2.repoName}`,ticketKey:row2.key,message:`${row2.key} resolved no tier (source=${source})`});if(agent.name==="codex"){out.push(codexDefaultRows([baseRow],()=>!0,warning,diagnostic)[0]);continue}out.push(await applyPremiumFallback(baseRow,warning,diagnostic));continue}let alias=resolveRoutingAlias(agent,tier,overrides,catalog);if(!alias){let warning=`model routing: no valid alias for tier=${tier}; using agent default`;out.push(appendSummaryRowWarning({...baseRow,modelAlias:null,modelRoutingReason:warning},warning));continue}let validation=await validateResolvedModelAliasForAgent(deps,agent,alias);if(!validation.ok){out.push(appendSummaryRowWarning({...baseRow,modelAlias:null,modelRoutingReason:validation.warning},validation.warning));continue}out.push({...baseRow,modelAlias:validation.alias,modelRoutingReason:`tier=${tier} model=${validation.alias} (source=${source})`,...agent.name==="codex"?{modelRoutingDiagnostic:codexEntitlementDiagnostic(access2.repoName)}:{}})}return out}async function resolveModelRoutingForRows(deps,rows,options,agent){return resolveModelRoutingForEligible(deps,rows,options,agent,isCreatedRoutingEligible)}async function resolveModelRoutingForDryRun(deps,rows,options,agent){return resolveModelRoutingForEligible(deps,rows,options,agent,isDryRunRoutingEligible)}function formatModelRoutingLine(row2,agent,context){let difficulty=typeof row2.difficulty=="number"?String(row2.difficulty):"?",tier=row2.modelTier??"fallback",model=row2.modelAlias??"default",base=`${row2.key} difficulty=${difficulty} tier=${tier} agent=${agent.name} model=${model}`,labels=[];return context?.workflow==="review-and-implement"&&labels.push("phase=review"),row2.modelRoutingSource==="injected"&&labels.push("routing=injected"),labels.length>0?`${base} ${labels.join(" ")}`:base}function modelRoutingDiagnosticKey(d){return[d.kind,d.repoName??"",d.storeTarget??"",d.storePath??"",d.ticketKey??"",typeof d.status=="number"?String(d.status):""].join("|")}function collectInvocationModelRoutingDiagnostics(rows){let byKey=new Map;for(let row2 of rows){let d=row2.modelRoutingDiagnostic;if(!d)continue;let key=modelRoutingDiagnosticKey(d);byKey.has(key)||byKey.set(key,d)}return[...byKey.values()]}function modelRoutingDiagnosticPriority(kind){switch(kind){case"repo-missing":case"credential-not-found":case"credential-missing-key":case"credential-read-error":case"credential-parse-error":return 0;case"unauthorized":return 1;case"server":case"network":case"http":case"config-unavailable":case"tier-fetch-unavailable":return 2;case"no-tier":return 3;default:return 4}}function selectPrimaryInvocationModelRoutingDiagnostic(diagnostics){let best=null,bestRank=Number.POSITIVE_INFINITY;for(let d of diagnostics){let rank=modelRoutingDiagnosticPriority(d.kind);rank<bestRank&&(best=d,bestRank=rank)}return best}function emitInvocationModelRoutingDiagnostic(diagnostic,errorLog){diagnostic&&errorLog(formatModelRoutingDiagnosticLine(diagnostic))}function emitInvocationModelRoutingDiagnosticForRows(rows,errorLog){emitInvocationModelRoutingDiagnostic(selectPrimaryInvocationModelRoutingDiagnostic(collectInvocationModelRoutingDiagnostics(rows)),errorLog)}async function provisionConductorForRows(rows,deps,options,agent,overrides){let createConductorContextFn=overrides.createConductorContext??createStartTicketsConductorContext,provisionConductorFn=overrides.provisionConductorHooksForRows??provisionConductorHooksForRows,emitRunStartedFn=overrides.emitStartTicketsRunStarted??emitStartTicketsRunStarted;try{let context=await createConductorContextFn({keys:options.keys,autoApprove:options.autoApprove,epic:options.epic},agent,{env:deps.env,cwd:deps.cwd,readFile:filePath=>readFile5(filePath,"utf-8")}),provisioned=await provisionConductorFn(rows,context,{readFile:filePath=>readFile5(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile3(filePath,data,"utf-8"),mkdir:(dirPath,opts)=>mkdir3(dirPath,opts),env:deps.env});return{rows:await emitRunStartedFn(context,provisioned,{keys:options.keys,dryRun:options.dryRun}),context}}catch{return{rows,context:null}}}async function launchSupervisorTab(context,deps,options,terminal){if(options.dryRun||!isSupervisorLaunchEnabled(context))return"skipped";try{let shellCommand=buildSupervisorTabCommand(context,deps.platform);return(await deps.spawnTerminalTab(deps,terminal,shellCommand,{key:supervisorSpawnKey(options.keys),worktreePath:deps.cwd})).ok?"spawned":"failed"}catch{return"failed"}}function stampSupervisorStatus(rows,status){return rows.map(row2=>({...row2,supervisorStatus:status}))}async function defaultClaimEpicDispatch(dispatchKey,runId,deps){let accessResult=await resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd});if(!accessResult.ok)throw new Error(`Epic dispatch correlation failed: ${accessResult.error}`);await transitionEpicDispatch(accessResult.access,{dispatchKey,nextStatus:"run_spawned",runId})}async function orchestrateStartTickets(deps,options,overrides={}){let indexScope;try{indexScope=validateOptionalIndexScope(options.epic?.index_scope_id)}catch{return{ok:!1,error:INDEX_SCOPE_CONFIGURATION_ERROR}}if(await pruneStaleLaunchScripts(),options.epic?.base_branch&&(options={...options,baseBranch:options.epic.base_branch}),options.dryRun)return{ok:!0,rows:buildDryRunResults(options.keys,options.branchOverrides)};let agent=resolveAgentSpec(options.agentName);if(!agent)return{ok:!1,error:`Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`};let preflight=await runPreflight(deps,options,overrides.liveSourceWarningLog);if(!preflight.ok)return{ok:!1,error:preflight.error};let resolvedRepoName=await(overrides.resolveRepoName??resolveStartTicketsRepoName2)(deps),bridgeApiBaseUrl=await resolveStartTicketsBridgeApiBaseUrl(deps,agent);resolvedRepoName||overrides.repoNameWarningLog?.("Warning: could not resolve the repo name (set BAPI_REPO_NAME or add a valid .bridge/config). Spawned worktrees may fall back to the directory name and hit 403 'repository not registered' if it differs from the Bridge API project name.");let platformConfig=resolveStartTicketsPlatformConfig(deps,agent,options.autoApprove,options.conductorEnabled??!1,resolvedRepoName,options.resumeMode??!1,options.workflow,options.reviewRounds,options.baseBranch,bridgeApiBaseUrl);if(!platformConfig.ok)return{ok:!1,error:platformConfig.error};let effectiveBaseStartPoint=options.baseBranch;if(options.nonMutatingBase===!0){let resolved=await fetchAndResolveBaseSha(deps,options.baseBranch);if(!resolved.ok)return{ok:!1,error:resolved.error};effectiveBaseStartPoint=resolved.base_sha}else{let refresh=await refreshBaseBranch(deps,{refreshMain:options.refreshMain,baseBranch:options.baseBranch});if(!refresh.ok)return{ok:!1,error:refresh.error}}await emitClaudeReviewWorkflowDriftAdvisory(deps,options.baseBranch,overrides);let createWorktreesFn=overrides.createWorktrees??createWorktrees,provisionCommandsFn=overrides.provisionCommands??((rows2,d)=>provisionCommandsForCreatedWorktrees(rows2,buildCommandProvisioningDeps(d),agent.name==="codex")),provisionFn=overrides.provisionMcpRegistrations??((rows2,d)=>provisionMcpRegistrationsForCreatedWorktrees(rows2,buildMcpProvisioningDeps(d,bridgeApiBaseUrl))),materializeFn=overrides.materializeFileCredentials??materializeFileCredentialsForCreatedWorktrees,detectTerminalFn=overrides.detectTerminal??detectTerminal,spawnTabsFn=overrides.spawnTabsForCreatedWorktrees??spawnTabsForCreatedWorktrees,resumeWorktreesFn=overrides.resumeWorktrees??resumeWorktrees,created=options.resumeMode?await resumeWorktreesFn(deps,options):await createWorktreesFn(deps,options,platformConfig.config.worktrunkBinary,effectiveBaseStartPoint),commandProvisioned=await provisionCommandsFn(created,deps),provisioned=await provisionFn(commandProvisioned,deps),materialized=await materializeFn(provisioned,deps);if(options.epic?.dispatch_key&&!overrides.createConductorContext)return{ok:!1,error:"Epic dispatch requires conductor context (createConductorContext seam must be injected when dispatch_key is set)"};let terminal=detectTerminalFn(options.terminal,deps.env),conductorReady;if(overrides.createConductorContext){let provisionResult=await provisionConductorForRows(materialized,deps,options,agent,overrides);if(options.epic?.dispatch_key&&!provisionResult.context)return{ok:!1,error:"Epic dispatch correlation unavailable: conductor provisioning failed (cannot claim dispatch without run_id)"};if(provisionResult.context){if(options.epic?.dispatch_key){let claimFn=overrides.claimEpicDispatch??defaultClaimEpicDispatch;try{await claimFn(options.epic.dispatch_key,provisionResult.context.runId,deps)}catch(e){return{ok:!1,error:`Epic dispatch correlation failed (idempotency guard): ${e instanceof Error?e.message:"epic dispatch correlation failed"}`}}}let supervisorStatus=await launchSupervisorTab(provisionResult.context,deps,options,terminal);conductorReady=stampSupervisorStatus(provisionResult.rows,supervisorStatus)}else conductorReady=provisionResult.rows}else conductorReady=materialized;let routed=await(overrides.resolveModelRoutingForRows??resolveModelRoutingForRows)(deps,conductorReady,options,agent);for(let row2 of routed)row2.status!=="created"||!row2.path||overrides.modelRoutingLog?.(formatModelRoutingLine(row2,agent,{workflow:options.workflow}));overrides.modelRoutingWarningLog&&emitInvocationModelRoutingDiagnosticForRows(routed,overrides.modelRoutingWarningLog);let scoped=stampIndexScopeOnRows(routed,indexScope);if(agent.name==="codex"){let mcpDeps=buildMcpProvisioningDeps(deps);scoped=await Promise.all(scoped.map(async row2=>{if(row2.status!=="created"||!row2.path)return row2;let scope=await(overrides.resolveCodexScope??resolveCodexLaunchScope)(row2.path,mcpDeps);return scope.ok?{...row2,codexLaunchArgs:scope.argv}:{...row2,status:"spawn-failed",error:scope.error}}))}return{ok:!0,rows:await spawnTabsFn(deps,scoped,terminal,platformConfig.config.buildAgentShellCommand)}}function spawnFailureHintForPlatform(platform){return platform==="darwin"?"One or more tabs failed to spawn. If osascript reported a permission error, grant the required macOS permissions under System Settings -> Privacy & Security, then re-run: Automation (to let the terminal app be controlled) and, for the Terminal.app path, Accessibility (the new-tab step sends Cmd-T via System Events keystrokes, which Automation alone does not cover).":platform==="win32"?"One or more tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is installed and on PATH; see the per-ticket warnings above for details.":platform==="linux"?"One or more tabs failed to spawn. Ensure tmux is installed and on PATH; see the per-ticket warnings above for details.":"One or more tabs failed to spawn; see the per-ticket warnings above for details."}async function runStartTicketsCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseStartTicketsArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getStartTicketsUsage()),1;let options=parsed.options,rawDeps=overrides.deps??createDefaultStartTicketsDeps(),orchestrate=overrides.orchestrate??orchestrateStartTickets,agent=resolveAgentSpec(options.agentName);if(!agent)return errorLog(`Error: Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`),1;let bridgeApiBaseUrl=await resolveStartTicketsBridgeApiBaseUrl(rawDeps,agent),deps=bridgeApiBaseUrl?{...rawDeps,env:{...rawDeps.env,BAPI_BASE_URL:bridgeApiBaseUrl}}:rawDeps;if(options.injectedTier===INJECTED_TIER_UNRESOLVED&&errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and using premium model routing for every ticket."),options.dryRun){let dryRunRepoName=await resolveStartTicketsRepoName2(deps),resolveDryRunRoutingFn=overrides.resolveModelRoutingForDryRun??resolveModelRoutingForDryRun,dryRunRows=buildDryRunResults(options.keys,options.branchOverrides),routedDryRunRows=await resolveDryRunRoutingFn(deps,dryRunRows,options,agent),routedByKey=new Map(routedDryRunRows.map(r=>[r.key,r])),dryRunMcpInvocation=buildMcpProvisioningDeps(deps).mcpServerInvocation;for(let key of options.keys){let branch=resolveBranchForTicket(key,options.branchOverrides),routedRow=routedByKey.get(key),modelAlias=routedRow?.modelAlias??null;for(let line of buildDryRunDetailLines(agent,key,branch,deps.platform,deps.env,options.baseBranch,options.autoApprove,modelAlias,options.conductorEnabled??!1,dryRunRepoName,dryRunMcpInvocation,options.workflow,options.reviewRounds))log(line);log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow??{key,branch,status:"dry-run"},agent,{workflow:options.workflow})}`),modelAlias==null&&routedRow?.modelRoutingReason&&log(`DRY-RUN: ${routedRow.modelRoutingReason}`)}log(""),emitInvocationModelRoutingDiagnosticForRows(routedDryRunRows,errorLog)}let result=await orchestrate(deps,options,{modelRoutingLog:log,modelRoutingWarningLog:errorLog,repoNameWarningLog:errorLog,workflowDriftWarningLog:errorLog,...options.conductorEnabled?{createConductorContext:createStartTicketsConductorContext,provisionConductorHooksForRows,emitStartTicketsRunStarted}:{}});if(!result.ok)return errorLog(`Error: ${result.error}`),1;log(formatSummaryReport(result.rows));let spawned=result.rows.filter(r=>r.status==="spawned");if(deps.platform==="linux"&&spawned.length>0){log(""),log("Linux: each ticket runs in a detached tmux session. Attach with:");for(let row2 of spawned)log(` tmux attach -t ${tmuxSessionNameForTicket(deps,row2.key)}`)}return result.rows.filter(r=>r.status==="spawn-failed").length>0&&(errorLog(""),errorLog(spawnFailureHintForPlatform(deps.platform))),0}var TICKET_KEY_PATTERN3,DEFAULT_MAX_PARALLEL,DEFAULT_TMUX_SESSION_PREFIX,TMUX_SESSION_OVERRIDE_ENV,INJECTED_TIER_UNRESOLVED,STALE_LAUNCH_SCRIPT_MAX_AGE_MS,defaultPruneStaleLaunchScriptsDeps,defaultWriteWorkerLaunchScript,MAX_TERMINAL_COMMAND_BYTES,START_TICKETS_DEFAULT_BASE_URL,TICKET_MODEL_TIER_FETCH_TIMEOUT_MS,CONFIG_FIELD_FETCH_TIMEOUT_MS,CODEX_CATALOG_FETCH_TIMEOUT_MS,CURSOR_MODEL_LIST_TIMEOUT_MS,StartTicketsHttpError,isCreatedRoutingEligible,isDryRunRoutingEligible,init_start_tickets=__esm({"src/start-tickets.ts"(){"use strict";init_base_url();init_version_generated();init_credential_store();init_mcp_identity();init_claude_review_workflow_drift();init_start_tickets_repo();init_mcp_provisioning();init_command_provisioning();init_mcp_server_invocation();init_ticket_key_utils();init_start_tickets_prereqs();init_agent_registry();init_mcp_host_targets();init_codex_launch_scope();init_start_tickets_conductor();init_bridge_api_client();init_pr_base_contract();init_index_scope_contract();init_worktree_core();init_env_file_link();init_base_ref();TICKET_KEY_PATTERN3=TICKET_KEY_PATTERN2,DEFAULT_MAX_PARALLEL=3,DEFAULT_TMUX_SESSION_PREFIX="bridge-start-tickets",TMUX_SESSION_OVERRIDE_ENV="BAPI_TMUX_SESSION",INJECTED_TIER_UNRESOLVED="__unresolved__";STALE_LAUNCH_SCRIPT_MAX_AGE_MS=1440*60*1e3,defaultPruneStaleLaunchScriptsDeps={readdir:dir=>readdir2(dir),stat:p=>stat3(p),rm:(p,options)=>rm2(p,options),now:()=>Date.now()};defaultWriteWorkerLaunchScript=async({platform,key,content})=>{let parent=path20.join(os5.tmpdir(),"bridge-start-tickets");await mkdir3(parent,{recursive:!0});let dir=await mkdtemp(path20.join(parent,"w-")),ext=platform==="win32"?"ps1":"sh",file=path20.join(dir,`launch-${sanitizeKeyForLaunchScript(key)}.${ext}`);return await writeFile3(file,content,{mode:384}),file},MAX_TERMINAL_COMMAND_BYTES=1024;START_TICKETS_DEFAULT_BASE_URL="https://bridgegpt-api.com",TICKET_MODEL_TIER_FETCH_TIMEOUT_MS=75e3,CONFIG_FIELD_FETCH_TIMEOUT_MS=3e4,CODEX_CATALOG_FETCH_TIMEOUT_MS=15e3,CURSOR_MODEL_LIST_TIMEOUT_MS=15e3;StartTicketsHttpError=class extends Error{kind;status;constructor(kind,status){super(`Bridge API request failed (${kind}${typeof status=="number"?`, status ${status}`:""})`),this.name="StartTicketsHttpError",this.kind=kind,this.status=status}};isCreatedRoutingEligible=r=>r.status==="created"&&!!r.path,isDryRunRoutingEligible=r=>r.status==="dry-run"}});function isReviewTicketsAgentName(value){return REVIEW_TICKETS_AGENT_NAMES.includes(value)}function formatReviewTicketsAgentNames(){return REVIEW_TICKETS_AGENT_NAMES.join(", ")}function getReviewTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} review-tickets [flags] KEY [KEY ...]`,"","Flags:"," --auto Auto-approve all review gates (global, applies to all tickets)"," --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: adaptive \u2014 backend routes by difficulty)"," --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)"," --agent claude|cursor-agent|codex Agent command to launch in each tab (default: claude)"," --model VALUE Model alias to pass as --model to the agent (optional passthrough)"," --max-parallel N Max tabs to spawn concurrently (default: 3)"," --dry-run Print intended commands; creates no tabs and opens no sessions",' --base-branch BRANCH Base branch to fetch+pin for grounding (default: config base_branch, else "main")'," --no-refresh-base Skip the parent-fetch-once base pin; restores git-free, in-place-grounded behavior"," -h, --help Show this help","","Examples:",` npx -y ${MCP_PACKAGE_NAME} review-tickets BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --auto --rounds=1 BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --review BAPI-1=auto,rounds=1 --review BAPI-2=rounds=2 BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --dry-run BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --base-branch=develop BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --no-refresh-base BAPI-1 BAPI-2`,"","Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248).","","Prerequisites (terminal launcher, plus git \u2014 no wt, git-wt, or Worktrunk required):"," macOS osascript, git"," Windows wt.exe or PowerShell, git"," Linux tmux, git","","review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees. It now","requires git on PATH (BAPI-474): before spawning tabs it fetches origin/<base_branch> once and pins a","single base_sha for the whole batch, so every spawned review grounds against the same base tree even if","origin advances mid-batch. Pass --no-refresh-base to skip this and restore the prior git-free behavior.","--dry-run works on any platform, including unsupported ones, and never performs the base-branch fetch."].join(`
3754
+ `),revision=`sha256:${createHash4("sha256").update(canonical,"utf8").digest("hex")}`;return obj.revision!==revision?invalid():{ok:!0,catalog:{models,revision}}}async function fetchTicketModelTierForStartTickets(access2,ticket){try{let url=buildStartTicketsJiraUrl(access2.baseUrl,`/tickets/${encodeURIComponent(ticket)}/model-tier`,{repo_name:access2.repoName}),obj=await fetchJsonWithTimeout(url,startTicketsGetHeaders(access2),TICKET_MODEL_TIER_FETCH_TIMEOUT_MS)??{},tier=isModelTier(obj.tier)?obj.tier:null,difficulty=typeof obj.difficulty=="number"?obj.difficulty:null,source=obj.source==="cached"||obj.source==="computed"||obj.source==="fallback"?obj.source:"fallback";return{ok:!0,value:{difficulty,tier,source}}}catch(err){return{ok:!1,warning:`model routing: tier lookup failed for ${ticket}; using agent default model`,diagnostic:diagnosticFromFetchError(err,{repoName:access2.repoName,ticketKey:ticket,fallbackKind:"tier-fetch-unavailable",message:`tier lookup failed for ${ticket}`})}}}async function fetchTicketModelTiersForRows(access2,rows,maxParallel,isEligible=r=>r.status==="created"&&!!r.path){let eligible=rows.filter(isEligible),limit=Math.min(maxParallel,3),results=await runWithConcurrency(eligible,limit,row2=>fetchTicketModelTierForStartTickets(access2,row2.key).then(res=>[row2.key,res]));return new Map(results)}function parseAdvertisedAgentModels(stdout){let out=new Set;for(let token of stdout.split(/[\s,]+/)){let cleaned=token.replace(/^[^A-Za-z0-9._:-]+/,"").replace(/[^A-Za-z0-9._:-]+$/,"");cleaned.length>0&&/[A-Za-z0-9]/.test(cleaned)&&isValidModelAlias(cleaned)&&out.add(cleaned)}return out}async function fetchCursorAgentAdvertisedModels(deps,agent){let tryCommand=async args=>{try{let result=await deps.runCommand(agent.command,args,{cwd:deps.cwd,timeoutMs:CURSOR_MODEL_LIST_TIMEOUT_MS});if(!commandSucceeded(result))return null;let models=parseAdvertisedAgentModels(result.stdout);return models.size>0?models:null}catch{return null}},primary=await tryCommand(["--list-models"]);return primary||tryCommand(["models"])}async function validateResolvedModelAliasForAgent(deps,agent,candidate){if(agent.name!=="cursor-agent")return{ok:!0,alias:candidate};let advertised=await fetchCursorAgentAdvertisedModels(deps,agent);return advertised?advertised.has(candidate)?{ok:!0,alias:candidate}:{ok:!1,warning:`model routing: cursor-agent model '${candidate}' not advertised; omitting --model`}:{ok:!1,warning:`model routing: could not verify cursor-agent models; omitting --model for '${candidate}'`}}function applyDefaultModelRoutingMetadata(row2,reason,diagnostic){return{...row2,difficulty:row2.difficulty??null,modelTier:null,modelAlias:null,modelRoutingSource:row2.modelRoutingSource??"default",modelRoutingReason:reason,...diagnostic?{modelRoutingDiagnostic:diagnostic}:{}}}function resolveRoutingAlias(agent,tier,overrides,catalog){return agent.name==="codex"?catalog?.models[tier]??null:resolveModelAlias(agent,tier,overrides)}function codexEntitlementDiagnostic(repoName){return makeModelRoutingDiagnostic({kind:"codex-entitlement-unverified",agentName:"codex",repoName,message:"Local Codex account entitlement cannot be verified non-interactively"})}function codexDefaultRows(rows,isEligible,warning,diagnostic){return rows.map(row2=>isEligible(row2)?appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row2,warning,diagnostic?{...diagnostic,agentName:"codex"}:void 0),warning):row2)}async function makePremiumFallbackApplier(deps,agent,overrides,catalog=null,repoName){let alias=resolveRoutingAlias(agent,"premium",overrides,catalog),validation=alias?await validateResolvedModelAliasForAgent(deps,agent,alias):null;return(row2,warning,diagnostic)=>{if(validation&&validation.ok){let reason=agent.name==="codex"?`${warning}; defaulting to premium (model=${validation.alias})`:`${warning}; defaulting to premium/Opus (model=${validation.alias})`;return appendSummaryRowWarning({...row2,difficulty:row2.difficulty??null,modelTier:"premium",modelAlias:validation.alias,modelRoutingSource:"fallback",modelRoutingReason:reason,...diagnostic?{modelRoutingDiagnostic:agent.name==="codex"?{...diagnostic,agentName:"codex"}:diagnostic}:agent.name==="codex"&&repoName?{modelRoutingDiagnostic:codexEntitlementDiagnostic(repoName)}:{}},warning)}return appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row2,warning,diagnostic),warning)}}async function applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,overrides,warning,diagnostic){let apply=await makePremiumFallbackApplier(deps,agent,overrides);return rows.map(r=>isEligible(r)?apply(r,warning,diagnostic):r)}async function applyInjectedTierRoutingToEligibleRows(deps,rows,isEligible,agent,overrides,tier,catalog=null,repoName){let stampInjected=row2=>({...row2,difficulty:null,modelTier:tier,modelRoutingSource:"injected"}),alias=resolveRoutingAlias(agent,tier,overrides,catalog);if(!alias){let warning=`model routing: no valid alias for injected tier=${tier}; using agent default`;return rows.map(r=>isEligible(r)?appendSummaryRowWarning({...stampInjected(r),modelAlias:null,modelRoutingReason:warning},warning):r)}let validation=await validateResolvedModelAliasForAgent(deps,agent,alias);return validation.ok?rows.map(r=>isEligible(r)?{...stampInjected(r),modelAlias:validation.alias,modelRoutingReason:`injected tier=${tier} model=${validation.alias}`,...agent.name==="codex"&&repoName?{modelRoutingDiagnostic:codexEntitlementDiagnostic(repoName)}:{}}:r):rows.map(r=>isEligible(r)?appendSummaryRowWarning({...stampInjected(r),modelAlias:null,modelRoutingReason:validation.warning},validation.warning):r)}async function resolveModelRoutingForEligible(deps,rows,options,agent,isEligible){let eligible=rows.filter(isEligible);if(eligible.length===0)return rows;if(!agent.supportsModelOverride){let reason=`model routing: agent '${agent.name}' does not support --model; using agent default`;return rows.map(r=>isEligible(r)?applyDefaultModelRoutingMetadata(r,reason):r)}let accessResult=await resolveStartTicketsBridgeApiAccess(deps);if(!accessResult.ok)return agent.name==="codex"?codexDefaultRows(rows,isEligible,accessResult.warning,accessResult.diagnostic):applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,null,accessResult.warning,accessResult.diagnostic);let access2=accessResult.access,configResult=await fetchDifficultyModelRoutingConfig(access2,agent.name!=="codex");if(!configResult.ok)return agent.name==="codex"?codexDefaultRows(rows,isEligible,configResult.warning,configResult.diagnostic):applyPremiumFallbackToEligibleRows(deps,rows,isEligible,agent,null,configResult.warning,configResult.diagnostic);let{enabled,overrides}=configResult.config;if(!enabled){let reason="model routing: disabled for this repo; using agent default";return rows.map(r=>isEligible(r)?applyDefaultModelRoutingMetadata(r,reason):r)}let catalog=null;if(agent.name==="codex"){let result=await fetchCodexModelCatalog(access2);if(!result.ok)return codexDefaultRows(rows,isEligible,result.warning,result.diagnostic);catalog=result.catalog}let injected=options.injectedTier;if(injected===INJECTED_TIER_UNRESOLVED){let warning="model routing: --tier was not one of cheap|basic|premium; defaulting to premium routing",apply=await makePremiumFallbackApplier(deps,agent,overrides,catalog,access2.repoName);return rows.map(r=>isEligible(r)?apply(r,warning):r)}if(injected)return applyInjectedTierRoutingToEligibleRows(deps,rows,isEligible,agent,overrides,injected,catalog,access2.repoName);let tierMap=await fetchTicketModelTiersForRows(access2,eligible,options.maxParallel,isEligible),premiumApplier=null,applyPremiumFallback=async(row2,warning,diagnostic)=>(premiumApplier||(premiumApplier=await makePremiumFallbackApplier(deps,agent,overrides,catalog,access2.repoName)),premiumApplier(row2,warning,diagnostic)),out=[];for(let row2 of rows){if(!isEligible(row2)){out.push(row2);continue}let tierResult=tierMap.get(row2.key);if(!tierResult||!tierResult.ok){let warning=tierResult&&!tierResult.ok?tierResult.warning:`model routing: no tier resolved for ${row2.key}; assuming hard ticket`,diagnostic=tierResult&&!tierResult.ok?tierResult.diagnostic:makeModelRoutingDiagnostic({kind:"tier-fetch-unavailable",repoName:access2.repoName,storeTarget:`bapi:${access2.repoName}`,ticketKey:row2.key,message:`no tier resolved for ${row2.key}`});if(agent.name==="codex"){out.push(codexDefaultRows([row2],()=>!0,warning,diagnostic)[0]);continue}out.push(await applyPremiumFallback(row2,warning,diagnostic));continue}let{difficulty,tier,source}=tierResult.value,baseRow={...row2,difficulty:difficulty??null,modelTier:tier??null,modelRoutingSource:source};if(!tier){let warning=`model routing: ${row2.key} resolved no tier (source=${source}); assuming hard ticket`,diagnostic=makeModelRoutingDiagnostic({kind:"no-tier",repoName:access2.repoName,storeTarget:`bapi:${access2.repoName}`,ticketKey:row2.key,message:`${row2.key} resolved no tier (source=${source})`});if(agent.name==="codex"){out.push(codexDefaultRows([baseRow],()=>!0,warning,diagnostic)[0]);continue}out.push(await applyPremiumFallback(baseRow,warning,diagnostic));continue}let alias=resolveRoutingAlias(agent,tier,overrides,catalog);if(!alias){let warning=`model routing: no valid alias for tier=${tier}; using agent default`;out.push(appendSummaryRowWarning({...baseRow,modelAlias:null,modelRoutingReason:warning},warning));continue}let validation=await validateResolvedModelAliasForAgent(deps,agent,alias);if(!validation.ok){out.push(appendSummaryRowWarning({...baseRow,modelAlias:null,modelRoutingReason:validation.warning},validation.warning));continue}out.push({...baseRow,modelAlias:validation.alias,modelRoutingReason:`tier=${tier} model=${validation.alias} (source=${source})`,...agent.name==="codex"?{modelRoutingDiagnostic:codexEntitlementDiagnostic(access2.repoName)}:{}})}return out}async function resolveModelRoutingForRows(deps,rows,options,agent){return resolveModelRoutingForEligible(deps,rows,options,agent,isCreatedRoutingEligible)}async function resolveModelRoutingForDryRun(deps,rows,options,agent){return resolveModelRoutingForEligible(deps,rows,options,agent,isDryRunRoutingEligible)}function formatModelRoutingLine(row2,agent,context){let difficulty=typeof row2.difficulty=="number"?String(row2.difficulty):"?",tier=row2.modelTier??"fallback",model=row2.modelAlias??"default",base=`${row2.key} difficulty=${difficulty} tier=${tier} agent=${agent.name} model=${model}`,labels=[];return context?.workflow==="review-and-implement"&&labels.push("phase=review"),row2.modelRoutingSource==="injected"&&labels.push("routing=injected"),labels.length>0?`${base} ${labels.join(" ")}`:base}function modelRoutingDiagnosticKey(d){return[d.kind,d.repoName??"",d.storeTarget??"",d.storePath??"",d.ticketKey??"",typeof d.status=="number"?String(d.status):""].join("|")}function collectInvocationModelRoutingDiagnostics(rows){let byKey=new Map;for(let row2 of rows){let d=row2.modelRoutingDiagnostic;if(!d)continue;let key=modelRoutingDiagnosticKey(d);byKey.has(key)||byKey.set(key,d)}return[...byKey.values()]}function modelRoutingDiagnosticPriority(kind){switch(kind){case"repo-missing":case"credential-not-found":case"credential-missing-key":case"credential-read-error":case"credential-parse-error":return 0;case"unauthorized":return 1;case"server":case"network":case"http":case"config-unavailable":case"tier-fetch-unavailable":return 2;case"no-tier":return 3;default:return 4}}function selectPrimaryInvocationModelRoutingDiagnostic(diagnostics){let best=null,bestRank=Number.POSITIVE_INFINITY;for(let d of diagnostics){let rank=modelRoutingDiagnosticPriority(d.kind);rank<bestRank&&(best=d,bestRank=rank)}return best}function emitInvocationModelRoutingDiagnostic(diagnostic,errorLog){diagnostic&&errorLog(formatModelRoutingDiagnosticLine(diagnostic))}function emitInvocationModelRoutingDiagnosticForRows(rows,errorLog){emitInvocationModelRoutingDiagnostic(selectPrimaryInvocationModelRoutingDiagnostic(collectInvocationModelRoutingDiagnostics(rows)),errorLog)}async function provisionConductorForRows(rows,deps,options,agent,overrides){let createConductorContextFn=overrides.createConductorContext??createStartTicketsConductorContext,provisionConductorFn=overrides.provisionConductorHooksForRows??provisionConductorHooksForRows,emitRunStartedFn=overrides.emitStartTicketsRunStarted??emitStartTicketsRunStarted;try{let context=await createConductorContextFn({keys:options.keys,autoApprove:options.autoApprove,epic:options.epic},agent,{env:deps.env,cwd:deps.cwd,readFile:filePath=>readFile5(filePath,"utf-8")}),provisioned=await provisionConductorFn(rows,context,{readFile:filePath=>readFile5(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile3(filePath,data,"utf-8"),mkdir:(dirPath,opts)=>mkdir3(dirPath,opts),env:deps.env});return{rows:await emitRunStartedFn(context,provisioned,{keys:options.keys,dryRun:options.dryRun}),context}}catch{return{rows,context:null}}}async function launchSupervisorTab(context,deps,options,terminal){if(options.dryRun||!isSupervisorLaunchEnabled(context))return"skipped";try{let shellCommand=buildSupervisorTabCommand(context,deps.platform);return(await deps.spawnTerminalTab(deps,terminal,shellCommand,{key:supervisorSpawnKey(options.keys),worktreePath:deps.cwd})).ok?"spawned":"failed"}catch{return"failed"}}function stampSupervisorStatus(rows,status){return rows.map(row2=>({...row2,supervisorStatus:status}))}async function defaultClaimEpicDispatch(dispatchKey,runId,deps){let accessResult=await resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd});if(!accessResult.ok)throw new Error(`Epic dispatch correlation failed: ${accessResult.error}`);await transitionEpicDispatch(accessResult.access,{dispatchKey,nextStatus:"run_spawned",runId})}async function orchestrateStartTickets(deps,options,overrides={}){let indexScope;try{indexScope=validateOptionalIndexScope(options.epic?.index_scope_id)}catch{return{ok:!1,error:INDEX_SCOPE_CONFIGURATION_ERROR}}if(await pruneStaleLaunchScripts(),options.epic?.base_branch&&(options={...options,baseBranch:options.epic.base_branch}),options.dryRun)return{ok:!0,rows:buildDryRunResults(options.keys,options.branchOverrides)};let agent=resolveAgentSpec(options.agentName);if(!agent)return{ok:!1,error:`Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`};let preflight=await runPreflight(deps,options,overrides.liveSourceWarningLog);if(!preflight.ok)return{ok:!1,error:preflight.error};let resolvedRepoName=await(overrides.resolveRepoName??resolveStartTicketsRepoName2)(deps),bridgeApiBaseUrl=await resolveStartTicketsBridgeApiBaseUrl(deps,agent);resolvedRepoName||overrides.repoNameWarningLog?.("Warning: could not resolve the repo name (set BAPI_REPO_NAME or add a valid .bridge/config). Spawned worktrees may fall back to the directory name and hit 403 'repository not registered' if it differs from the Bridge API project name.");let platformConfig=resolveStartTicketsPlatformConfig(deps,agent,options.autoApprove,options.conductorEnabled??!1,resolvedRepoName,options.resumeMode??!1,options.workflow,options.reviewRounds,options.baseBranch,bridgeApiBaseUrl);if(!platformConfig.ok)return{ok:!1,error:platformConfig.error};let effectiveBaseStartPoint=options.baseBranch;if(options.nonMutatingBase===!0){let resolved=await fetchAndResolveBaseSha(deps,options.baseBranch);if(!resolved.ok)return{ok:!1,error:resolved.error};effectiveBaseStartPoint=resolved.base_sha}else{let refresh=await refreshBaseBranch(deps,{refreshMain:options.refreshMain,baseBranch:options.baseBranch});if(!refresh.ok)return{ok:!1,error:refresh.error}}await emitClaudeReviewWorkflowDriftAdvisory(deps,options.baseBranch,overrides);let createWorktreesFn=overrides.createWorktrees??createWorktrees,provisionCommandsFn=overrides.provisionCommands??((rows2,d)=>provisionCommandsForCreatedWorktrees(rows2,buildCommandProvisioningDeps(d),agent.name==="codex")),provisionFn=overrides.provisionMcpRegistrations??((rows2,d)=>provisionMcpRegistrationsForCreatedWorktrees(rows2,buildMcpProvisioningDeps(d,bridgeApiBaseUrl))),materializeFn=overrides.materializeFileCredentials??materializeFileCredentialsForCreatedWorktrees,detectTerminalFn=overrides.detectTerminal??detectTerminal,spawnTabsFn=overrides.spawnTabsForCreatedWorktrees??spawnTabsForCreatedWorktrees,resumeWorktreesFn=overrides.resumeWorktrees??resumeWorktrees,created=options.resumeMode?await resumeWorktreesFn(deps,options):await createWorktreesFn(deps,options,platformConfig.config.worktrunkBinary,effectiveBaseStartPoint),commandProvisioned=await provisionCommandsFn(created,deps),provisioned=await provisionFn(commandProvisioned,deps),materialized=await materializeFn(provisioned,deps);if(options.epic?.dispatch_key&&!overrides.createConductorContext)return{ok:!1,error:"Epic dispatch requires conductor context (createConductorContext seam must be injected when dispatch_key is set)"};let terminal=detectTerminalFn(options.terminal,deps.env),conductorReady;if(overrides.createConductorContext){let provisionResult=await provisionConductorForRows(materialized,deps,options,agent,overrides);if(options.epic?.dispatch_key&&!provisionResult.context)return{ok:!1,error:"Epic dispatch correlation unavailable: conductor provisioning failed (cannot claim dispatch without run_id)"};if(provisionResult.context){if(options.epic?.dispatch_key){let claimFn=overrides.claimEpicDispatch??defaultClaimEpicDispatch;try{await claimFn(options.epic.dispatch_key,provisionResult.context.runId,deps)}catch(e){return{ok:!1,error:`Epic dispatch correlation failed (idempotency guard): ${e instanceof Error?e.message:"epic dispatch correlation failed"}`}}}let supervisorStatus=await launchSupervisorTab(provisionResult.context,deps,options,terminal);conductorReady=stampSupervisorStatus(provisionResult.rows,supervisorStatus)}else conductorReady=provisionResult.rows}else conductorReady=materialized;let routed=await(overrides.resolveModelRoutingForRows??resolveModelRoutingForRows)(deps,conductorReady,options,agent);for(let row2 of routed)row2.status!=="created"||!row2.path||overrides.modelRoutingLog?.(formatModelRoutingLine(row2,agent,{workflow:options.workflow}));overrides.modelRoutingWarningLog&&emitInvocationModelRoutingDiagnosticForRows(routed,overrides.modelRoutingWarningLog);let scoped=stampIndexScopeOnRows(routed,indexScope);if(agent.name==="codex"){let mcpDeps=buildMcpProvisioningDeps(deps),resolved=[];for(let row2 of scoped){if(row2.status!=="created"||!row2.path){resolved.push(row2);continue}let scope=await(overrides.resolveCodexScope??resolveCodexLaunchScope)(row2.path,mcpDeps);resolved.push(scope.ok?{...row2,codexLaunchArgs:scope.argv}:{...row2,status:"spawn-failed",error:scope.error})}scoped=resolved}return{ok:!0,rows:await spawnTabsFn(deps,scoped,terminal,platformConfig.config.buildAgentShellCommand)}}function spawnFailureHintForPlatform(platform){return platform==="darwin"?"One or more tabs failed to spawn. If osascript reported a permission error, grant the required macOS permissions under System Settings -> Privacy & Security, then re-run: Automation (to let the terminal app be controlled) and, for the Terminal.app path, Accessibility (the new-tab step sends Cmd-T via System Events keystrokes, which Automation alone does not cover).":platform==="win32"?"One or more tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is installed and on PATH; see the per-ticket warnings above for details.":platform==="linux"?"One or more tabs failed to spawn. Ensure tmux is installed and on PATH; see the per-ticket warnings above for details.":"One or more tabs failed to spawn; see the per-ticket warnings above for details."}async function runStartTicketsCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseStartTicketsArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getStartTicketsUsage()),1;let options=parsed.options,rawDeps=overrides.deps??createDefaultStartTicketsDeps(),orchestrate=overrides.orchestrate??orchestrateStartTickets,agent=resolveAgentSpec(options.agentName);if(!agent)return errorLog(`Error: Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`),1;let bridgeApiBaseUrl=await resolveStartTicketsBridgeApiBaseUrl(rawDeps,agent),deps=bridgeApiBaseUrl?{...rawDeps,env:{...rawDeps.env,BAPI_BASE_URL:bridgeApiBaseUrl}}:rawDeps;if(options.injectedTier===INJECTED_TIER_UNRESOLVED&&errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and using premium model routing for every ticket."),options.dryRun){let dryRunRepoName=await resolveStartTicketsRepoName2(deps),resolveDryRunRoutingFn=overrides.resolveModelRoutingForDryRun??resolveModelRoutingForDryRun,dryRunRows=buildDryRunResults(options.keys,options.branchOverrides),routedDryRunRows=await resolveDryRunRoutingFn(deps,dryRunRows,options,agent),routedByKey=new Map(routedDryRunRows.map(r=>[r.key,r])),dryRunMcpInvocation=buildMcpProvisioningDeps(deps).mcpServerInvocation;for(let key of options.keys){let branch=resolveBranchForTicket(key,options.branchOverrides),routedRow=routedByKey.get(key),modelAlias=routedRow?.modelAlias??null;for(let line of buildDryRunDetailLines(agent,key,branch,deps.platform,deps.env,options.baseBranch,options.autoApprove,modelAlias,options.conductorEnabled??!1,dryRunRepoName,dryRunMcpInvocation,options.workflow,options.reviewRounds))log(line);log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow??{key,branch,status:"dry-run"},agent,{workflow:options.workflow})}`),modelAlias==null&&routedRow?.modelRoutingReason&&log(`DRY-RUN: ${routedRow.modelRoutingReason}`)}log(""),emitInvocationModelRoutingDiagnosticForRows(routedDryRunRows,errorLog)}let result=await orchestrate(deps,options,{modelRoutingLog:log,modelRoutingWarningLog:errorLog,repoNameWarningLog:errorLog,workflowDriftWarningLog:errorLog,...options.conductorEnabled?{createConductorContext:createStartTicketsConductorContext,provisionConductorHooksForRows,emitStartTicketsRunStarted}:{}});if(!result.ok)return errorLog(`Error: ${result.error}`),1;log(formatSummaryReport(result.rows));let spawned=result.rows.filter(r=>r.status==="spawned");if(deps.platform==="linux"&&spawned.length>0){log(""),log("Linux: each ticket runs in a detached tmux session. Attach with:");for(let row2 of spawned)log(` tmux attach -t ${tmuxSessionNameForTicket(deps,row2.key)}`)}return result.rows.filter(r=>r.status==="spawn-failed").length>0&&(errorLog(""),errorLog(spawnFailureHintForPlatform(deps.platform))),0}var TICKET_KEY_PATTERN3,DEFAULT_MAX_PARALLEL,DEFAULT_TMUX_SESSION_PREFIX,TMUX_SESSION_OVERRIDE_ENV,INJECTED_TIER_UNRESOLVED,STALE_LAUNCH_SCRIPT_MAX_AGE_MS,defaultPruneStaleLaunchScriptsDeps,defaultWriteWorkerLaunchScript,MAX_TERMINAL_COMMAND_BYTES,START_TICKETS_DEFAULT_BASE_URL,TICKET_MODEL_TIER_FETCH_TIMEOUT_MS,CONFIG_FIELD_FETCH_TIMEOUT_MS,CODEX_CATALOG_FETCH_TIMEOUT_MS,CURSOR_MODEL_LIST_TIMEOUT_MS,StartTicketsHttpError,isCreatedRoutingEligible,isDryRunRoutingEligible,init_start_tickets=__esm({"src/start-tickets.ts"(){"use strict";init_base_url();init_version_generated();init_credential_store();init_mcp_identity();init_claude_review_workflow_drift();init_start_tickets_repo();init_mcp_provisioning();init_command_provisioning();init_mcp_server_invocation();init_ticket_key_utils();init_start_tickets_prereqs();init_agent_registry();init_mcp_host_targets();init_codex_launch_scope();init_start_tickets_conductor();init_bridge_api_client();init_pr_base_contract();init_index_scope_contract();init_worktree_core();init_env_file_link();init_base_ref();TICKET_KEY_PATTERN3=TICKET_KEY_PATTERN2,DEFAULT_MAX_PARALLEL=3,DEFAULT_TMUX_SESSION_PREFIX="bridge-start-tickets",TMUX_SESSION_OVERRIDE_ENV="BAPI_TMUX_SESSION",INJECTED_TIER_UNRESOLVED="__unresolved__";STALE_LAUNCH_SCRIPT_MAX_AGE_MS=1440*60*1e3,defaultPruneStaleLaunchScriptsDeps={readdir:dir=>readdir2(dir),stat:p=>stat3(p),rm:(p,options)=>rm2(p,options),now:()=>Date.now()};defaultWriteWorkerLaunchScript=async({platform,key,content})=>{let parent=path20.join(os5.tmpdir(),"bridge-start-tickets");await mkdir3(parent,{recursive:!0});let dir=await mkdtemp(path20.join(parent,"w-")),ext=platform==="win32"?"ps1":"sh",file=path20.join(dir,`launch-${sanitizeKeyForLaunchScript(key)}.${ext}`);return await writeFile3(file,content,{mode:384}),file},MAX_TERMINAL_COMMAND_BYTES=1024;START_TICKETS_DEFAULT_BASE_URL="https://bridgegpt-api.com",TICKET_MODEL_TIER_FETCH_TIMEOUT_MS=75e3,CONFIG_FIELD_FETCH_TIMEOUT_MS=3e4,CODEX_CATALOG_FETCH_TIMEOUT_MS=15e3,CURSOR_MODEL_LIST_TIMEOUT_MS=15e3;StartTicketsHttpError=class extends Error{kind;status;constructor(kind,status){super(`Bridge API request failed (${kind}${typeof status=="number"?`, status ${status}`:""})`),this.name="StartTicketsHttpError",this.kind=kind,this.status=status}};isCreatedRoutingEligible=r=>r.status==="created"&&!!r.path,isDryRunRoutingEligible=r=>r.status==="dry-run"}});function isReviewTicketsAgentName(value){return REVIEW_TICKETS_AGENT_NAMES.includes(value)}function formatReviewTicketsAgentNames(){return REVIEW_TICKETS_AGENT_NAMES.join(", ")}function getReviewTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} review-tickets [flags] KEY [KEY ...]`,"","Flags:"," --auto Auto-approve all review gates (global, applies to all tickets)"," --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: adaptive \u2014 backend routes by difficulty)"," --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)"," --agent claude|cursor-agent|codex Agent command to launch in each tab (default: claude)"," --model VALUE Model alias to pass as --model to the agent (optional passthrough)"," --max-parallel N Max tabs to spawn concurrently (default: 3)"," --dry-run Print intended commands; creates no tabs and opens no sessions",' --base-branch BRANCH Base branch to fetch+pin for grounding (default: config base_branch, else "main")'," --no-refresh-base Skip the parent-fetch-once base pin; restores git-free, in-place-grounded behavior"," -h, --help Show this help","","Examples:",` npx -y ${MCP_PACKAGE_NAME} review-tickets BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --auto --rounds=1 BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --review BAPI-1=auto,rounds=1 --review BAPI-2=rounds=2 BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --dry-run BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --base-branch=develop BAPI-1 BAPI-2`,` npx -y ${MCP_PACKAGE_NAME} review-tickets --no-refresh-base BAPI-1 BAPI-2`,"","Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248).","","Prerequisites (terminal launcher, plus git \u2014 no wt, git-wt, or Worktrunk required):"," macOS osascript, git"," Windows wt.exe or PowerShell, git"," Linux tmux, git","","review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees. It now","requires git on PATH (BAPI-474): before spawning tabs it fetches origin/<base_branch> once and pins a","single base_sha for the whole batch, so every spawned review grounds against the same base tree even if","origin advances mid-batch. Pass --no-refresh-base to skip this and restore the prior git-free behavior.","--dry-run works on any platform, including unsupported ones, and never performs the base-branch fetch."].join(`
3755
3755
  `)}function formatBaseFetchFailureBox(baseBranch,error){let title="review-tickets: base-branch fetch failed",reason=error.replace(/\s*Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip\.?\s*$/i,"").trim(),bodyLines=[`Target base branch: origin/${baseBranch}`,`Reason: ${reason}`,"Remediation: check your network and 'git remote get-url origin', or rerun with --no-refresh-base to evaluate in-place."],contentWidth=Math.max(title.length+2,...bodyLines.map(l=>l.length))+2,horizontal="\u2500".repeat(contentWidth),pad=s=>`\u2502 ${s}${" ".repeat(contentWidth-s.length-1)}\u2502`,lines=[`\u256D${horizontal}\u256E`,pad(`\u2716 ${title}`),`\u251C${horizontal}\u2524`,...bodyLines.map(l=>pad(l)),`\u2570${horizontal}\u256F`];return`${ANSI_BOLD}${ANSI_WARM_CORAL}${lines.join(`
3756
3756
  `)}${ANSI_RESET}`}async function resolveBatchBaseContext(deps,options){if(options.noRefreshBase)return{ok:!0,context:{noRefreshBase:!0}};let effectiveBaseBranch=(options.baseBranch??"").trim();if(effectiveBaseBranch.length===0)try{let accessResult=await resolveStartTicketsBridgeApiAccess(deps);if(accessResult.ok){let configValue=await fetchStartTicketsConfigField(accessResult.access,"base_branch");typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let fetchResult=await fetchAndResolveBaseSha(deps,effectiveBaseBranch);return fetchResult.ok?{ok:!0,context:{baseBranch:effectiveBaseBranch,baseSha:fetchResult.base_sha,noRefreshBase:!1}}:{ok:!1,baseBranch:effectiveBaseBranch,error:fetchResult.error}}function parseReviewOverrideEntry(entry,requestedKeys){let eqIdx=entry.indexOf("=");if(eqIdx<=0)return{ok:!1,error:`Invalid --review value: '${entry}' (expected KEY=auto,rounds=1 or KEY=rounds=2).`};let key=entry.slice(0,eqIdx),rest=entry.slice(eqIdx+1);if(!TICKET_KEY_PATTERN3.test(key))return{ok:!1,error:`Invalid --review override key: '${key}' (keys must match [A-Z]+-[0-9]+).`};if(!requestedKeys.has(key))return{ok:!1,error:`--review override key '${key}' is not one of the requested tickets.`};let subtokens=rest.split(","),override={};for(let token of subtokens){if(token.trim()==="")return{ok:!1,error:`Invalid --review value for '${key}': empty subtoken in '${rest}'.`};if(token==="auto")override.auto=!0;else if(token.startsWith("rounds=")){let val=token.slice(7);if(val!=="1"&&val!=="2")return{ok:!1,error:`Invalid --review rounds for '${key}': '${val}' (must be 1 or 2).`};override.rounds=Number(val)}else return{ok:!1,error:`Invalid --review subtoken for '${key}': '${token}' (supported: auto, rounds=1, rounds=2).`}}return{ok:!0,key,override}}function parseReviewTicketsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getReviewTicketsUsage()};let dryRun=!1,globalAuto=!1,globalRounds,maxParallelRaw,agentName=DEFAULT_AGENT_NAME,modelAlias,noRefreshBase=!1,baseBranch,keys=[],rawReviewEntries=[];for(let i=0;i<argv.length;i++){let arg=argv[i],takeValue3=()=>{if(!(i+1>=argv.length))return i+=1,argv[i]};if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--auto"){globalAuto=!0;continue}if(arg==="--no-refresh-base"){noRefreshBase=!0;continue}if(arg==="--base-branch"||arg.startsWith("--base-branch=")){let value;if(arg.startsWith("--base-branch="))value=arg.slice(14);else{let next=i+1<argv.length?argv[i+1]:void 0;if(next===void 0||next.startsWith("-"))return{status:"error",message:"--base-branch requires a value (a branch name)."};value=takeValue3()}let trimmed=(value??"").trim(),error=validateBranchName(trimmed);if(error)return{status:"error",message:`Invalid --base-branch value: ${error}`};baseBranch=trimmed;continue}if(arg==="--rounds"||arg.startsWith("--rounds=")){let value;if(arg.startsWith("--rounds="))value=arg.slice(9);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--rounds requires a value (1 or 2)."};if(value!=="1"&&value!=="2")return{status:"error",message:`Invalid --rounds value: '${value}' (must be 1 or 2).`};globalRounds=Number(value);continue}if(arg==="--max-parallel"||arg.startsWith("--max-parallel=")){if(arg.startsWith("--max-parallel="))maxParallelRaw=arg.slice(15);else{let value=takeValue3();if(value===void 0)return{status:"error",message:"--max-parallel requires a positive integer value."};maxParallelRaw=value}continue}if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--agent requires a value (an agent name)."};if(!isAgentName(value)||!isReviewTicketsAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatReviewTicketsAgentNames()}).`};agentName=value;continue}if(arg==="--model"||arg.startsWith("--model=")){let value;if(arg.startsWith("--model="))value=arg.slice(8);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--model requires a value (a model alias)."};if(!value||!isValidModelAlias(value))return{status:"error",message:`Invalid --model value: '${value??""}' (must match [A-Za-z0-9._:-]+).`};modelAlias=value;continue}if(arg==="--review"||arg.startsWith("--review=")){let value;if(arg.startsWith("--review="))value=arg.slice(9);else if(value=takeValue3(),value===void 0)return{status:"error",message:"--review requires a value (KEY=auto,rounds=1)."};rawReviewEntries.push(value);continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};keys.push(arg)}if(keys.length===0)return{status:"error",message:"At least one ticket key is required (e.g., BAPI-248)."};let seen=new Set;for(let key of keys){if(!TICKET_KEY_PATTERN3.test(key))return{status:"error",message:`Invalid ticket key: '${key}' (keys must match [A-Z]+-[0-9]+, e.g., BAPI-248).`};if(seen.has(key))return{status:"error",message:`Duplicate ticket key: '${key}'.`};seen.add(key)}let maxParallel=DEFAULT_MAX_PARALLEL;if(maxParallelRaw!==void 0){if(!/^[0-9]+$/.test(maxParallelRaw)||Number(maxParallelRaw)<1)return{status:"error",message:`Invalid --max-parallel value: '${maxParallelRaw}' (must be a positive integer).`};maxParallel=Number(maxParallelRaw)}let reviewOverrides={};for(let entry of rawReviewEntries){let result=parseReviewOverrideEntry(entry,seen);if(!result.ok)return{status:"error",message:result.error};reviewOverrides[result.key]={...reviewOverrides[result.key]??{},...result.override}}return{status:"ok",options:{keys,dryRun,maxParallel,agentName,modelAlias,auto:globalAuto,rounds:globalRounds,reviewOverrides,noRefreshBase,baseBranch}}}function resolveEffectiveReviewMode(key,options){let override=options.reviewOverrides[key],auto=override?.auto!==void 0?override.auto:options.auto,rounds=override?.rounds!==void 0?override.rounds:options.rounds!==void 0?options.rounds:void 0;return{auto,rounds}}function buildReviewTicketPrompt(key,mode,base=NO_BASE_CONTEXT,agentName="claude"){let autoFlag=mode.auto?" --auto":"",roundsFlag=mode.rounds!==void 0?` --rounds=${mode.rounds}`:"",baseBranchFlag=base.baseBranch?` --base-branch=${base.baseBranch}`:"",baseShaFlag=base.baseSha?` --base-sha=${base.baseSha}`:"",noRefreshFlag=base.noRefreshBase?" --no-refresh-base":"";return`${agentName==="codex"?"$bridge-review-ticket":"/review-ticket"} ${key}${autoFlag}${roundsFlag}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`}function buildPosixReviewAgentShellCommand(agent,key,mode,cwd,modelAlias,base=NO_BASE_CONTEXT,launchArgs=[]){let prompt=buildReviewTicketPrompt(key,mode,base,agent.name),invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias,launchArgs);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function buildPowerShellReviewAgentShellCommand(agent,key,mode,modelAlias,base=NO_BASE_CONTEXT,launchArgs=[]){let prompt=buildReviewTicketPrompt(key,mode,base,agent.name);return buildAgentInvocation(agent,prompt,powershellSquote,modelAlias,launchArgs)}function buildReviewAgentShellCommand(agent,key,mode,platform,cwd,modelAlias,base=NO_BASE_CONTEXT,launchArgs=[]){return platform==="win32"?buildPowerShellReviewAgentShellCommand(agent,key,mode,modelAlias,base,launchArgs):buildPosixReviewAgentShellCommand(agent,key,mode,cwd,modelAlias,base,launchArgs)}function unsupportedReviewTicketsPlatformMessage(platform){return unsupportedPlatformMessage(platform).replace(/start-tickets/g,"review-tickets")}async function runReviewTicketsPreflight(deps,options){return options.dryRun?{ok:!0}:isSupportedStartTicketsPlatform(deps.platform)?!options.noRefreshBase&&!await isCommandOnPath(deps,"git")?{ok:!1,error:"git is required for review-tickets' parent-fetch-once base-branch pin (BAPI-474) but was not found on PATH. Install git, or rerun with --no-refresh-base to skip the fetch and evaluate in-place."}:deps.platform==="darwin"?await isCommandOnPath(deps,"osascript")?{ok:!0}:{ok:!1,error:"osascript is required to open a macOS terminal tab for review-tickets but was not found on PATH."}:deps.platform==="win32"?await isCommandOnPath(deps,WINDOWS_TERMINAL_COMMAND)?{ok:!0}:await resolveFirstCommandOnPath(deps,WINDOWS_POWERSHELL_CANDIDATES)?{ok:!0}:{ok:!1,error:"Windows Terminal (wt.exe) or PowerShell is required to open a review-tickets tab, but neither was found on PATH."}:await isCommandOnPath(deps,TMUX_COMMAND)?{ok:!0}:{ok:!1,error:"tmux is required to spawn Linux review-tickets sessions but was not found on PATH. Install tmux and retry."}:{ok:!1,error:unsupportedReviewTicketsPlatformMessage(deps.platform)}}function buildReviewPlanRows(deps,options,agent,status,overrides={},baseContext=NO_BASE_CONTEXT){return options.keys.map(key=>{let mode=resolveEffectiveReviewMode(key,options),baseCommand=buildReviewAgentShellCommand(agent,key,mode,deps.platform,deps.cwd,options.modelAlias,baseContext),command=baseCommand,runId;if(options.epic){runId=(overrides.mintRunId??mintStartTicketsRunId)([key]);let env={...buildEpicIdentityEnv(options.epic),BAPI_CONDUCTOR_RUN_ID:runId};command=injectConductorEnvIntoShellCommand(deps.platform,baseCommand,env)}return{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status,command,runId}})}function previewBaseContextFromOptions(options){return options.noRefreshBase?{noRefreshBase:!0}:options.baseBranch?{baseBranch:options.baseBranch,noRefreshBase:!1}:NO_BASE_CONTEXT}async function orchestrateReviewTickets(deps,options,overrides={}){let agent=resolveAgentSpec(options.agentName);if(!agent)return{ok:!1,error:`Unknown agent: '${options.agentName}'. Valid agents: ${formatReviewTicketsAgentNames()}.`};if(options.epic?.dispatch_key&&options.keys.length>1)return{ok:!1,error:"epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead."};if(options.dryRun)return{ok:!0,rows:buildReviewPlanRows(deps,options,agent,"dry-run",overrides,previewBaseContextFromOptions(options))};let preflight=await runReviewTicketsPreflight(deps,options);if(!preflight.ok)return{ok:!1,error:preflight.error};let codexLaunchArgs=[];if(agent.name==="codex"){let scope=await(overrides.prepareCodexSession??(async d=>{let provision=await provisionCommandsForWorktree(d.cwd,buildCommandProvisioningDeps(d),!0);return provision.ok?resolveCodexLaunchScope(d.cwd,buildMcpProvisioningDeps(d)):{ok:!1,error:provision.error}}))(deps);if(!scope.ok)return{ok:!1,error:scope.error};codexLaunchArgs=scope.argv}let baseContextResult=await(overrides.resolveBaseContext??resolveBatchBaseContext)(deps,options);if(!baseContextResult.ok)return console.error(formatBaseFetchFailureBox(baseContextResult.baseBranch,baseContextResult.error)),{ok:!1,error:baseContextResult.error};let baseContext=baseContextResult.context,terminal=detectTerminal(void 0,deps.env);return{ok:!0,rows:await runWithConcurrency(options.keys,options.maxParallel,async key=>{let mode=resolveEffectiveReviewMode(key,options),baseShellCommand=buildReviewAgentShellCommand(agent,key,mode,deps.platform,deps.cwd,options.modelAlias,baseContext,codexLaunchArgs),shellCommand=baseShellCommand,runId;if(options.epic){runId=(overrides.mintRunId??mintStartTicketsRunId)([key]);let env={...buildEpicIdentityEnv(options.epic),BAPI_CONDUCTOR_RUN_ID:runId};if(shellCommand=injectConductorEnvIntoShellCommand(deps.platform,baseShellCommand,env),options.epic.dispatch_key){if(!overrides.claimEpicDispatch)return{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status:"spawn-failed",command:shellCommand,runId,error:"claimEpicDispatch is required when dispatch_key is present but was not provided"};try{await overrides.claimEpicDispatch(options.epic.dispatch_key,runId,deps)}catch(err){let message=err instanceof Error?err.message:String(err);return{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status:"already-dispatched",command:shellCommand,runId,error:message}}}}let context={key,worktreePath:deps.cwd};try{let result=await deps.spawnTerminalTab(deps,terminal,shellCommand,context);return result.ok?{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status:"spawned",command:shellCommand,runId}:{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status:"spawn-failed",command:shellCommand,runId,error:result.error}}catch(err){let message=err instanceof Error?err.message:String(err);return{key,auto:mode.auto,rounds:mode.rounds,agentName:options.agentName,modelAlias:options.modelAlias,status:"spawn-failed",command:shellCommand,runId,error:message}}})}}function formatReviewTicketsSummaryReport(rows){let lines=["Summary:"];for(let row2 of rows){let modelPart=row2.modelAlias?row2.modelAlias:"default",roundsPart=row2.rounds!==void 0?String(row2.rounds):"adaptive",runIdPart=row2.runId?` run_id=${row2.runId}`:"";lines.push(`${row2.key} auto=${row2.auto} rounds=${roundsPart} agent=${row2.agentName} model=${modelPart} status=${row2.status}${runIdPart}`)}let failedRows=rows.filter(r=>r.status==="spawn-failed"||r.status==="already-dispatched");if(failedRows.length>0){lines.push(""),lines.push("Warnings:");for(let row2 of failedRows)lines.push(` ${row2.key}: ${row2.error??row2.status}`)}return lines.join(`
3757
- `)}function reviewSpawnFailureHintForPlatform(platform){return platform==="darwin"?"One or more review tabs failed to spawn. If osascript reported a permission error, grant the required macOS permissions under System Settings -> Privacy & Security, then re-run: Automation (to let the terminal app be controlled) and Accessibility for the new-tab step.":platform==="win32"?"One or more review tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is installed and on PATH; see the per-ticket warnings above for details.":platform==="linux"?"One or more review sessions failed to spawn. Ensure tmux is installed and on PATH; see the per-ticket warnings above for details.":"One or more review tabs failed to spawn; see the per-ticket warnings above for details."}async function runReviewTicketsCli(argv,overrides={}){let log=m=>console.log(m),errorLog=m=>console.error(m),parsed=parseReviewTicketsArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getReviewTicketsUsage()),1;let options=parsed.options,deps=overrides.deps??createDefaultStartTicketsDeps();if(options.dryRun){let agent=resolveAgentSpec(options.agentName);if(!agent)return errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatReviewTicketsAgentNames()}.`),1;let rows=buildReviewPlanRows(deps,options,agent,"dry-run",overrides.orchestrateOverrides,previewBaseContextFromOptions(options));for(let row2 of rows)log(`DRY-RUN: ${row2.key} -> ${row2.command}`);return log(""),log(formatReviewTicketsSummaryReport(rows)),0}let result=await orchestrateReviewTickets(deps,options,overrides.orchestrateOverrides);if(!result.ok)return errorLog(`Error: ${result.error}`),1;log(formatReviewTicketsSummaryReport(result.rows));let spawned=result.rows.filter(r=>r.status==="spawned");if(deps.platform==="linux"&&spawned.length>0){log(""),log("Linux: each ticket runs in a detached tmux session. Attach with:");for(let row2 of spawned)log(` tmux attach -t ${tmuxSessionNameForTicket(deps,row2.key)}`)}return result.rows.filter(r=>r.status==="spawn-failed").length>0&&(errorLog(""),errorLog(reviewSpawnFailureHintForPlatform(deps.platform))),0}var REVIEW_TICKETS_AGENT_NAMES,NO_BASE_CONTEXT,ANSI_BOLD,ANSI_WARM_CORAL,ANSI_RESET,init_review_tickets=__esm({"src/review-tickets.ts"(){"use strict";init_start_tickets();init_command_provisioning();init_codex_launch_scope();init_start_tickets_prereqs();init_agent_registry();init_start_tickets_conductor();init_mcp_identity();REVIEW_TICKETS_AGENT_NAMES=["claude","cursor-agent","codex"];NO_BASE_CONTEXT={noRefreshBase:!1};ANSI_BOLD="\x1B[1m",ANSI_WARM_CORAL="\x1B[38;5;203m",ANSI_RESET="\x1B[0m"}});function containsAbsolutePath(value){return ABSOLUTE_PATH_PATTERN.test(value)}function containsTransportArtifact(value){return TRANSPORT_ARTIFACT_PATTERN.test(value)}function normalizeProse(value,field,maxLength){if(typeof value!="string")throw new ReadinessCheckValidationError(Array.isArray(value)?`${field} must be a single string, not an array`:`${field} must be a string`);let collapsed=value.replace(/[\u0000-\u001f\u007f]+/g," ").replace(/\s{2,}/g," ").trim();if(collapsed.length===0)throw new ReadinessCheckValidationError(`${field} must not be blank`);if(containsAbsolutePath(collapsed))throw new ReadinessCheckValidationError(`${field} must not contain an absolute path`);if(containsTransportArtifact(collapsed))throw new ReadinessCheckValidationError(`${field} must not contain a request header or a response body`);return collapsed.length>maxLength?`${collapsed.slice(0,maxLength-1).trimEnd()}\u2026`:collapsed}function normalizeReadinessDetail(value){return normalizeProse(value,"detail",READINESS_DETAIL_MAX_LENGTH)}function normalizeReadinessRemediation(value){return normalizeProse(value,"remediation",READINESS_REMEDIATION_MAX_LENGTH)}function createReadinessCheck(input){if(input===null||typeof input!="object")throw new ReadinessCheckValidationError("check must be an object");if(!READINESS_CHECK_SOURCES.includes(input.source))throw new ReadinessCheckValidationError("source is not a supported readiness source");if(!READINESS_CHECK_STATUSES.includes(input.status))throw new ReadinessCheckValidationError("status is not a supported readiness status");if(typeof input.id!="string"||!READINESS_ID_PATTERN.test(input.id))throw new ReadinessCheckValidationError("id is not a stable dotted lowercase identifier");if(!input.id.startsWith(`${input.source}.`))throw new ReadinessCheckValidationError("id namespace does not match its source");let label=normalizeProse(input.label,"label",READINESS_DETAIL_MAX_LENGTH),detail=input.detail===void 0?void 0:normalizeReadinessDetail(input.detail);if(input.status==="pass"){if(input.remediation!==void 0)throw new ReadinessCheckValidationError("a passing check must not carry a remediation");return{id:input.id,source:input.source,label,status:"pass",...detail?{detail}:{}}}if(input.status==="fail"){if(input.remediation===void 0)throw new ReadinessCheckValidationError("a failing check must carry exactly one remediation");return{id:input.id,source:input.source,label,status:"fail",...detail?{detail}:{},remediation:normalizeReadinessRemediation(input.remediation)}}let remediation=input.remediation===void 0?void 0:normalizeReadinessRemediation(input.remediation);return{id:input.id,source:input.source,label,status:input.status,...detail?{detail}:{},...remediation?{remediation}:{}}}function validateReadinessChecks(checks,fallbackSource="conductor"){let validated=[],malformedIndex=0;for(let candidate of checks)try{validated.push(createReadinessCheck(candidate))}catch{let raw=candidate??{},source=READINESS_CHECK_SOURCES.includes(raw.source)?raw.source:fallbackSource;malformedIndex+=1,validated.push(createReadinessCheck({id:`${source}.unreadable-check-${malformedIndex}`,source,label:READINESS_MALFORMED_CHECK_LABEL,status:"fail",detail:READINESS_MALFORMED_CHECK_DETAIL,remediation:READINESS_MALFORMED_CHECK_REMEDIATION}))}return validated}function summarizeReadinessChecks(checks){let counts={pass:0,warn:0,fail:0,skip:0};for(let check of checks)counts[check.status]+=1;return counts}function createReadinessCheckSafely(input){let safeRemediation=()=>input?.status==="pass"?{remediation:void 0}:{remediation:READINESS_UNSAFE_REMEDIATION_REPLACEMENT};try{return createReadinessCheck(input)}catch{}try{return createReadinessCheck({...input,detail:READINESS_UNSAFE_DETAIL_REPLACEMENT})}catch{}try{return createReadinessCheck({...input,...safeRemediation()})}catch{}try{return createReadinessCheck({...input,detail:READINESS_UNSAFE_DETAIL_REPLACEMENT,...safeRemediation()})}catch{}let source=READINESS_CHECK_SOURCES.includes(input?.source)?input.source:"conductor";return createReadinessCheck({id:`${source}.unreadable-check`,source,label:READINESS_MALFORMED_CHECK_LABEL,status:"fail",detail:READINESS_MALFORMED_CHECK_DETAIL,remediation:READINESS_MALFORMED_CHECK_REMEDIATION})}var READINESS_CHECK_STATUSES,READINESS_CHECK_SOURCES,ReadinessCheckValidationError,READINESS_DETAIL_MAX_LENGTH,READINESS_REMEDIATION_MAX_LENGTH,ID_SEGMENT,READINESS_ID_PATTERN,POSIX_PATH_ROOTS,ABSOLUTE_PATH_PATTERN,TRANSPORT_ARTIFACT_PATTERN,READINESS_MALFORMED_CHECK_DETAIL,READINESS_MALFORMED_CHECK_REMEDIATION,READINESS_MALFORMED_CHECK_LABEL,READINESS_UNSAFE_DETAIL_REPLACEMENT,READINESS_UNSAFE_REMEDIATION_REPLACEMENT,init_readiness_check=__esm({"src/readiness-check.ts"(){"use strict";READINESS_CHECK_STATUSES=["pass","warn","fail","skip"],READINESS_CHECK_SOURCES=["install","conductor","plane","server"],ReadinessCheckValidationError=class extends Error{constructor(reason){super(`invalid readiness check: ${reason}`),this.name="ReadinessCheckValidationError"}},READINESS_DETAIL_MAX_LENGTH=400,READINESS_REMEDIATION_MAX_LENGTH=400,ID_SEGMENT="[a-z0-9]+(?:-[a-z0-9]+)*",READINESS_ID_PATTERN=new RegExp(`^${ID_SEGMENT}(?:\\.${ID_SEGMENT})+$`),POSIX_PATH_ROOTS=["Users","home","root","var","tmp","private","opt","usr","etc","srv","mnt","media","Applications","Library","System","Volumes","node_modules"],ABSOLUTE_PATH_PATTERN=new RegExp(`(^|[\\s"'\`(<])(?:\\/(?:${POSIX_PATH_ROOTS.join("|")})\\/|[A-Za-z]:[\\\\/]|file:\\/\\/)`);TRANSPORT_ARTIFACT_PATTERN=/(^|[\s"'`(<])(?:authorization\s*:|x-api-key\s*:|bearer\s+\S|\{\s*"[A-Za-z_$][\w$]*"\s*:)/i;READINESS_MALFORMED_CHECK_DETAIL="this prerequisite reported a result that failed the readiness contract, so its real state is unknown",READINESS_MALFORMED_CHECK_REMEDIATION="re-run `epic-implementer readiness`; if it persists, run `epic-implementer doctor` and `install conductor` to re-collect this locus.",READINESS_MALFORMED_CHECK_LABEL="Unreadable prerequisite";READINESS_UNSAFE_DETAIL_REPLACEMENT="detail withheld \u2014 the source reported it in a form this report may not carry",READINESS_UNSAFE_REMEDIATION_REPLACEMENT="re-run `epic-implementer readiness`; this prerequisite's own fix text could not be carried into this report safely."}});import path21 from"path";async function detectClaudeLogin(deps){let raw;try{raw=await deps.readFile(path21.join(deps.homedir(),".claude.json"))}catch{return{detected:!1}}let parsed;try{parsed=JSON.parse(raw)}catch{return{detected:!1}}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{detected:!1};let oauthAccount=parsed.oauthAccount;return oauthAccount!==null&&typeof oauthAccount=="object"&&!Array.isArray(oauthAccount)?{detected:!0}:{detected:!1}}function formatClaudeLoginAdvisory(result){return result.detected?"Claude login: detected (~/.claude.json has an OAuth account). This is advisory only \u2014 it does not guarantee the next worker spawn will authenticate.":"Claude login: not detected. Run `claude login` on this host, or, on a headless host, export CLAUDE_CODE_OAUTH_TOKEN before starting the executor."}var init_claude_login=__esm({"src/claude-login.ts"(){"use strict"}});import path22 from"path";function buildSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}async function resolveInstallDoctorTarget(deps){let envRepo=deps.env.BAPI_REPO_NAME?.trim(),envBase=deps.env.BAPI_BASE_URL?.trim(),repoName=envRepo&&envRepo.length>0?envRepo:null,repoSource=repoName?"env":null,baseUrl=envBase&&envBase.length>0?envBase:null;if(!repoName||!baseUrl)for(let{relPath,topLevelKey}of MCP_CONFIG_ENV_TARGETS){let raw;try{raw=await deps.readFile(path22.join(deps.cwd,relPath))}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{continue}let servers=parsed&&typeof parsed=="object"?parsed[topLevelKey]:void 0,registrationKey=existingRegistrationKey(servers),envBlock=registrationKey!==null?servers[registrationKey]?.env:void 0;if(envBlock&&(!repoName&&typeof envBlock.BAPI_REPO_NAME=="string"&&envBlock.BAPI_REPO_NAME.trim()&&(repoName=envBlock.BAPI_REPO_NAME.trim(),repoSource="config"),!baseUrl&&typeof envBlock.BAPI_BASE_URL=="string"&&envBlock.BAPI_BASE_URL.trim()&&(baseUrl=envBlock.BAPI_BASE_URL.trim()),repoName&&baseUrl))break}return{repoName,repoSource,baseUrl:baseUrl??DEFAULT_BASE_URL}}function classifyProbeFailure(error){let name=error?.name;return name==="TimeoutError"||name==="AbortError"?"request timed out":"request unavailable"}async function probeGet(deps,url,apiKey){try{let resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(PROBE_TIMEOUT_MS)}),body=null;try{body=await resp.json()}catch{}return{ok:!0,status:resp.status,body}}catch(e){return{ok:!1,error:classifyProbeFailure(e)}}}function summarizeManifestGroups(body){if(!body||typeof body!="object")return null;let groups=body.groups;if(!Array.isArray(groups))return null;let total=0,unset=[];for(let group of groups){let fields=group?.fields;if(Array.isArray(fields))for(let field of fields){let name=field?.field_name;typeof name=="string"&&(total+=1,field.is_set||unset.push(name))}}return{total,unset}}function summarizeIntegrations(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;let total=0,unconfigured=[];for(let item of integrations){let label=item?.label;typeof label=="string"&&(total+=1,item.is_configured||unconfigured.push(label))}return{total,unconfigured}}function readGithubConfiguredFlag(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;for(let item of integrations){if(item?.id!=="github_app")continue;let configured=item.is_configured;return typeof configured=="boolean"?configured:null}return null}async function collectInstallStatusChecks(deps){let checks=[],loginResult=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.readFile});checks.push({id:"claude-login",label:"Claude login",status:loginResult.detected?"PASS":"INFO",detail:formatClaudeLoginAdvisory(loginResult)});let target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return checks.push({id:"identity",label:"Repository identity",status:"SKIP",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs",remediation:"run install-bridge (or set BAPI_REPO_NAME) to configure this project."}),checks;checks.push({id:"identity",label:"Repository identity",status:"PASS",detail:`${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`});let setupUrl=buildSetupUrl(target.baseUrl),credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok){checks.push({id:"credential",label:"Bridge API credential",status:"WARN",detail:`not resolved (${cred.kind})`,remediation:"set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; until then the remaining install checks are skipped."});for(let[id,label]of[["connectivity","Server connectivity"],["bootstrap","Bootstrap config fields"],["indexing","Repository indexing"]])checks.push({id,label,status:"SKIP",detail:"no credential resolved"});return checks}checks.push({id:"credential",label:"Bridge API credential",status:"PASS",detail:`resolved from ${cred.credentials.source} (value never read into the report)`});let apiKey=cred.credentials.apiKey,repoQuery=`repo_name=${encodeURIComponent(target.repoName)}`,ping=await probeGet(deps,`${target.baseUrl}/jira/ping?${repoQuery}`,apiKey);ping.ok?ping.status===200?checks.push({id:"connectivity",label:"Server connectivity",status:"PASS",detail:"ping OK"}):ping.status===401||ping.status===403?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server rejected the resolved key (HTTP ${ping.status})`,remediation:"the key may have been rotated \u2014 re-run install-bridge with a current key."}):ping.status===404?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server does not know repo '${target.repoName}' (HTTP 404)`,remediation:`create the project at ${setupUrl} (setup UI), or check BAPI_REPO_NAME spelling.`}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unexpected HTTP ${ping.status} from /jira/ping`,remediation:"the server answered but not with a status this check understands \u2014 re-run doctor; if it persists the Bridge API is unhealthy and no install fact below can be trusted."}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unreachable (${ping.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});let manifest=await probeGet(deps,`${target.baseUrl}/jira/config/install-manifest?${repoQuery}`,apiKey);if(!manifest.ok)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`manifest unreachable (${manifest.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});else if(manifest.status===404)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"no configuration row found for this repository",remediation:`create the project at ${setupUrl} (setup UI), then run /install-bridge.`});else if(manifest.status===200){let summary=summarizeManifestGroups(manifest.body);summary?summary.unset.length===0?checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"PASS",detail:`${summary.total}/${summary.total} bootstrap fields set`}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`${summary.total-summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,remediation:"run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave)."}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"manifest response had an unexpected shape",remediation:"the manifest could not be read, so no bootstrap field state is known \u2014 re-run doctor, then run /install-bridge to re-derive the project configuration."})}else checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`unexpected HTTP ${manifest.status} from the install manifest`,remediation:"the manifest endpoint answered with an unrecognized status \u2014 re-run doctor; if it persists, re-run /install-bridge for this repository."});if(manifest.ok&&manifest.status===200){let integrations=summarizeIntegrations(manifest.body);integrations===null?checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"the manifest response carried no integrations checklist"}):integrations.unconfigured.length===0?checks.push({id:"integrations",label:"Integration credentials",status:"PASS",detail:`${integrations.total}/${integrations.total} configured`}):checks.push({id:"integrations",label:"Integration credentials",status:"WARN",detail:`not configured: ${integrations.unconfigured.join(", ")}`,remediation:`a human configures these at ${setupUrl} (setup UI \u2192 project settings) \u2014 Bridge API never accepts integration secrets through an agent or MCP tool.`})}else checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"manifest unavailable"});if(manifest.ok&&manifest.status===200){let github=readGithubConfiguredFlag(manifest.body);github===null?checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"the manifest response carried no GitHub integration entry"}):github?checks.push({id:"github",label:"GitHub connection",status:"PASS",detail:"a GitHub repository is connected to this project"}):checks.push({id:"github",label:"GitHub connection",status:"WARN",detail:"no GitHub repository is connected to this project",remediation:`run 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${target.repoName}'.`})}else checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"manifest unavailable"});let parse=await probeGet(deps,`${target.baseUrl}/jira/parse-status?${repoQuery}`,apiKey);if(!parse.ok||parse.status!==200)checks.push({id:"indexing",label:"Repository indexing",status:"SKIP",detail:parse.ok?`HTTP ${parse.status} from /jira/parse-status`:`unreachable (${parse.error})`});else if(parse.body?.status==="in_progress"){let startedAt=parse.body.started_at;checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:`parse job in progress${typeof startedAt=="string"?` (started ${startedAt})`:""}`})}else checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:'no parse job currently running \u2014 if this repository has never been indexed, queue one with /parse-repository (monitor with parse_repository action "status").'});return checks}function formatInstallStatusReport(checks){let lines=["","Install status (easy-install done criteria \u2014 advisory)",""],pad={PASS:"PASS ",WARN:"WARN ",INFO:"INFO ",SKIP:"SKIPPED"};for(let check of checks)lines.push(`${pad[check.status]} ${check.label}${check.detail?` \u2014 ${check.detail}`:""}`),check.remediation&&lines.push(` ${check.remediation}`);return lines.push(""),lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only."),lines.join(`
3757
+ `)}function reviewSpawnFailureHintForPlatform(platform){return platform==="darwin"?"One or more review tabs failed to spawn. If osascript reported a permission error, grant the required macOS permissions under System Settings -> Privacy & Security, then re-run: Automation (to let the terminal app be controlled) and Accessibility for the new-tab step.":platform==="win32"?"One or more review tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is installed and on PATH; see the per-ticket warnings above for details.":platform==="linux"?"One or more review sessions failed to spawn. Ensure tmux is installed and on PATH; see the per-ticket warnings above for details.":"One or more review tabs failed to spawn; see the per-ticket warnings above for details."}async function runReviewTicketsCli(argv,overrides={}){let log=m=>console.log(m),errorLog=m=>console.error(m),parsed=parseReviewTicketsArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getReviewTicketsUsage()),1;let options=parsed.options,deps=overrides.deps??createDefaultStartTicketsDeps();if(options.dryRun){let agent=resolveAgentSpec(options.agentName);if(!agent)return errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatReviewTicketsAgentNames()}.`),1;let rows=buildReviewPlanRows(deps,options,agent,"dry-run",overrides.orchestrateOverrides,previewBaseContextFromOptions(options));for(let row2 of rows)log(`DRY-RUN: ${row2.key} -> ${row2.command}`);return log(""),log(formatReviewTicketsSummaryReport(rows)),0}let result=await orchestrateReviewTickets(deps,options,overrides.orchestrateOverrides);if(!result.ok)return errorLog(`Error: ${result.error}`),1;log(formatReviewTicketsSummaryReport(result.rows));let spawned=result.rows.filter(r=>r.status==="spawned");if(deps.platform==="linux"&&spawned.length>0){log(""),log("Linux: each ticket runs in a detached tmux session. Attach with:");for(let row2 of spawned)log(` tmux attach -t ${tmuxSessionNameForTicket(deps,row2.key)}`)}return result.rows.filter(r=>r.status==="spawn-failed").length>0&&(errorLog(""),errorLog(reviewSpawnFailureHintForPlatform(deps.platform))),0}var REVIEW_TICKETS_AGENT_NAMES,NO_BASE_CONTEXT,ANSI_BOLD,ANSI_WARM_CORAL,ANSI_RESET,init_review_tickets=__esm({"src/review-tickets.ts"(){"use strict";init_start_tickets();init_command_provisioning();init_codex_launch_scope();init_start_tickets_prereqs();init_agent_registry();init_start_tickets_conductor();init_mcp_identity();REVIEW_TICKETS_AGENT_NAMES=["claude","cursor-agent","codex"];NO_BASE_CONTEXT={noRefreshBase:!1};ANSI_BOLD="\x1B[1m",ANSI_WARM_CORAL="\x1B[38;5;203m",ANSI_RESET="\x1B[0m"}});function containsAbsolutePath(value){return ABSOLUTE_PATH_PATTERN.test(value)}function containsTransportArtifact(value){return TRANSPORT_ARTIFACT_PATTERN.test(value)}function normalizeProse(value,field,maxLength){if(typeof value!="string")throw new ReadinessCheckValidationError(Array.isArray(value)?`${field} must be a single string, not an array`:`${field} must be a string`);let collapsed=value.replace(/[\u0000-\u001f\u007f]+/g," ").replace(/\s{2,}/g," ").trim();if(collapsed.length===0)throw new ReadinessCheckValidationError(`${field} must not be blank`);if(containsAbsolutePath(collapsed))throw new ReadinessCheckValidationError(`${field} must not contain an absolute path`);if(containsTransportArtifact(collapsed))throw new ReadinessCheckValidationError(`${field} must not contain a request header or a response body`);return collapsed.length>maxLength?`${collapsed.slice(0,maxLength-1).trimEnd()}\u2026`:collapsed}function normalizeReadinessDetail(value){return normalizeProse(value,"detail",READINESS_DETAIL_MAX_LENGTH)}function normalizeReadinessRemediation(value){return normalizeProse(value,"remediation",READINESS_REMEDIATION_MAX_LENGTH)}function createReadinessCheck(input){if(input===null||typeof input!="object")throw new ReadinessCheckValidationError("check must be an object");if(!READINESS_CHECK_SOURCES.includes(input.source))throw new ReadinessCheckValidationError("source is not a supported readiness source");if(!READINESS_CHECK_STATUSES.includes(input.status))throw new ReadinessCheckValidationError("status is not a supported readiness status");if(typeof input.id!="string"||!READINESS_ID_PATTERN.test(input.id))throw new ReadinessCheckValidationError("id is not a stable dotted lowercase identifier");if(!input.id.startsWith(`${input.source}.`))throw new ReadinessCheckValidationError("id namespace does not match its source");let label=normalizeProse(input.label,"label",READINESS_DETAIL_MAX_LENGTH),detail=input.detail===void 0?void 0:normalizeReadinessDetail(input.detail);if(input.status==="pass"){if(input.remediation!==void 0)throw new ReadinessCheckValidationError("a passing check must not carry a remediation");return{id:input.id,source:input.source,label,status:"pass",...detail?{detail}:{}}}if(input.status==="fail"){if(input.remediation===void 0)throw new ReadinessCheckValidationError("a failing check must carry exactly one remediation");return{id:input.id,source:input.source,label,status:"fail",...detail?{detail}:{},remediation:normalizeReadinessRemediation(input.remediation)}}let remediation=input.remediation===void 0?void 0:normalizeReadinessRemediation(input.remediation);return{id:input.id,source:input.source,label,status:input.status,...detail?{detail}:{},...remediation?{remediation}:{}}}function validateReadinessChecks(checks,fallbackSource="conductor"){let validated=[],malformedIndex=0;for(let candidate of checks)try{validated.push(createReadinessCheck(candidate))}catch{let raw=candidate??{},source=READINESS_CHECK_SOURCES.includes(raw.source)?raw.source:fallbackSource;malformedIndex+=1,validated.push(createReadinessCheck({id:`${source}.unreadable-check-${malformedIndex}`,source,label:READINESS_MALFORMED_CHECK_LABEL,status:"fail",detail:READINESS_MALFORMED_CHECK_DETAIL,remediation:READINESS_MALFORMED_CHECK_REMEDIATION}))}return validated}function summarizeReadinessChecks(checks){let counts={pass:0,warn:0,fail:0,skip:0};for(let check of checks)counts[check.status]+=1;return counts}function createReadinessCheckSafely(input){let safeRemediation=()=>input?.status==="pass"?{remediation:void 0}:{remediation:READINESS_UNSAFE_REMEDIATION_REPLACEMENT};try{return createReadinessCheck(input)}catch{}try{return createReadinessCheck({...input,detail:READINESS_UNSAFE_DETAIL_REPLACEMENT})}catch{}try{return createReadinessCheck({...input,...safeRemediation()})}catch{}try{return createReadinessCheck({...input,detail:READINESS_UNSAFE_DETAIL_REPLACEMENT,...safeRemediation()})}catch{}let source=READINESS_CHECK_SOURCES.includes(input?.source)?input.source:"conductor";return createReadinessCheck({id:`${source}.unreadable-check`,source,label:READINESS_MALFORMED_CHECK_LABEL,status:"fail",detail:READINESS_MALFORMED_CHECK_DETAIL,remediation:READINESS_MALFORMED_CHECK_REMEDIATION})}var READINESS_CHECK_STATUSES,READINESS_CHECK_SOURCES,ReadinessCheckValidationError,READINESS_DETAIL_MAX_LENGTH,READINESS_REMEDIATION_MAX_LENGTH,ID_SEGMENT,READINESS_ID_PATTERN,POSIX_PATH_ROOTS,ABSOLUTE_PATH_PATTERN,TRANSPORT_ARTIFACT_PATTERN,READINESS_MALFORMED_CHECK_DETAIL,READINESS_MALFORMED_CHECK_REMEDIATION,READINESS_MALFORMED_CHECK_LABEL,READINESS_UNSAFE_DETAIL_REPLACEMENT,READINESS_UNSAFE_REMEDIATION_REPLACEMENT,init_readiness_check=__esm({"src/readiness-check.ts"(){"use strict";READINESS_CHECK_STATUSES=["pass","warn","fail","skip"],READINESS_CHECK_SOURCES=["install","conductor","plane","server"],ReadinessCheckValidationError=class extends Error{constructor(reason){super(`invalid readiness check: ${reason}`),this.name="ReadinessCheckValidationError"}},READINESS_DETAIL_MAX_LENGTH=400,READINESS_REMEDIATION_MAX_LENGTH=400,ID_SEGMENT="[a-z0-9]+(?:-[a-z0-9]+)*",READINESS_ID_PATTERN=new RegExp(`^${ID_SEGMENT}(?:\\.${ID_SEGMENT})+$`),POSIX_PATH_ROOTS=["Users","home","root","var","tmp","private","opt","usr","etc","srv","mnt","media","Applications","Library","System","Volumes","node_modules"],ABSOLUTE_PATH_PATTERN=new RegExp(`(^|[\\s"'\`(<])(?:\\/(?:${POSIX_PATH_ROOTS.join("|")})\\/|[A-Za-z]:[\\\\/]|file:\\/\\/)`);TRANSPORT_ARTIFACT_PATTERN=/(^|[\s"'`(<])(?:authorization\s*:|x-api-key\s*:|bearer\s+\S|\{\s*"[A-Za-z_$][\w$]*"\s*:)/i;READINESS_MALFORMED_CHECK_DETAIL="this prerequisite reported a result that failed the readiness contract, so its real state is unknown",READINESS_MALFORMED_CHECK_REMEDIATION="re-run `epic-implementer readiness`; if it persists, run `epic-implementer doctor` and `install conductor` to re-collect this locus.",READINESS_MALFORMED_CHECK_LABEL="Unreadable prerequisite";READINESS_UNSAFE_DETAIL_REPLACEMENT="detail withheld \u2014 the source reported it in a form this report may not carry",READINESS_UNSAFE_REMEDIATION_REPLACEMENT="re-run `epic-implementer readiness`; this prerequisite's own fix text could not be carried into this report safely."}});import path21 from"path";async function detectClaudeLogin(deps){let raw;try{raw=await deps.readFile(path21.join(deps.homedir(),".claude.json"))}catch{return{detected:!1}}let parsed;try{parsed=JSON.parse(raw)}catch{return{detected:!1}}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{detected:!1};let oauthAccount=parsed.oauthAccount;return oauthAccount!==null&&typeof oauthAccount=="object"&&!Array.isArray(oauthAccount)?{detected:!0}:{detected:!1}}function formatClaudeLoginAdvisory(result){return result.detected?"Claude login: detected (~/.claude.json has an OAuth account). This is advisory only \u2014 it does not guarantee the next worker spawn will authenticate.":"Claude login: not detected. Run `claude login` on this host, or, on a headless host, export CLAUDE_CODE_OAUTH_TOKEN before starting the executor."}var init_claude_login=__esm({"src/claude-login.ts"(){"use strict"}});import path22 from"path";function buildSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}async function resolveInstallDoctorTarget(deps){let envRepo=deps.env.BAPI_REPO_NAME?.trim(),envBase=deps.env.BAPI_BASE_URL?.trim(),repoName=envRepo&&envRepo.length>0?envRepo:null,repoSource=repoName?"env":null,baseUrl=envBase&&envBase.length>0?envBase:null;if(!repoName||!baseUrl)for(let{relPath,topLevelKey}of MCP_CONFIG_ENV_TARGETS){let raw;try{raw=await deps.readFile(path22.join(deps.cwd,relPath))}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{continue}let servers=parsed&&typeof parsed=="object"?parsed[topLevelKey]:void 0,registrationKey=existingRegistrationKey(servers),envBlock=registrationKey!==null?servers[registrationKey]?.env:void 0;if(envBlock&&(!repoName&&typeof envBlock.BAPI_REPO_NAME=="string"&&envBlock.BAPI_REPO_NAME.trim()&&(repoName=envBlock.BAPI_REPO_NAME.trim(),repoSource="config"),!baseUrl&&typeof envBlock.BAPI_BASE_URL=="string"&&envBlock.BAPI_BASE_URL.trim()&&(baseUrl=envBlock.BAPI_BASE_URL.trim()),repoName&&baseUrl))break}return{repoName,repoSource,baseUrl:(baseUrl??DEFAULT_BASE_URL).replace(/\/+$/,"")}}function classifyProbeFailure(error){let name=error?.name;return name==="TimeoutError"||name==="AbortError"?"request timed out":"request unavailable"}async function probeGet(deps,url,apiKey){try{let resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(PROBE_TIMEOUT_MS)}),body=null;try{body=await resp.json()}catch{}return{ok:!0,status:resp.status,body}}catch(e){return{ok:!1,error:classifyProbeFailure(e)}}}function summarizeManifestGroups(body){if(!body||typeof body!="object")return null;let groups=body.groups;if(!Array.isArray(groups))return null;let total=0,unset=[];for(let group of groups){let fields=group?.fields;if(Array.isArray(fields))for(let field of fields){let name=field?.field_name;typeof name=="string"&&(total+=1,field.is_set||unset.push(name))}}return{total,unset}}function summarizeIntegrations(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;let total=0,unconfigured=[];for(let item of integrations){let label=item?.label;typeof label=="string"&&(total+=1,item.is_configured||unconfigured.push(label))}return{total,unconfigured}}function readGithubConfiguredFlag(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;for(let item of integrations){if(item?.id!=="github_app")continue;let configured=item.is_configured;return typeof configured=="boolean"?configured:null}return null}async function collectInstallStatusChecks(deps){let checks=[],loginResult=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.readFile});checks.push({id:"claude-login",label:"Claude login",status:loginResult.detected?"PASS":"INFO",detail:formatClaudeLoginAdvisory(loginResult)});let target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return checks.push({id:"identity",label:"Repository identity",status:"SKIP",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs",remediation:"run install-bridge (or set BAPI_REPO_NAME) to configure this project."}),checks;checks.push({id:"identity",label:"Repository identity",status:"PASS",detail:`${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`});let setupUrl=buildSetupUrl(target.baseUrl),credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok){checks.push({id:"credential",label:"Bridge API credential",status:"WARN",detail:`not resolved (${cred.kind})`,remediation:"set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; until then the remaining install checks are skipped."});for(let[id,label]of[["connectivity","Server connectivity"],["bootstrap","Bootstrap config fields"],["indexing","Repository indexing"]])checks.push({id,label,status:"SKIP",detail:"no credential resolved"});return checks}checks.push({id:"credential",label:"Bridge API credential",status:"PASS",detail:`resolved from ${cred.credentials.source} (value never read into the report)`});let apiKey=cred.credentials.apiKey,repoQuery=`repo_name=${encodeURIComponent(target.repoName)}`,ping=await probeGet(deps,`${target.baseUrl}/jira/ping?${repoQuery}`,apiKey);ping.ok?ping.status===200?checks.push({id:"connectivity",label:"Server connectivity",status:"PASS",detail:"ping OK"}):ping.status===401||ping.status===403?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server rejected the resolved key (HTTP ${ping.status})`,remediation:"the key may have been rotated \u2014 re-run install-bridge with a current key."}):ping.status===404?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server does not know repo '${target.repoName}' (HTTP 404)`,remediation:`create the project at ${setupUrl} (setup UI), or check BAPI_REPO_NAME spelling.`}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unexpected HTTP ${ping.status} from /jira/ping`,remediation:"the server answered but not with a status this check understands \u2014 re-run doctor; if it persists the Bridge API is unhealthy and no install fact below can be trusted."}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unreachable (${ping.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});let manifest=await probeGet(deps,`${target.baseUrl}/jira/config/install-manifest?${repoQuery}`,apiKey);if(!manifest.ok)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`manifest unreachable (${manifest.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});else if(manifest.status===404)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"no configuration row found for this repository",remediation:`create the project at ${setupUrl} (setup UI), then run /install-bridge.`});else if(manifest.status===200){let summary=summarizeManifestGroups(manifest.body);summary?summary.unset.length===0?checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"PASS",detail:`${summary.total}/${summary.total} bootstrap fields set`}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`${summary.total-summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,remediation:"run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave)."}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"manifest response had an unexpected shape",remediation:"the manifest could not be read, so no bootstrap field state is known \u2014 re-run doctor, then run /install-bridge to re-derive the project configuration."})}else checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`unexpected HTTP ${manifest.status} from the install manifest`,remediation:"the manifest endpoint answered with an unrecognized status \u2014 re-run doctor; if it persists, re-run /install-bridge for this repository."});if(manifest.ok&&manifest.status===200){let integrations=summarizeIntegrations(manifest.body);integrations===null?checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"the manifest response carried no integrations checklist"}):integrations.unconfigured.length===0?checks.push({id:"integrations",label:"Integration credentials",status:"PASS",detail:`${integrations.total}/${integrations.total} configured`}):checks.push({id:"integrations",label:"Integration credentials",status:"WARN",detail:`not configured: ${integrations.unconfigured.join(", ")}`,remediation:`a human configures these at ${setupUrl} (setup UI \u2192 project settings) \u2014 Bridge API never accepts integration secrets through an agent or MCP tool.`})}else checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"manifest unavailable"});if(manifest.ok&&manifest.status===200){let github=readGithubConfiguredFlag(manifest.body);github===null?checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"the manifest response carried no GitHub integration entry"}):github?checks.push({id:"github",label:"GitHub connection",status:"PASS",detail:"a GitHub repository is connected to this project"}):checks.push({id:"github",label:"GitHub connection",status:"WARN",detail:"no GitHub repository is connected to this project",remediation:`run 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${target.repoName}'.`})}else checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"manifest unavailable"});let parse=await probeGet(deps,`${target.baseUrl}/jira/parse-status?${repoQuery}`,apiKey);if(!parse.ok||parse.status!==200)checks.push({id:"indexing",label:"Repository indexing",status:"SKIP",detail:parse.ok?`HTTP ${parse.status} from /jira/parse-status`:`unreachable (${parse.error})`});else if(parse.body?.status==="in_progress"){let startedAt=parse.body.started_at;checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:`parse job in progress${typeof startedAt=="string"?` (started ${startedAt})`:""}`})}else checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:'no parse job currently running \u2014 if this repository has never been indexed, queue one with /parse-repository (monitor with parse_repository action "status").'});return checks}function formatInstallStatusReport(checks){let lines=["","Install status (easy-install done criteria \u2014 advisory)",""],pad={PASS:"PASS ",WARN:"WARN ",INFO:"INFO ",SKIP:"SKIPPED"};for(let check of checks)lines.push(`${pad[check.status]} ${check.label}${check.detail?` \u2014 ${check.detail}`:""}`),check.remediation&&lines.push(` ${check.remediation}`);return lines.push(""),lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only."),lines.join(`
3758
3758
  `)}function formatInstallStatusFallbackReport(){return formatInstallStatusReport([{id:"collection",label:"Install status",status:"SKIP",detail:"the advisory install-status probes could not be collected",remediation:"re-run doctor; if it persists, run install-bridge to re-verify this project."}])}function projectInstallStatus(check){return check.status==="PASS"?{status:"pass"}:check.status==="WARN"?{status:"warn",...check.remediation?{remediation:check.remediation}:{}}:check.status==="INFO"?check.id==="claude-login"?{status:"warn",remediation:INSTALL_CLAUDE_LOGIN_REMEDIATION}:{status:"pass"}:check.id==="identity"?{status:"fail",remediation:check.remediation??INSTALL_IDENTITY_REMEDIATION}:{status:"skip",...check.remediation?{remediation:check.remediation}:{}}}function mapInstallStatusChecksToReadinessChecks(checks,collectorUnavailable=!1){let byId=new Map;for(let check of checks??[])byId.has(check.id)||byId.set(check.id,check);return INSTALL_READINESS_DESCRIPTORS.map(({id,label})=>{let source=byId.get(id);if(!source)return collectorUnavailable?createReadinessCheck({id:`install.${id}`,source:"install",label,status:"fail",detail:INSTALL_UNAVAILABLE_DETAIL,remediation:INSTALL_UNAVAILABLE_REMEDIATION}):createReadinessCheck({id:`install.${id}`,source:"install",label,status:"skip",detail:INSTALL_NOT_COLLECTED_DETAIL});let projected=projectInstallStatus(source);return createReadinessCheckSafely({id:`install.${id}`,source:"install",label:source.label||label,status:projected.status,...source.detail?{detail:source.detail}:{},...projected.status==="pass"?{}:projected.remediation?{remediation:projected.remediation}:projected.status==="fail"?{remediation:INSTALL_IDENTITY_REMEDIATION}:{}})})}var DEFAULT_BASE_URL,MCP_CONFIG_ENV_TARGETS,PROBE_TIMEOUT_MS,INSTALL_READINESS_DESCRIPTORS,INSTALL_IDENTITY_REMEDIATION,INSTALL_CLAUDE_LOGIN_REMEDIATION,INSTALL_NOT_COLLECTED_DETAIL,INSTALL_UNAVAILABLE_DETAIL,INSTALL_UNAVAILABLE_REMEDIATION,init_install_doctor=__esm({"src/install-doctor.ts"(){"use strict";init_readiness_check();init_credential_store();init_claude_login();init_mcp_host_targets();init_mcp_identity();DEFAULT_BASE_URL="https://bridgegpt-api.com";MCP_CONFIG_ENV_TARGETS=getProjectJsonTargets().map(target=>({relPath:target.relPath,topLevelKey:hostAdapterForTarget(target).topLevelKey})),PROBE_TIMEOUT_MS=5e3;INSTALL_READINESS_DESCRIPTORS=[{id:"claude-login",label:"Claude login"},{id:"identity",label:"Repository identity"},{id:"credential",label:"Bridge API credential"},{id:"connectivity",label:"Server connectivity"},{id:"bootstrap",label:"Bootstrap config fields"},{id:"integrations",label:"Integration credentials"},{id:"github",label:"GitHub connection"},{id:"indexing",label:"Repository indexing"}],INSTALL_IDENTITY_REMEDIATION="run /install-bridge (or set BAPI_REPO_NAME) to configure this project; without a repository identity no server-side conductor fact can be resolved.",INSTALL_CLAUDE_LOGIN_REMEDIATION="run `claude login` on the host that will run conductor workers; Bridge stores no Anthropic credential and cannot confirm this for you.",INSTALL_NOT_COLLECTED_DETAIL="not collected \u2014 an earlier install prerequisite stopped the checklist",INSTALL_UNAVAILABLE_DETAIL="unknown \u2014 the install-status checklist could not be collected at all",INSTALL_UNAVAILABLE_REMEDIATION="run `doctor` to collect the advisory install-status checklist directly; this prerequisite's state is unknown, not healthy."}});function supported(value){return{supported:!0,value}}function readStrategyId(value){if(!value||typeof value!="object")return null;let candidate=value.strategyId;return typeof candidate=="string"&&candidate.length>0?candidate:null}function validateExecutorAdapterCapabilities(adapter){let identity=adapter.identity,refusals=[],capabilities=[];for(let capability of REQUIRED_ADAPTER_CAPABILITIES){let declared=adapter[capability];if(declared==null){refusals.push({capability,kind:"absent",reason:`required capability '${capability}' was not declared by adapter '${identity.adapterId}'`}),capabilities.push({capability,supported:!1,strategyId:null,reason:"not declared"});continue}if(declared.supported!==!0){let reason=typeof declared.reason=="string"&&declared.reason.length>0?declared.reason:`capability '${capability}' is declared unsupported`,refusal={capability,kind:"unsupported",reason};typeof declared.strategyId=="string"&&(refusal.strategyId=declared.strategyId),refusals.push(refusal),capabilities.push({capability,supported:!1,strategyId:typeof declared.strategyId=="string"?declared.strategyId:null,reason});continue}capabilities.push({capability,supported:!0,strategyId:readStrategyId(declared.value),reason:null})}return refusals.length>0?{ok:!1,identity,refusals,capabilities}:{ok:!0,identity,capabilities}}function formatAdapterRefusal(identity,refusals){let names=refusals.map(r=>`${r.capability} (${r.kind})`).join(", ");return`executor agent adapter '${identity.adapterId}' for agent '${identity.agentId}' is incomplete: ${names}. A missing or unsupported capability is never defaulted to host or Claude behavior.`}function redactAdapterDiagnostic(text3,parentEnv,redaction){if(typeof text3!="string"||text3.length===0)return"";let output=text3;for(let name of redaction.secretEnvNames){let value=parentEnv[name];typeof value!="string"||value.length===0||output.includes(value)&&(output=output.split(value).join(redaction.replacement))}return output}var REQUIRED_ADAPTER_CAPABILITIES,init_executor_adapter=__esm({"src/agent-launchers/executor-adapter.ts"(){"use strict";REQUIRED_ADAPTER_CAPABILITIES=Object.freeze(["executable","platform","headlessInvocation","mcpScoping","mcpInitParsing","auth","authFailureDetection","denyEnforcement","redaction","lifecycle"])}});function capabilityValue(declared){return!declared||declared.supported!==!0?null:declared.value}function safeFailure(error,env,redaction){let raw=error instanceof Error?error.message:String(error??"unknown error");return(redaction?redactAdapterDiagnostic(raw,env,redaction):raw).slice(0,FAILURE_DETAIL_MAX)}function summarizeAuth(auth){return{managedCarriers:auth.managedAuthCarriers.map(carrier=>({carrierId:carrier.carrierId,billingClass:carrier.billingClass,ownership:carrier.ownership,secretEnvNames:carrier.secretEnvNames,validationStrategyId:carrier.validationStrategyId})),passthroughs:auth.operatorOwnedPassthroughs.map(passthrough=>({passthroughId:passthrough.passthroughId,envName:passthrough.envName,billingClass:passthrough.billingClass,ownership:passthrough.ownership,rule:passthrough.rule}))}}function inspectionForFailedResolution(agentName,resolution){return{identity:{agentId:agentName,adapterId:"(unresolved)",adapterVersion:"(unresolved)",strategyId:"(unresolved)"},ok:!1,capabilities:[],refusals:resolution.refusals??[],executable:null,platform:null,auth:null,lifecycleKind:null,warnings:[resolution.message]}}async function inspectExecutorAgentAdapter(adapter,deps,options={}){let validation=validateExecutorAdapterCapabilities(adapter),redaction=capabilityValue(adapter.redaction),warnings=[],base={identity:adapter.identity,ok:validation.ok,capabilities:validation.capabilities,refusals:validation.ok?[]:validation.refusals,executable:null,platform:null,auth:null,lifecycleKind:null,warnings};if(!validation.ok)return{inspection:base,denyEnforcement:null};let auth=capabilityValue(adapter.auth);auth&&(base.auth=summarizeAuth(auth));let lifecycle=capabilityValue(adapter.lifecycle);lifecycle&&(base.lifecycleKind=lifecycle.declaration.kind);let platformCapability=capabilityValue(adapter.platform);platformCapability&&(base.platform=platformCapability.evaluate(deps.platform));let executableCapability=capabilityValue(adapter.executable);if(executableCapability){base.executable={command:executableCapability.executable,version:"",fingerprint:null,probed:!1,failure:null};let platformOk=base.platform===null||base.platform.supported;if(options.probeExecutable===!0&&platformOk)try{let output=await deps.runCommand(executableCapability.executable,[...executableCapability.versionArgv]),identification=executableCapability.parseVersion(output);base.executable={command:executableCapability.executable,version:identification.version,fingerprint:identification.fingerprint,probed:!0,failure:output.exitCode===0?null:`version probe exited ${output.exitCode}`}}catch(error){base.executable={command:executableCapability.executable,version:"",fingerprint:null,probed:!0,failure:safeFailure(error,deps.env,redaction)}}}let denyEnforcement2=null,denyCapability=capabilityValue(adapter.denyEnforcement);if(options.probeDenyEnforcement===!0&&denyCapability)try{denyEnforcement2=await denyCapability.probeEnforcement(options.denyProbeTimeoutMs===void 0?{}:{timeoutMs:options.denyProbeTimeoutMs})}catch(error){denyEnforcement2={enforced:!1,layer:"none",degraded:!0,detail:safeFailure(error,deps.env,redaction),warnings:["deny-enforcement probe threw; enforcement is UNVERIFIED on this host"]}}return{inspection:base,denyEnforcement:denyEnforcement2}}var FAILURE_DETAIL_MAX,init_executor_adapter_inspection=__esm({"src/agent-launchers/executor-adapter-inspection.ts"(){"use strict";init_executor_adapter();FAILURE_DETAIL_MAX=300}});function assertNoNul(value,label){if(value.includes("\0"))throw new Error(`Refusing to generate scheduler content: ${label} contains a NUL byte, which is not allowed.`)}function xmlEscape(value){return assertNoNul(value,"xml value"),value.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")}function posixShellQuote(value){return assertNoNul(value,"posix shell value"),value===""?"''":`'${value.replace(/'/g,"'\\''")}'`}function windowsCmdQuote(value){return assertNoNul(value,"windows cmd value"),`"${value.replace(/"/g,'""')}"`}function systemdQuote(value){return assertNoNul(value,"systemd value"),`"${value.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}var init_platform_escaping=__esm({"src/platform-escaping.ts"(){"use strict"}});var init_prompt=__esm({"src/agent-launchers/prompt.ts"(){"use strict"}});async function resolveCommandOnPath(command,envPath,deps){let pathApi=pathApiForPlatform3(deps.platform),probe=deps.platform==="win32"?"where.exe":"which",env={...deps.env,PATH:envPath};deps.platform==="win32"&&(env.Path=envPath);let result=await deps.runCommand(probe,[command],{env});if(result.exitCode!==0)return null;let candidate=result.stdout.split(/\r?\n/).map(line=>line.trim()).find(line=>line.length>0);return!candidate||!pathApi.isAbsolute(candidate)?null:pathApi.normalize(candidate)}var init_claude=__esm({"src/agent-launchers/claude.ts"(){"use strict";init_worktree_core();init_platform_escaping();init_prompt()}});var AGENT_AUTH,DEFAULT_PROBE_TIMEOUT_MS,init_types=__esm({"src/agent-capabilities/types.ts"(){"use strict";AGENT_AUTH={claude:{kind:"interactive",note:"Claude Code authenticates via keychain/OAuth \u2014 cannot be verified from the environment."},"cursor-agent":{kind:"env",varName:"CURSOR_API_KEY"},codex:{kind:"interactive",note:"Codex authenticates via login/OAuth \u2014 cannot be verified from the environment."}},DEFAULT_PROBE_TIMEOUT_MS=9e4}});import{join}from"node:path";function buildHeadlessArgs(agentName,opts){let fmt=opts.outputFormat??"text";if(agentName==="cursor-agent")return["-p","--output-format",fmt,"--trust","--workspace",opts.cwd,opts.prompt];let args=["-p"];return opts.permissionMode==="auto"?args.push("--permission-mode","auto"):opts.permissionMode==="acceptEdits"?args.push("--permission-mode","acceptEdits"):opts.skipPermissions===!0&&args.push("--dangerously-skip-permissions"),typeof opts.model=="string"&&opts.model.trim().length>0&&args.push("--model",opts.model),fmt==="json"?args.push("--output-format","json"):fmt==="stream-json"&&args.push("--output-format","stream-json","--verbose"),args.push(opts.prompt),args}async function createProbeContext(deps,agent,defaultTimeoutMs=DEFAULT_PROBE_TIMEOUT_MS){let launcherDeps={platform:deps.platform,env:deps.env,runCommand:(file,args,options)=>deps.runCommand(file,args,options)},resolvedBinary=await resolveCommandOnPath(agent.command,deps.env.PATH??"",launcherDeps),createdDirs=[],counter=0;return{ctx:{agent,deps,resolvedBinary,marker(name){return`${name}_${deps.uniqueSuffix()}_${counter++}`},async makeTempProject(seed){let dir=await deps.mkdtemp(join(deps.tmpRoot,"agent-cap-"));return createdDirs.push(dir),seed&&await seed(dir),dir},async runHeadless(opts){let exe=resolvedBinary??agent.command,args=buildHeadlessArgs(agent.name,opts),timeoutMs=opts.timeoutMs??defaultTimeoutMs,controller=new AbortController,timedOut=!1,timer=setTimeout(()=>{timedOut=!0,controller.abort()},timeoutMs),start=deps.now();try{let result=await deps.runCommand(exe,args,{cwd:opts.cwd,env:deps.env,signal:controller.signal}),elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:result.stdout??""}:{kind:"exited",exitCode:result.exitCode,stdout:result.stdout??"",stderr:result.stderr??"",elapsedMs}}catch(err){let elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:""}:{kind:"spawn-error",message:err instanceof Error?err.message:String(err)}}finally{clearTimeout(timer)}}},cleanup:async()=>{for(let dir of createdDirs.splice(0))try{await deps.rm(dir,{recursive:!0,force:!0})}catch{}}}}var init_probe_context=__esm({"src/agent-capabilities/probe-context.ts"(){"use strict";init_claude();init_types()}});import{execFile as execFile2}from"node:child_process";import{mkdtemp as mkdtemp2,rm as rm3,writeFile as writeFile4,mkdir as mkdir4}from"node:fs/promises";import os6 from"node:os";import{randomBytes as randomBytes2}from"node:crypto";function createDefaultAgentCapabilitiesDeps(){let runCommand=(file,args,options)=>new Promise(resolve2=>{execFile2(file,args,{cwd:options?.cwd,env:options?.env??process.env,signal:options?.signal,killSignal:"SIGKILL",maxBuffer:67108864,encoding:"utf-8"},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})});return{platform:process.platform,env:process.env,runCommand,tmpRoot:os6.tmpdir(),mkdtemp:prefix=>mkdtemp2(prefix),rm:(target,opts)=>rm3(target,opts),writeFile:(target,data)=>writeFile4(target,data,"utf-8"),mkdir:(target,opts)=>mkdir4(target,opts).then(()=>{}),now:()=>Date.now(),uniqueSuffix:()=>randomBytes2(3).toString("hex").toUpperCase()}}var init_default_deps=__esm({"src/agent-capabilities/default-deps.ts"(){"use strict"}});import{join as join2}from"node:path";function truncate(text3){let flat=text3.replace(/\s+/g," ").trim();return flat.length>EVIDENCE_MAX?`${flat.slice(0,EVIDENCE_MAX)}\u2026`:flat}function validateStreamJsonLines(stdout){let lines=stdout.split(`
3759
3759
  `).map(line=>line.trim()).filter(line=>line.length>0);if(lines.length===0)return{ok:!1,reason:"no non-empty stream-json lines were emitted"};for(let line of lines)try{JSON.parse(line)}catch{return{ok:!1,reason:"a stream-json line was not parseable JSON"}}return{ok:!0}}function echoCommandBody(marker){return["---","description: Disposable no-op probe command that emits a marker token.","---","This is an agent-capability probe. Do not use any tools.",`As part of your output, include the token ${marker} on its own line.`,"You may also include other content from earlier steps.",""].join(`
3760
3760
  `)}async function seedEchoCommand(ctx,dir,marker){let cmdDir=join2(dir,".claude","commands");await ctx.deps.mkdir(cmdDir,{recursive:!0}),await ctx.deps.writeFile(join2(cmdDir,"echo-test.md"),echoCommandBody(marker))}function nonExitedResult(run){return run.kind==="hang"?{status:"hang",detail:`agent did not exit within the timeout (${run.elapsedMs}ms) \u2014 likely the version-sensitive -p hang`,elapsedMs:run.elapsedMs,evidence:run.partialStdout?truncate(run.partialStdout):void 0}:run.kind==="spawn-error"?{status:"fail",detail:`could not spawn agent: ${run.message}`}:null}function assertMarkers(run,markers,failHint){let nonExited=nonExitedResult(run);if(nonExited)return nonExited;let{exitCode,stdout,elapsedMs}=run;if(exitCode!==0)return{status:"fail",detail:`agent exited ${exitCode} (${failHint})`,elapsedMs,evidence:truncate(stdout)};let missing=markers.filter(m=>!stdout.includes(m));return missing.length>0?{status:"fail",detail:`${failHint}; missing marker(s): ${missing.join(", ")}`,elapsedMs,evidence:truncate(stdout)}:{status:"pass",detail:"markers present, clean exit",elapsedMs}}function denyHookCommand(){return`printf '%s' '${JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"agent-capability deny-enforcement probe fallback: tool call denied."}})}'`}async function seedDenyTargetFile(ctx,dir,marker){await ctx.deps.writeFile(join2(dir,DENY_TARGET_FILE),`${marker}
@@ -3841,8 +3841,8 @@ ${getExecutorUsage()}`),1;let options=parsed.options;options.epicRunIds!==void 0
3841
3841
  doctor performed a PATH-PRESENCE check only: it did not run, read, or
3842
3842
  modify either file, and it cannot delete them for you.`)}return lines.push(" Read-only: this section issues two Bridge GETs and reads local lock state. It"),lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code."),lines.join(`
3843
3843
  `)}async function runDoctorCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseDoctorArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getDoctorUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),agent=resolveAgentSpec(parsed.options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),collection=await collectDoctorResults(deps,parsed.options.agentName);if(log(buildDoctorReportHeader(deps.platform,agent).join(`
3844
- `)),overrides.installStatus!==!1)try{let injectedFs=deps,installDeps={env:overrides.installStatus?.env??deps.env,cwd:overrides.installStatus?.cwd??deps.cwd,platform:overrides.installStatus?.platform??deps.platform,homedir:overrides.installStatus?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.installStatus?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.installStatus?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.installStatus?.fetch??((...args)=>fetch(...args))},checks=await collectInstallStatusChecks(installDeps);log(formatInstallStatusReport(checks))}catch{log(formatInstallStatusFallbackReport())}if(parsed.options.agentName==="codex"){let readCodexFile=deps.readFile??(p=>readFile9(p,"utf-8")),profile=await inspectBridgeCodexProfile(codexHome(deps.env),readCodexFile);log(profile.status==="active"?"Codex Bridge approval profile installed; spawned workers use --profile bridge.":`Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`)}if(log(formatDoctorPrereqSection(deps.platform,collection)),overrides.executorAdapter!==!1)try{let adapterDeps={platform:overrides.executorAdapter?.platform??deps.platform,env:overrides.executorAdapter?.env??deps.env,runCommand:overrides.executorAdapter?.runCommand??(async(command,args)=>{let probe=await deps.runCommand(command,args);return{stdout:probe.stdout??"",stderr:probe.stderr??"",exitCode:probe.exitCode??1}}),...overrides.executorAdapter?.resolveAdapter?{resolveAdapter:overrides.executorAdapter.resolveAdapter}:{}},inspection=await collectExecutorAdapterDiagnostic(agent.name,adapterDeps);log(formatExecutorAdapterDiagnosticReport(inspection))}catch{}let duplicateRegistrationFault=!1;try{let registrationFindings=await inspectRegistrationStatus({cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8"))});duplicateRegistrationFault=hasDuplicateRegistrationFault(registrationFindings),log(formatRegistrationStatusReport(registrationFindings))}catch{log(formatRegistrationStatusReport([]))}try{let launcherDeps={cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8")),probeNpxNoInstall:overrides.launcherProbe?.probeNpxNoInstall??probeNpxNoInstallDefault},launcherInspections=await inspectLauncherCache(launcherDeps);log(formatLauncherCacheReport(launcherInspections))}catch{}if(overrides.localCliOverride!==!1)try{let localCliDeps={env:overrides.localCliOverride?.env??deps.env,runCommand:overrides.localCliOverride?.runCommand??(async(command,args)=>deps.runCommand(command,args))},diagnostic=await collectLocalCliOverrideDiagnostic(localCliDeps);log(formatLocalCliOverrideDiagnosticReport(diagnostic))}catch{}if(overrides.workerContainment!==!1)try{log(formatWorkerContainmentDiagnosticReport(collectWorkerContainmentDiagnostic()))}catch{try{log(formatWorkerContainmentDiagnosticReport(void 0))}catch{}}if(overrides.toolSurface!==!1)try{let injectedFs=deps,toolSurfaceDeps={env:overrides.toolSurface?.env??deps.env,cwd:overrides.toolSurface?.cwd??deps.cwd,platform:overrides.toolSurface?.platform??deps.platform,homedir:overrides.toolSurface?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.toolSurface?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.toolSurface?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.toolSurface?.fetch??((...args)=>fetch(...args))},diagnostic=await collectToolSurfaceDiagnostic(toolSurfaceDeps);log(formatToolSurfaceDiagnosticReport(diagnostic))}catch{log(formatToolSurfaceDiagnosticReport({enabled:parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),reason:"malformed",subtype:"unexpected"}))}if(overrides.executorService!==!1)try{let injectedFs=deps,executorDeps={platform:overrides.executorService?.platform??deps.platform,env:overrides.executorService?.env??deps.env,homedir:overrides.executorService?.homedir??injectedFs.homedir??os12.homedir,readdir:overrides.executorService?.readdir??(p=>readdir5(p)),readFile:overrides.executorService?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.executorService?.stat??injectedFs.stat??(p=>stat5(p)),resolveCredentials:overrides.executorService?.resolveCredentials},executorDiagnostics=await collectExecutorServiceDiagnostics(executorDeps);log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));let loginResult=await detectClaudeLogin({homedir:executorDeps.homedir,readFile:executorDeps.readFile});log(`
3845
- ${formatClaudeLoginAdvisory(loginResult)}`)}catch{log(formatExecutorServiceDiagnosticsReport({status:"skipped",reason:"executor service units could not be enumerated on this host."}))}if(overrides.planeCredentials!==!1)try{let injectedFs=deps,planeDeps={env:overrides.planeCredentials?.env??deps.env,cwd:overrides.planeCredentials?.cwd??deps.cwd,platform:overrides.planeCredentials?.platform??deps.platform,homedir:overrides.planeCredentials?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.planeCredentials?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.planeCredentials?.stat??injectedFs.stat??(p=>stat5(p)),resolveRepoName:overrides.planeCredentials?.resolveRepoName,resolveCredentials:overrides.planeCredentials?.resolveCredentials,readPlaneManifest:overrides.planeCredentials?.readPlaneManifest};log(formatPlaneCredentialDiagnosticReport(await collectPlaneCredentialDiagnostic(planeDeps)))}catch{log(formatPlaneCredentialDiagnosticReport({repo:null,target:null,bridgeCredentialResolved:!1}))}if(overrides.conductEpic!==!1&&collection.ok)try{let injectedFs=deps,conductDeps={env:overrides.conductEpic?.env??deps.env,cwd:overrides.conductEpic?.cwd??deps.cwd,platform:overrides.conductEpic?.platform??deps.platform,homedir:overrides.conductEpic?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.conductEpic?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.conductEpic?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.conductEpic?.readdir??(p=>readdir5(p)),runCommand:overrides.conductEpic?.runCommand??deps.runCommand,resolveAccess:overrides.conductEpic?.resolveAccess,isProcessAlive:overrides.conductEpic?.isProcessAlive};log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)))}catch{log(formatConductEpicDiagnosticReport({ghPresent:!1,ghAuthenticated:!1,worktrunkBinary:resolveWorktrunkBinary(deps.platform,deps.env),worktrunkAvailable:!1,repo:null,bridgeCredentialResolved:!1,stateDirectory:null,stateDirectoryReadable:!1,checkpoints:[],locks:[]}))}if(overrides.operationalAdvisories!==!1&&collection.ok)try{let injectedFs=deps,advisoryDeps={env:overrides.operationalAdvisories?.env??deps.env,cwd:overrides.operationalAdvisories?.cwd??deps.cwd,platform:overrides.operationalAdvisories?.platform??deps.platform,homedir:overrides.operationalAdvisories?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.operationalAdvisories?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.operationalAdvisories?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.operationalAdvisories?.readdir??(p=>readdir5(p)),resolveAccess:overrides.operationalAdvisories?.resolveAccess,isProcessAlive:overrides.operationalAdvisories?.isProcessAlive,fetchImpl:overrides.operationalAdvisories?.fetchImpl,now:overrides.operationalAdvisories?.now};log(formatOperationalAdvisoryReport(await collectOperationalAdvisories(advisoryDeps)))}catch{log(formatOperationalAdvisoryReport({dispatcher:null,conductorObservation:"unavailable",conductors:[],indeterminateLocks:0,legacyDriveHelpers:[]}))}if(overrides.staleBranch!==!1&&parsed.options.staleBranchKeys.length>0&&parsed.options.staleBranchBase!==null){let base=parsed.options.staleBranchBase,staleDeps={runCommand:overrides.staleBranch?.runCommand??deps.runCommand,platform:overrides.staleBranch?.platform??deps.platform,env:overrides.staleBranch?.env??deps.env,cwd:overrides.staleBranch?.cwd??deps.cwd},targets=parsed.options.staleBranchKeys.map(key=>({key,branch:resolveBranchForTicket(key,parsed.options.staleBranchOverrides),baseStartPoint:base}));try{log(formatStaleWorktreeDiagnosticReport(await diagnoseStaleWorktreeBranches(staleDeps,targets)))}catch{log(formatStaleWorktreeDiagnosticReport([]))}}return!collection.ok||duplicateRegistrationFault||collection.results.some(r=>!r.found)?1:0}var DOCTOR_REPORT_TITLE,LEGACY_REGISTRATION_INFO_TEXT,LOCAL_CLI_OVERRIDE_ENV,LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT,SHELL_OPERATOR_PATTERN,LEGACY_DRIVE_HELPER_RELATIVE_PATHS,init_doctor=__esm({"src/doctor.ts"(){"use strict";init_start_tickets();init_version_generated();init_install_doctor();init_codex_bridge_profile();init_executor_adapter_inspection();init_executor_adapter_registry();init_env_flags();init_bridge_api_urls();init_tool_surface_gating();init_credential_store();init_bridge_api_client();init_bridge_client();init_checkpoint_store();init_lock();init_start_tickets_repo();init_worker_containment_diagnostic();init_launcher_config_inspection();init_mcp_identity();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();init_executor();init_credentials();init_claude_login();init_base_ref();init_manifest();init_ticket_key_utils();init_worktree_core();init_stale_worktree_doctor();DOCTOR_REPORT_TITLE="bridge doctor \u2014 read-only diagnostics";LEGACY_REGISTRATION_INFO_TEXT=`This registration remains supported. New installs use \`${MCP_SERVER_NAME}\`. No action is required.`;LOCAL_CLI_OVERRIDE_ENV="BAPI_MCP_CLI",LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT=200,SHELL_OPERATOR_PATTERN=/[;&|<>$`\n\r]/;LEGACY_DRIVE_HELPER_RELATIVE_PATHS=[".local/bin/bapi-runtime-up",".local/bin/bapi-drive"]}});function canonicalizePlanDAG(plan){let nodes=plan.nodes.map(node=>({...node,ticket_key:node.ticket_key.trim(),depends_on:[...node.depends_on].map(k=>k.trim()).sort(),...node.touched_files?{touched_files:[...node.touched_files].sort()}:{}})).sort((a,b)=>a.ticket_key.localeCompare(b.ticket_key)),edges=[...plan.edges].map(e=>({from:e.from.trim(),to:e.to.trim(),...e.kind?{kind:e.kind}:{},...e.overlap_files?{overlap_files:[...e.overlap_files].sort()}:{}})).sort((a,b)=>{let cmp=a.from.localeCompare(b.from);return cmp!==0?cmp:a.to.localeCompare(b.to)});return{plan_version:plan.plan_version,nodes,edges}}function hashPlan(plan){return stableJsonHash(canonicalizePlanDAG(plan))}var init_plan=__esm({"src/conductor/plan.ts"(){"use strict";init_git_ci_types()}});function unreported(id){return{id,state:"unreported",detail:UNREPORTED_DETAIL,remediation:BRING_UP_UNREPORTED_REMEDIATION,refusal:"bring-up-facts-unreported"}}function classifyDatabaseMigration(bringUp){let id="database-migration-current";return bringUp===null?unreported(id):bringUp.migration_readable?bringUp.database_migration_current?{id,state:"ok",detail:"the database is at (or ahead of) the code migration head"}:{id,state:"bad",detail:"the database is BEHIND the server's code migration head",remediation:DATABASE_MIGRATION_BEHIND_REMEDIATION,refusal:"database-migration-behind"}:{id,state:"unreadable",detail:"the migration state could not be read (unknown, not confirmed behind)",remediation:DATABASE_MIGRATION_UNREADABLE_REMEDIATION,refusal:"database-migration-unreadable"}}function classifyWorkerPresent(bringUp){let id="worker-present";return bringUp===null?unreported(id):bringUp.heartbeat_readable?bringUp.worker_present?{id,state:"ok",detail:"a reconciler heartbeat is fresh"}:{id,state:"bad",detail:"0 fresh reconcilers \u2014 no worker is running against this deployment",remediation:WORKER_NOT_PRESENT_REMEDIATION,refusal:"worker-not-present"}:{id,state:"unreadable",detail:"the reconciler heartbeat could not be read (unknown, not confirmed absent)",remediation:WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,refusal:"worker-heartbeat-unreadable"}}function classifySingleTransitionAuthority(bringUp){let id="single-transition-authority";return bringUp===null?unreported(id):bringUp.heartbeat_readable?bringUp.fresh_reconciler_count>=2?{id,state:"bad",detail:"2+ fresh reconcilers \u2014 more than one transition authority is ticking",remediation:SECOND_RECONCILER_REMEDIATION,refusal:"second-reconciler-detected"}:bringUp.fresh_reconciler_count===0?{id,state:"bad",detail:"0 fresh reconcilers \u2014 no transition authority is ticking",remediation:WORKER_NOT_PRESENT_REMEDIATION,refusal:"worker-not-present"}:{id,state:"ok",detail:"exactly 1 fresh reconciler"}:{id,state:"unreadable",detail:"the reconciler heartbeat could not be read (unknown, not confirmed)",remediation:WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,refusal:"worker-heartbeat-unreadable"}}function classifyBringUpFacts(bringUp){return[classifyDatabaseMigration(bringUp),classifyWorkerPresent(bringUp),classifySingleTransitionAuthority(bringUp)]}function firstBringUpRefusal(bringUp){let reasons=new Set(classifyBringUpFacts(bringUp).map(verdict=>verdict.refusal).filter(reason=>reason!==void 0));return BRING_UP_REFUSAL_REASONS.find(reason=>reasons.has(reason))??null}function capitalize(text3){return text3.length===0?text3:text3[0].toUpperCase()+text3.slice(1)}var BRING_UP_RUNBOOK,BRING_UP_REFUSAL_REASONS,BRING_UP_UNREPORTED_REMEDIATION,DATABASE_MIGRATION_UNREADABLE_REMEDIATION,DATABASE_MIGRATION_BEHIND_REMEDIATION,WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,WORKER_NOT_PRESENT_REMEDIATION,SECOND_RECONCILER_REMEDIATION,UNREPORTED_DETAIL,BRING_UP_REFUSAL_MESSAGES,init_bring_up_facts=__esm({"src/conductor/bring-up-facts.ts"(){"use strict";BRING_UP_RUNBOOK="docs/claude/epic-conductor-v2-operator-runbook.md",BRING_UP_REFUSAL_REASONS=["bring-up-facts-unreported","database-migration-unreadable","database-migration-behind","worker-heartbeat-unreadable","worker-not-present","second-reconciler-detected"],BRING_UP_UNREPORTED_REMEDIATION="upgrade the Bridge API deploy to a build that reports the bring-up safety facts; their state is unknown here, not healthy.",DATABASE_MIGRATION_UNREADABLE_REMEDIATION=`retry; the migration state could not be read, which is not the same as confirmed-behind. If it persists, check the applied revision with \`alembic current\` (${BRING_UP_RUNBOOK} \xA73d).`,DATABASE_MIGRATION_BEHIND_REMEDIATION=`confirm the applied revision with \`alembic current\` (not \`alembic heads\`, which reads the migration scripts), apply this checkout's migrations with \`alembic -c alembic.ini upgrade head\`, then retry (${BRING_UP_RUNBOOK} \xA73d).`,WORKER_HEARTBEAT_UNREADABLE_REMEDIATION="retry once the Bridge API database is reachable; the reconciler heartbeat could not be read, so worker presence is unknown, not confirmed absent.",WORKER_NOT_PRESENT_REMEDIATION=`start a worker (\`python worker.py\`, or bring up a local plane) \u2014 a server started with DISABLE_SCHEDULER=true runs none. Create and approve only against a server that has a worker (${BRING_UP_RUNBOOK} \xA73d).`,SECOND_RECONCILER_REMEDIATION=`find and stop the extra reconciler: check \`ps aux | grep worker.py\` for a reparented survivor of an earlier plane (${BRING_UP_RUNBOOK} \xA76), and note that a dev server started without DISABLE_SCHEDULER=true runs a reconciler too. A reconciler stopped in the last 120 seconds still counts \u2014 heartbeat rows are not deleted on shutdown \u2014 so wait and retry.`,UNREPORTED_DETAIL="not reported by this Bridge API deploy";BRING_UP_REFUSAL_MESSAGES={"bring-up-facts-unreported":"the Bridge API did not report the bring-up safety facts (database migration, worker presence, single reconciler), so none of them could be confirmed. "+capitalize(BRING_UP_UNREPORTED_REMEDIATION),"database-migration-unreadable":"database migration: the server could not read whether its database is at the migration head, so this is unknown, not confirmed behind. "+capitalize(DATABASE_MIGRATION_UNREADABLE_REMEDIATION),"database-migration-behind":"database migration: the server's database is BEHIND its code's migration head, so the code would run against a schema that was never applied. "+capitalize(DATABASE_MIGRATION_BEHIND_REMEDIATION),"worker-heartbeat-unreadable":"worker presence: the durable reconciler heartbeat could not be read, so whether a worker is running is unknown, not confirmed absent. "+capitalize(WORKER_HEARTBEAT_UNREADABLE_REMEDIATION),"worker-not-present":"worker presence: no reconciler heartbeat is fresh, so nothing would drive this run. "+capitalize(WORKER_NOT_PRESENT_REMEDIATION),"second-reconciler-detected":"single transition authority: two or more reconcilers are ticking the shared database, and two transition authorities wedge an epic. "+capitalize(SECOND_RECONCILER_REMEDIATION)}}});import{createWriteStream,realpathSync,statSync}from"fs";import{mkdir as mkdir8,open as open3,readdir as readdir6,readFile as readFile11,rename as rename3,stat as stat7,unlink as unlink3,writeFile as writeFile7}from"fs/promises";import{execFile as execFileCb,spawn as nodeSpawn}from"child_process";import net from"net";import os14 from"os";import path32 from"path";import{fileURLToPath as fileURLToPath2}from"url";function createPlaneFsDeps(){return{readFile:filePath=>readFile11(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile7(filePath,data,"utf-8"),createExclusive:async(filePath,data)=>{let handle=await open3(filePath,"wx");try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(from,to)=>rename3(from,to),unlink:filePath=>unlink3(filePath),mkdir:async dirPath=>{await mkdir8(dirPath,{recursive:!0})},stat:async filePath=>{let result=await stat7(filePath);return{mtimeMs:result.mtimeMs,mode:result.mode}},readdir:async dirPath=>(await readdir6(dirPath,{withFileTypes:!0})).map(entry=>({name:entry.name,isDirectory:()=>entry.isDirectory(),isFile:()=>entry.isFile()}))}}function createPlaneClock(){return{now:()=>new Date,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms))}}function createPlaneProcessDeps(){return{isAlive:pid=>isProcessAlive(pid,(target,signal)=>process.kill(target,signal)),signal:(pid,signal)=>{try{return process.kill(pid,signal),"sent"}catch(err){let code=err?.code;return code==="ESRCH"?"no-such-process":code==="EPERM"?"permission-denied":"error"}}}}function probeTcpPort(host,port,timeoutMs){return new Promise(resolve2=>{let socket=new net.Socket,settled=!1,finish=result=>{settled||(settled=!0,socket.destroy(),resolve2(result))};socket.setTimeout(timeoutMs),socket.once("connect",()=>finish({kind:"connected"})),socket.once("timeout",()=>finish({kind:"error",error:"probe timed out"})),socket.once("error",err=>{err.code==="ECONNREFUSED"?finish({kind:"refused"}):finish({kind:"error",error:err.code??"probe failed"})}),socket.connect(port,host)})}function createPlaneExecFile(){return(file,args,options)=>new Promise(resolve2=>{execFileCb(file,args,{cwd:options.cwd,shell:!1,windowsHide:!0,timeout:3e4},(error,stdout,stderr)=>{if(error){let code=error.code;resolve2({ok:!1,error:typeof code=="string"?code:"command failed"});return}resolve2({ok:!0,stdout:String(stdout),stderr:String(stderr)})})})}function createPlaneSpawn(){return(command,args,options)=>nodeSpawn(command,args,{cwd:options.cwd,env:options.env,detached:options.detached,shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function createMemberLogDeps(){return{openAppendStream:absolutePath=>{let stream=createWriteStream(absolutePath,{flags:"a"});return stream.on("error",()=>{}),{write:chunk=>{stream.write(chunk)},end:()=>stream.end()}}}}function registerSignalHandler(signal,handler){return process.on(signal,handler),()=>{process.off(signal,handler)}}function resolvePackageEntrypoint(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath,isFile:overrides.isFile??defaultIsFile},moduleFile=moduleFilePath(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=path32.dirname(moduleFile);path32.basename(moduleFile)==="index.js"&&candidates.push(moduleFile),candidates.push(path32.join(dir,"index.js")),candidates.push(path32.join(dir,"..","index.js"))}typeof deps.argv1=="string"&&deps.argv1.length>0&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical=deps.realpath(candidate);if(canonical!==null&&!seen.has(canonical)&&(seen.add(canonical),deps.isFile(canonical)))return{ok:!0,entrypoint:canonical}}return{ok:!1,candidatesChecked:seen.size}}function moduleFilePath(moduleUrl){try{return fileURLToPath2(moduleUrl)}catch{return null}}function defaultRealpath(filePath){try{return realpathSync(filePath)}catch{return null}}function defaultIsFile(filePath){try{return statSync(filePath).isFile()}catch{return!1}}async function probeAutomationHealth(request){let url;try{let target=new URL("/automation/health",request.baseUrl);target.searchParams.set("repo_name",request.repoName),request.instanceId!==void 0&&(target.searchParams.set("component",request.component),target.searchParams.set("instance_id",request.instanceId)),url=target.toString()}catch{return unclassified({kind:"malformed"})}let doFetch=request.fetchImpl??fetch,response;try{response=await doFetch(url,{method:"GET",headers:{"X-API-Key":request.apiKey,Accept:"application/json"},signal:AbortSignal.timeout(request.timeoutMs??PLANE_HEALTH_PROBE_TIMEOUT_MS)})}catch{return unclassified({kind:"unavailable"})}if(response.status===401||response.status===403)return unclassified({kind:"unauthorized"});if(!response.ok)return unclassified({kind:"unavailable"});let body;try{body=await response.json()}catch{return unclassified({kind:"malformed"})}let reading=readComponentObservation(body,request.component);return reading===null?unclassified({kind:"malformed"}):{result:{kind:"state",state:reading.state},observedAtMs:reading.observedAtMs}}function unclassified(result){return{result,observedAtMs:null}}function readComponentObservation(body,component){if(typeof body!="object"||body===null)return null;let block,timestampKey;if(component==="reconciler")block=body.reconciler,timestampKey="heartbeat_last_seen_at";else{let executor=body.executor;if(typeof executor!="object"||executor===null)return null;block=executor.process_heartbeat,timestampKey="last_seen_at"}if(typeof block!="object"||block===null)return null;let state=block.state;if(typeof state!="string"||!PLANE_HEARTBEAT_HEALTH_STATES.includes(state))return null;let known=state,raw=block[timestampKey];if(raw==null)return known==="fresh"||known==="stale"?null:{state:known,observedAtMs:null};if(typeof raw!="string")return null;let observedAtMs=Date.parse(raw);return Number.isNaN(observedAtMs)?null:{state:known,observedAtMs}}function resolveHostname(){return os14.hostname()}function resolveNodeExecutable(){return process.execPath}function resolveHomedir(){return os14.homedir()}var PLANE_HEALTH_PROBE_TIMEOUT_MS,init_defaults=__esm({"src/plane/defaults.ts"(){"use strict";init_types3();init_manifest();PLANE_HEALTH_PROBE_TIMEOUT_MS=5e3}});function isPlainObject4(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function inactiveConfig(reason){return{enabled:!1,valid:!1,reason,conditions:[],config_hash:null,gate_name:DEFAULT_GATE_NAME}}function coerceConfigObject(value){if(value==null)return{kind:"unset"};if(typeof value=="string"){let trimmed=value.trim();if(trimmed.length===0)return{kind:"unset"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"invalid"}}return isPlainObject4(parsed)?Object.keys(parsed).length===0?{kind:"unset"}:{kind:"object",object:parsed}:{kind:"invalid"}}return isPlainObject4(value)?Object.keys(value).length===0?{kind:"unset"}:{kind:"object",object:value}:{kind:"invalid"}}function parseCiChecksCondition(entry){let rawChecks=entry.required_checks;if(!Array.isArray(rawChecks)||rawChecks.length===0)return null;let normalized=[],seen=new Set;for(let raw of rawChecks){let name=normalizeCheckName(raw);if(name===null||seen.has(name))return null;seen.add(name),normalized.push(name)}return{type:REQUIRED_CI_CHECKS_GREEN,required_checks:normalized}}function normalizeReviewSource(source){return REVIEW_SOURCE_ALIASES[source]??source}function parseReviewStateCondition(entry){let rawSource=entry.source;if(typeof rawSource!="string")return null;let source=normalizeReviewSource(rawSource);if(!VALID_REVIEW_SOURCES.has(source))return null;let condition={type:REVIEW_STATE,source};if(entry.require_sticky_verdict!==void 0){if(typeof entry.require_sticky_verdict!="boolean")return null;condition.require_sticky_verdict=entry.require_sticky_verdict}if(entry.require_native_decision!==void 0){if(typeof entry.require_native_decision!="boolean")return null;condition.require_native_decision=entry.require_native_decision}if(entry.min_approvals!==void 0){if(typeof entry.min_approvals!="number"||!Number.isInteger(entry.min_approvals)||entry.min_approvals<0)return null;condition.min_approvals=entry.min_approvals}if(entry.logic!==void 0){if(entry.logic!=="and")return null;condition.logic="and"}if(entry.verdictless_disposition!==void 0){if(typeof entry.verdictless_disposition!="string"||!VERDICTLESS_DISPOSITIONS.includes(entry.verdictless_disposition))return null;condition.verdictless_disposition=entry.verdictless_disposition}if(condition.source==="combination"){let hasSticky=condition.require_sticky_verdict===!0,hasNative=condition.require_native_decision===!0,hasMin=typeof condition.min_approvals=="number"&&condition.min_approvals>0;if(!hasSticky&&!hasNative&&!hasMin)return null}return condition}function parseConditions(object){let raw=object.conditions;if(!Array.isArray(raw)||raw.length===0)return null;let seenTypes=new Set,parsed=[];for(let entry of raw){if(!isPlainObject4(entry))return null;let type=entry.type;if(typeof type!="string"||seenTypes.has(type))return null;if(type===REQUIRED_CI_CHECKS_GREEN){let condition=parseCiChecksCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else if(type===REVIEW_STATE){let condition=parseReviewStateCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else return null}return parsed}function parseDoneGateConfig(value){let coerced=coerceConfigObject(value);if(coerced.kind==="unset")return inactiveConfig("unset");if(coerced.kind==="invalid")return inactiveConfig("malformed");let object=coerced.object;if(object.enabled!==!0)return object.enabled===!1?inactiveConfig("disabled"):inactiveConfig("invalid: 'enabled' must be the boolean true");let conditions=parseConditions(object);if(conditions===null)return inactiveConfig("invalid: conditions must be a non-empty array of valid, non-duplicate condition objects");let gateName=DEFAULT_GATE_NAME,configHash=stableJsonHash({gate_name:gateName,conditions:conditions.map(c=>{if(c.type===REQUIRED_CI_CHECKS_GREEN)return{type:c.type,required_checks:c.required_checks};let r={type:c.type,source:c.source};return c.require_sticky_verdict!==void 0&&(r.require_sticky_verdict=c.require_sticky_verdict),c.require_native_decision!==void 0&&(r.require_native_decision=c.require_native_decision),c.min_approvals!==void 0&&(r.min_approvals=c.min_approvals),c.logic!==void 0&&(r.logic=c.logic),c.verdictless_disposition!==void 0&&(r.verdictless_disposition=c.verdictless_disposition),r})});return{enabled:!0,valid:!0,reason:"active",conditions,config_hash:configHash,gate_name:gateName}}function asLowerString(value){return typeof value=="string"&&value.trim().length>0?value.trim().toLowerCase():void 0}function normalizeOneCheck(name,raw){let checkName=normalizeCheckName(name);if(checkName===null)return null;if(!isPlainObject4(raw))return{name:checkName,complete:!1,green:!1};let status=asLowerString(raw.status),conclusion=asLowerString(raw.conclusion),explicitComplete=typeof raw.complete=="boolean"?raw.complete:void 0,explicitPassed=typeof raw.passed=="boolean"?raw.passed:void 0,complete=!1;explicitComplete!==void 0?complete=explicitComplete:(conclusion!==void 0&&COMPLETE_STATES.has(conclusion)||status!==void 0&&COMPLETE_STATES.has(status))&&(complete=!0);let green=!1;complete&&(explicitPassed===!0||conclusion!==void 0&&SUCCESS_STATES.has(conclusion)||conclusion===void 0&&explicitPassed===void 0&&status!==void 0&&SUCCESS_STATES.has(status))&&(green=!0),explicitPassed===!1&&(green=!1);let state=conclusion??status??(explicitPassed===!0?"passed":void 0),check={name:checkName,complete,green};return state!==void 0&&(check.state=state),typeof raw.started_at=="string"&&(check.started_at=raw.started_at),typeof raw.completed_at=="string"&&(check.completed_at=raw.completed_at),typeof raw.recency_id=="number"&&Number.isFinite(raw.recency_id)&&(check.recency_id=raw.recency_id),check}function normalizeCiSnapshot(response){let checks=[],byName=new Map,source=isPlainObject4(response)?response:void 0,detail=source&&isPlainObject4(source.detail)?source.detail:void 0;if(source){let rawChecks=source.checks??detail?.checks;if(Array.isArray(rawChecks)){let{selected,ambiguousNames}=selectLatestChecks(rawChecks);for(let entry of selected){let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(ambiguousNames.has(normalized.name)&&(normalized.complete=!1,normalized.green=!1,normalized.recency_ambiguous=!0),byName.set(normalized.name,normalized),checks.push(normalized))}}else if(isPlainObject4(rawChecks))for(let[name,value]of Object.entries(rawChecks)){let normalized=normalizeOneCheck(name,value);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}}let unknownChecks=[],rawUnknown=source?source.unknown_checks??detail?.unknown_checks:void 0;if(Array.isArray(rawUnknown))for(let raw of rawUnknown){let name=normalizeCheckName(raw);name!==null&&!unknownChecks.includes(name)&&unknownChecks.push(name)}let allComplete=checks.length>0&&checks.every(c=>c.complete),allPassed=checks.length>0&&checks.every(c=>c.green)&&unknownChecks.length===0,hashInput={checks:[...checks].sort((a,b)=>a.name.localeCompare(b.name)).map(c=>({name:c.name,complete:c.complete,green:c.green})),unknown_checks:[...unknownChecks].sort()};return{checks,unknown_checks:unknownChecks,check_state_hash:stableJsonHash(hashInput),all_complete:allComplete,all_passed:allPassed}}function normalizeReviewSnapshot(raw){if(!isPlainObject4(raw)||raw.available===!1)return null;let detail=isPlainObject4(raw.detail)?raw.detail:null;if(detail===null)return null;let reviewDecision=typeof detail.review_decision=="string"&&detail.review_decision.length>0?detail.review_decision:null,approvals=typeof detail.approvals=="number"&&Number.isInteger(detail.approvals)&&detail.approvals>=0?detail.approvals:0,rawVerdict=detail.sticky_verdict,stickyVerdict;rawVerdict===REVIEW_VERDICT_APPROVED?stickyVerdict="approved":rawVerdict===REVIEW_VERDICT_CHANGES_REQUESTED?stickyVerdict="changes_requested":rawVerdict===REVIEW_VERDICT_UNKNOWN?stickyVerdict="unknown":stickyVerdict=null;let headSha=typeof detail.head_sha=="string"&&detail.head_sha.trim().length>0?detail.head_sha.trim():null,reviewStateHash=stableJsonHash({review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha});return{review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha,review_state_hash:reviewStateHash}}function evaluateReviewCondition(condition,snapshot){if(snapshot===null)return{passed:!1,changesRequested:!1,reason:"review snapshot unavailable"};let source=condition.source;if(source==="verdict_protocol")return snapshot.sticky_verdict==="approved"?{passed:!0,changesRequested:!1,reason:"sticky verdict approved"}:snapshot.sticky_verdict==="changes_requested"?{passed:!1,changesRequested:!0,reason:"sticky verdict requests changes"}:{passed:!1,changesRequested:!1,reason:`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`};if(source==="native_review_decision"){let dec=snapshot.review_decision?.toUpperCase();return dec==="APPROVED"?{passed:!0,changesRequested:!1,reason:"native review decision approved"}:dec==="CHANGES_REQUESTED"?{passed:!1,changesRequested:!0,reason:"native review decision requests changes"}:{passed:!1,changesRequested:!1,reason:`native review decision not approved: ${snapshot.review_decision??"null"}`}}if(source==="min_approvals"){let required=typeof condition.min_approvals=="number"?condition.min_approvals:1;return snapshot.approvals>=required?{passed:!0,changesRequested:!1,reason:`approvals ${snapshot.approvals} >= ${required}`}:{passed:!1,changesRequested:!1,reason:`approvals ${snapshot.approvals} < ${required}`}}if(source==="combination"){let requireSticky=condition.require_sticky_verdict===!0,requireNative=condition.require_native_decision===!0,minApprovals=typeof condition.min_approvals=="number"?condition.min_approvals:0,failures=[],changesRequested=!1;if(requireSticky&&(snapshot.sticky_verdict==="changes_requested"&&(changesRequested=!0),snapshot.sticky_verdict!=="approved"&&failures.push(`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`)),requireNative){let dec=snapshot.review_decision?.toUpperCase();dec==="CHANGES_REQUESTED"&&(changesRequested=!0),dec!=="APPROVED"&&failures.push(`native decision not approved: ${snapshot.review_decision??"null"}`)}return minApprovals>0&&snapshot.approvals<minApprovals&&failures.push(`approvals ${snapshot.approvals} < ${minApprovals}`),failures.length>0?{passed:!1,changesRequested,reason:failures.join("; ")}:{passed:!0,changesRequested:!1,reason:"all combination sources satisfied"}}return{passed:!1,changesRequested:!1,reason:`unknown review source: ${source}`}}function failedEvaluation(reason){return{met:!1,reason}}function evaluateDoneGate(config,binding,snapshot,evaluatedAtIso,reviewSnapshot=null){if(!config.enabled||!config.valid||config.conditions.length===0)return failedEvaluation(`gate inactive: ${config.reason}`);let headSha=normalizeSha(binding.head_sha);if(headSha===null)return failedEvaluation("invalid binding: head_sha is not a valid SHA");let allFailureReasons=[],checkResults=[],ciConditionType,requiredChecks,reviewResult,byName=new Map;for(let check of snapshot.checks)byName.set(check.name,check);let unknownSet=new Set(snapshot.unknown_checks);for(let condition of config.conditions)if(condition.type===REQUIRED_CI_CHECKS_GREEN){ciConditionType=condition.type,requiredChecks=[...condition.required_checks],checkResults=[];let unmet=[];for(let name of condition.required_checks){let check=byName.get(name);if(!check){checkResults.push({name,present:!1,complete:!1,green:!1}),unmet.push(unknownSet.has(name)?`${name} (unknown)`:`${name} (missing)`);continue}checkResults.push({name,present:!0,complete:check.complete,green:check.green}),check.green||unmet.push(check.complete?`${name} (not green)`:`${name} (pending)`)}unmet.length>0&&allFailureReasons.push(`required checks not green: ${unmet.join(", ")}`)}else condition.type===REVIEW_STATE&&(reviewResult=evaluateReviewCondition(condition,reviewSnapshot),reviewResult.passed||allFailureReasons.push(`review condition not met: ${reviewResult.reason}`));if(allFailureReasons.length>0)return failedEvaluation(allFailureReasons.join("; "));let ciCheckStatus={};ciConditionType!==void 0&&(ciCheckStatus.condition_type=ciConditionType,ciCheckStatus.required_checks=requiredChecks,ciCheckStatus.check_results=checkResults);let reviewStatus={};reviewResult!==void 0&&(reviewStatus.passed=reviewResult.passed,reviewStatus.reason=reviewResult.reason);let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:headSha,gate_name:config.gate_name,config_hash:config.config_hash,evaluated_at:evaluatedAtIso,ci_check_status:ciCheckStatus};return reviewResult!==void 0&&(details.review_status=reviewStatus),{met:!0,reason:"met",gateEventData:{summary:`Done gate "${config.gate_name}" met for ${binding.subject}`,status:"met",details}}}var VALID_REVIEW_SOURCES,REVIEW_SOURCE_ALIASES,SUCCESS_STATES,COMPLETE_STATES,REVIEW_VERDICT_APPROVED,REVIEW_VERDICT_CHANGES_REQUESTED,REVIEW_VERDICT_UNKNOWN,init_done_gate=__esm({"src/conductor/done-gate.ts"(){"use strict";init_git_ci_types();init_latest_check_selector();VALID_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision","min_approvals","combination"]),REVIEW_SOURCE_ALIASES=Object.freeze({sticky_verdict:"verdict_protocol",claude_review_sticky:"verdict_protocol",github_review_decision:"native_review_decision"});SUCCESS_STATES=new Set(["success","passed","succeeded"]),COMPLETE_STATES=new Set(["completed","complete","success","passed","succeeded","failure","failed","error","cancelled","canceled","timed_out","action_required","neutral","skipped"]);REVIEW_VERDICT_APPROVED="approved",REVIEW_VERDICT_CHANGES_REQUESTED="changes_requested",REVIEW_VERDICT_UNKNOWN="unknown"}});import{execFileSync as execFileSync2}from"node:child_process";import{basename}from"node:path";function runGitCommand(args,options={}){try{let stdout=execFileSync2("git",args,{cwd:options.cwd,timeout:options.timeoutMs??GIT_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:GIT_COMMAND_MAX_BUFFER,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function firstLine(result){if(!result.ok)return null;let trimmed=result.stdout.trim();return trimmed.length>0?trimmed:null}function sanitizeGitRemoteUrl(url){if(typeof url!="string")return null;let trimmed=url.trim();if(trimmed.length===0)return null;if(/^https?:\/\//i.test(trimmed))try{let parsed=new URL(trimmed);return parsed.username="",parsed.password="",parsed.toString()}catch{return trimmed.replace(/^(https?:\/\/)[^/@]*@/i,"$1")}return trimmed}function getGitWorktreeContext(options={}){let cwd=options.cwd??process.cwd(),env=options.env??process.env,topLevel=firstLine(runGitCommand(["rev-parse","--show-toplevel"],{cwd})),isWorktree=topLevel!==null,worktreePath=topLevel??cwd,gitCommonDir=firstLine(runGitCommand(["rev-parse","--git-common-dir"],{cwd})),branchRaw=firstLine(runGitCommand(["rev-parse","--abbrev-ref","HEAD"],{cwd})),branch=branchRaw===null||branchRaw==="HEAD"?null:branchRaw,headSha=normalizeSha(firstLine(runGitCommand(["rev-parse","HEAD"],{cwd}))??""),remoteOrigin=sanitizeGitRemoteUrl(firstLine(runGitCommand(["config","--get","remote.origin.url"],{cwd}))??"");return{repo:normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME)??normalizeRepoName(env.BAPI_REPO_NAME)??normalizeRepoName(basename(worktreePath))??"unknown",worktree_path:worktreePath,git_common_dir:gitCommonDir,branch,head_sha:headSha,remote_origin:remoteOrigin,is_worktree:isWorktree}}function parseCoAuthoredByTrailers(message){if(typeof message!="string"||message.length===0)return[];let out=[];for(let line of message.split(/\r?\n/)){let match=CO_AUTHOR_RE.exec(line.trim());match&&out.push({name:match[1].trim(),email:match[2].trim()})}return out}function readHeadCommitMetadata(options={}){let ref=options.ref??"HEAD",result=runGitCommand(["show","-s",`--format=${COMMIT_FORMAT}`,ref],{cwd:options.cwd});if(!result.ok)return null;let fields=result.stdout.replace(/\n$/,"").split("");if(fields.length<10)return null;let[sha,parentsRaw,authorName,authorEmail,committerName,committerEmail,authoredAt,committedAt,subject,body]=fields,parents=parentsRaw.trim().split(/\s+/).map(p=>normalizeSha(p)).filter(p=>p!==null),coAuthors=parseCoAuthoredByTrailers(body);return{sha:normalizeSha(sha),parents,author_name:authorName,author_email:authorEmail,committer_name:committerName,committer_email:committerEmail,authored_at:authoredAt,committed_at:committedAt,subject,body,co_authors:coAuthors,attribution_source:coAuthors.length>0?"co-authored-by-trailer":"commit-author"}}function parseReferenceTransactionUpdates(stdin){if(typeof stdin!="string"||stdin.length===0)return[];let out=[];for(let line of stdin.split(/\r?\n/)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length!==3)continue;let oldSha=normalizeSha(parts[0]),newSha=normalizeSha(parts[1]),ref=parts[2];oldSha===null||newSha===null||ref.length===0||REF_CONTROL_CHAR_RE.test(ref)||out.push({old_sha:oldSha,new_sha:newSha,ref})}return out}var GIT_COMMAND_TIMEOUT_MS,GIT_COMMAND_MAX_BUFFER,CO_AUTHOR_RE,COMMIT_FORMAT,REF_CONTROL_CHAR_RE,init_git_inspection=__esm({"src/conductor/git-inspection.ts"(){"use strict";init_git_ci_types();GIT_COMMAND_TIMEOUT_MS=5e3,GIT_COMMAND_MAX_BUFFER=10*1024*1024;CO_AUTHOR_RE=/^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;COMMIT_FORMAT="%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";REF_CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/}});import{execFileSync as execFileSync3}from"node:child_process";function runGhCommand(args,options={}){try{let stdout=execFileSync3("gh",args,{cwd:options.cwd,timeout:GH_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:4194304,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function discoverPrWithGhCli(options={},deps={}){let result=(deps.runGh??runGhCommand)(GH_PR_VIEW_ARGS,{cwd:options.cwd});if(!result.ok)return null;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return null}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return null;let record=parsed,number=typeof record.number=="number"?record.number:null,state=typeof record.state=="string"?record.state:"";if(number===null||state.length===0)return null;let mergeability=parseGhPrMergeabilityFields(record),discovered={number,head_sha:normalizeSha(record.headRefOid),state,mergeable:mergeability.mergeable,mergeStateStatus:mergeability.mergeStateStatus};return typeof record.headRefName=="string"&&record.headRefName.trim().length>0&&(discovered.head_ref=record.headRefName.trim()),typeof record.baseRefName=="string"&&record.baseRefName.trim().length>0&&(discovered.base_ref=record.baseRefName.trim()),typeof record.url=="string"&&record.url.trim().length>0&&(discovered.url=record.url.trim()),discovered}function makeBinding(repo,prNumber,headSha,extra={}){let binding={repo,pr_number:prNumber,head_sha:headSha,subject:`${repo}#${prNumber}`};return extra.url!==void 0&&(binding.url=extra.url),extra.head_ref!==void 0&&(binding.head_ref=extra.head_ref),extra.base_ref!==void 0&&(binding.base_ref=extra.base_ref),binding}function resolvePrHeadBinding(input={},deps={}){let explicitRepo=input.repoName!==void 0?normalizeRepoName(input.repoName):null;if(input.prNumber!==void 0||input.headSha!==void 0){let prNumber2=normalizePrNumber(input.prNumber),headSha=normalizeSha(input.headSha);if(prNumber2===null||headSha===null)return{ok:!1,reason:"invalid explicit pr_number or head_sha"};if(input.repoName!==void 0&&explicitRepo===null)return{ok:!1,reason:"invalid explicit repo_name"};let repo2=explicitRepo??normalizeRepoName(deps.getContext?.({cwd:input.cwd,env:input.env})?.repo);return repo2===null?{ok:!1,reason:"could not resolve repo name"}:{ok:!0,binding:makeBinding(repo2,prNumber2,headSha)}}let context=(deps.getContext??getGitWorktreeContext)({cwd:input.cwd,env:input.env}),repo=explicitRepo??normalizeRepoName(context.repo),localSha=normalizeSha(context.head_sha??"");if(repo===null||localSha===null)return{ok:!1,reason:"no local repo/HEAD to bind"};let pr=discoverPrWithGhCli({cwd:input.cwd},deps);if(pr===null)return{ok:!1,reason:"gh unavailable or no PR for current branch"};if(pr.state.toUpperCase()!=="OPEN")return{ok:!1,reason:`PR is not open (state: ${pr.state})`};let prNumber=normalizePrNumber(pr.number);return prNumber===null?{ok:!1,reason:"discovered PR number is invalid"}:pr.head_sha!==null&&pr.head_sha!==localSha?{ok:!1,reason:"PR head SHA does not match local HEAD"}:{ok:!0,binding:makeBinding(repo,prNumber,localSha,{url:pr.url,head_ref:pr.head_ref,base_ref:pr.base_ref})}}var GH_COMMAND_TIMEOUT_MS,GH_PR_VIEW_ARGS,init_pr_discovery=__esm({"src/conductor/pr-discovery.ts"(){"use strict";init_git_ci_types();init_github_mergeability();init_git_inspection();GH_COMMAND_TIMEOUT_MS=5e3;GH_PR_VIEW_ARGS=["pr","view","--json","number,headRefOid,headRefName,baseRefName,url,state,mergeable,mergeStateStatus"]}});import path34 from"path";function isRelevantSourceFile(name){return!(!name.endsWith(".ts")||name.endsWith(".generated.ts"))}async function findNewestSourceMtime(srcDir,deps){let newest=null,walk=async dir=>{let entries;try{entries=await deps.fs.readdir(dir)}catch(err){return sanitize(err)}for(let entry of entries){if(entry.isDirectory()){if(EXCLUDED_DIR_NAMES.has(entry.name))continue;let error2=await walk(path34.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat13=await deps.fs.stat(path34.join(dir,entry.name));(newest===null||stat13.mtimeMs>newest)&&(newest=stat13.mtimeMs)}catch(err){return sanitize(err)}}return null},error=await walk(srcDir);return error?{ok:!1,error}:{ok:!0,mtimeMs:newest}}async function findBuildMtime(executorEntrypoint,deps){try{return{ok:!0,mtimeMs:(await deps.fs.stat(executorEntrypoint)).mtimeMs}}catch(err){return{ok:!1,error:sanitize(err)}}}async function checkPlaneBuildFreshness(repoRoot,deps){let label="Executor build freshness",packageRoot=path34.join(repoRoot,"mcp_server"),buildDir=path34.join(packageRoot,"build"),executorEntrypoint=path34.join(buildDir,"index.js"),srcDir=path34.join(packageRoot,"src"),buildMtime=await findBuildMtime(executorEntrypoint,deps);if(!buildMtime.ok)return planeCheckFinding({check:"executor-build",severity:"blocking",message:`the executor build entrypoint is missing or unreadable at mcp_server/build/index.js \u2014 run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"the executor build entrypoint is missing or unreadable");let sourceMtime=await findNewestSourceMtime(srcDir,deps);return sourceMtime.ok?sourceMtime.mtimeMs!==null&&sourceMtime.mtimeMs>buildMtime.mtimeMs?planeCheckFinding({check:"executor-build",severity:"blocking",message:`mcp_server/build/ is STALE \u2014 a source file under mcp_server/src is newer than mcp_server/build/index.js, so executors would run old code. Run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"mcp_server/build/ is stale \u2014 a source file is newer than the build entrypoint"):planeCheckPassed("executor-build",label,"the executor build is present and current"):planeCheckFinding({check:"executor-build",severity:"blocking",message:`could not read mcp_server/src to compare against the build (${sourceMtime.error}) \u2014 run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,`the executor sources could not be read for comparison (${sourceMtime.error})`)}function checkPlaneRuntimeEntrypoint(resolution){let label="Runtime re-exec entrypoint";return resolution.ok?planeCheckPassed("runtime-entrypoint",label,"the executing build can re-enter itself"):planeCheckFinding({check:"runtime-entrypoint",severity:"blocking",message:PLANE_RUNTIME_ENTRYPOINT_REFUSAL,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"the currently executing MCP build could not be re-entered")}function sanitize(err){let code=err?.code;return typeof code=="string"?code:"unreadable"}var PLANE_BUILD_REMEDIATION,EXCLUDED_DIR_NAMES,PLANE_BUILD_FIX_REMEDIATION,PLANE_RUNTIME_ENTRYPOINT_REFUSAL,init_build_freshness=__esm({"src/plane/build-freshness.ts"(){"use strict";init_types3();PLANE_BUILD_REMEDIATION="cd mcp_server && npm run build",EXCLUDED_DIR_NAMES=new Set(["node_modules","build"]);PLANE_BUILD_FIX_REMEDIATION="run `cd mcp_server && npm run build` to rebuild the executor entrypoint, then retry.";PLANE_RUNTIME_ENTRYPOINT_REFUSAL=`the currently executing MCP build could not be re-entered \u2014 \`plane up\` re-execs this package's own compiled entrypoint to create the detached runtime, and no existing entrypoint belonging to it could be found. Run \`${PLANE_BUILD_REMEDIATION}\` and retry from the rebuilt CLI.`}});import path35 from"path";function resolveRepositoryAlembicCommand(repoRoot,platform){return platform==="win32"?path35.join(repoRoot,".venv","Scripts","alembic.exe"):path35.join(repoRoot,".venv","bin","alembic")}function parseAlembicRevisions(output){let revisions=new Set;for(let rawLine of output.split(/\r?\n/)){let line=rawLine.trim();if(line.length===0||LOG_LINE_PATTERN.test(line))continue;let token=line.split(/\s+/)[0];!token||NON_REVISION_TOKENS.has(token.toLowerCase())||REVISION_PATTERN.test(token)&&revisions.add(token)}return revisions.size===0?null:[...revisions].sort()}async function checkAlembicHead(repoRoot,deps){let alembic=resolveRepositoryAlembicCommand(repoRoot,deps.platform);if(!await deps.fileExists(alembic))return unverified("the repository virtualenv Alembic executable was not found","the repository virtualenv Alembic executable was not found");let heads=await deps.execFile(alembic,["-c","alembic.ini","heads"],{cwd:repoRoot});if(!heads.ok)return unverified(`\`alembic heads\` did not complete (${heads.error})`,"`alembic heads` did not complete");let current=await deps.execFile(alembic,["-c","alembic.ini","current"],{cwd:repoRoot});if(!current.ok)return unverified(`\`alembic current\` did not complete (${current.error})`,"`alembic current` did not complete");let headRevisions=parseAlembicRevisions(heads.stdout),currentRevisions=parseAlembicRevisions(current.stdout);return headRevisions===null||currentRevisions===null?unverified("Alembic output did not contain a readable revision","Alembic output did not contain a readable revision"):headRevisions.length===currentRevisions.length&&headRevisions.every((rev,i)=>rev===currentRevisions[i])?planeCheckPassed("alembic-head",ALEMBIC_CHECK_LABEL,"the database is at the repository migration head"):planeCheckFinding({check:"alembic-head",severity:"blocking",message:`the database is not at the migration head \u2014 heads [${headRevisions.join(", ")}], current [${currentRevisions.join(", ")}]. A database behind the head blocks conductor dispatch. Run \`${PLANE_ALEMBIC_REMEDIATION}\``,remediation:PLANE_ALEMBIC_MISMATCH_REMEDIATION},ALEMBIC_CHECK_LABEL,`database at [${currentRevisions.join(", ")}], repository head [${headRevisions.join(", ")}]`)}function unverified(messageDetail,outcomeDetail){let diagnostic={check:"alembic-head",severity:"warning",message:`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${messageDetail}. Startup continues \u2014 verify with \`alembic -c alembic.ini heads\` and \`alembic -c alembic.ini current\` if a run later reports CONTRACT_MIGRATION_BEHIND.`,remediation:PLANE_ALEMBIC_UNVERIFIED_REMEDIATION};return planeCheckFinding(diagnostic,ALEMBIC_CHECK_LABEL,`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${outcomeDetail}`)}var PLANE_ALEMBIC_REMEDIATION,PLANE_ALEMBIC_UNVERIFIED_PREFIX,PLANE_ALEMBIC_MISMATCH_REMEDIATION,PLANE_ALEMBIC_UNVERIFIED_REMEDIATION,LOG_LINE_PATTERN,REVISION_PATTERN,NON_REVISION_TOKENS,ALEMBIC_CHECK_LABEL,init_alembic_head=__esm({"src/plane/alembic-head.ts"(){"use strict";init_types3();PLANE_ALEMBIC_REMEDIATION="alembic -c alembic.ini upgrade head",PLANE_ALEMBIC_UNVERIFIED_PREFIX="could not verify database migration head",PLANE_ALEMBIC_MISMATCH_REMEDIATION=`run \`${PLANE_ALEMBIC_REMEDIATION}\` to bring the database up to the migration head, then retry.`,PLANE_ALEMBIC_UNVERIFIED_REMEDIATION="verify by hand with `alembic -c alembic.ini heads` and `alembic -c alembic.ini current`; startup continues, and the server reports CONTRACT_MIGRATION_BEHIND at dispatch if the database is behind.";LOG_LINE_PATTERN=/^(INFO|WARNING|ERROR|DEBUG|CRITICAL)\b|\[alembic/i,REVISION_PATTERN=/^[0-9A-Za-z][0-9A-Za-z_.-]*$/,NON_REVISION_TOKENS=new Set(["head","heads","base","none","(head)"]);ALEMBIC_CHECK_LABEL="Database migration head"}});import path36 from"path";function classifyLivePlaneForAttach(repoRoot,manifest,proc){if(manifest.repoRoot!==repoRoot)return{kind:"foreign-root"};let lifecycle=planeManifestLifecycle(manifest),probes=probeManifestMembers(manifest,proc),memberAlive=name=>probes.some(probe=>probe.member.name===name&&probe.liveness==="alive");return!memberAlive("server")||!memberAlive("worker")?{kind:"not-attachable",lifecycle}:lifecycle==="control-plane-ready"?{kind:"attachable",manifest}:BOUND_RUN_ATTACHABLE_PHASES.includes(lifecycle)&&manifest.epicRunId!==void 0&&manifest.laneScope!==void 0?{kind:"attachable",manifest}:{kind:"not-attachable",lifecycle}}function describePlanePortOccupant(observation){switch(observation?.kind){case"unvalidated":return{classification:"an existing plane manifest could not be validated",remediation:PLANE_MANIFEST_UNREADABLE_REMEDIATION};case"foreign-root":return{classification:"a live plane manifest names a different repository root",remediation:PLANE_FOREIGN_ROOT_REMEDIATION};case"not-attachable":return{classification:`this repository's plane is live but not attachable (lifecycle: ${observation.lifecycle})`,remediation:PLANE_NOT_ATTACHABLE_REMEDIATION};case"attachable":return{classification:"this repository's own healthy plane holds the port, but BAPI_BASE_URL does not name it",remediation:PLANE_ATTACHABLE_BASE_URL_REMEDIATION};default:return{classification:"the port is held by a listener that is not a live plane of this repository",remediation:PLANE_PORT_UNRELATED_LISTENER_REMEDIATION}}}async function runPlanePreflight(repoRoot,deps){let diagnostics=[],outcomes=[],add=diagnostic=>{diagnostic&&diagnostics.push(diagnostic)},record=report=>(add(report.diagnostic),outcomes.push(...report.outcomes),report);if(record(await checkRepositoryRoot(repoRoot,deps)).diagnostic){for(let[check,id,label]of ROOT_DEPENDENT_CHECKS)outcomes.push({check,id,label,status:"skip",detail:PLANE_ROOT_DEPENDENT_SKIP_DETAIL});return{ok:!1,diagnostics,outcomes}}record(await checkRepositoryWorktree(repoRoot,deps)),record(await checkClaudeLogin(deps));let credentials=await checkBridgeCredentials(repoRoot,deps);outcomes.push(...credentials.outcomes);for(let advisory of credentials.advisories)add(advisory);credentials.ok||add(credentials.diagnostic),record(await checkPlaneBuildFreshness(repoRoot,{fs:deps.fs}));let runtimeEntrypoint=deps.resolveRuntimeEntrypoint();record(checkPlaneRuntimeEntrypoint(runtimeEntrypoint));let observations={};if(deps.endpoint.ok){let port=await checkServerPort(deps,deps.endpoint.endpoint);record(port),observations.serverPort=port.probe}else record(planeCheckFinding({check:"server-port",severity:"blocking",message:deps.endpoint.message,remediation:PLANE_SERVER_ENDPOINT_REMEDIATION},"Local server port","the configured local server endpoint could not be resolved"));record(await checkAlembicHead(repoRoot,{execFile:deps.execFile,fileExists:filePath=>fileExists(filePath,deps.fs),platform:deps.platform}));let existing=await checkExistingPlane(repoRoot,deps);return record(existing),observations.existingPlane=existing.observation,diagnostics.filter(d=>d.severity==="blocking").length>0||!credentials.ok||!runtimeEntrypoint.ok||!deps.endpoint.ok?{ok:!1,diagnostics,outcomes,observations}:{ok:!0,diagnostics,outcomes,observations,context:{repoRoot,repoName:credentials.repoName,endpoint:deps.endpoint.endpoint,executorEntrypoint:path36.join(repoRoot,"mcp_server","build","index.js"),runtimeEntrypoint:runtimeEntrypoint.entrypoint,bridgeApiKey:credentials.apiKey,bridgeCredentialSource:credentials.source}}}async function checkRepositoryRoot(repoRoot,deps){let label="Repository root";if(!path36.isAbsolute(repoRoot))return planeCheckFinding({check:"repository-root",severity:"blocking",message:"the repository root could not be resolved to an absolute path",remediation:PLANE_REPOSITORY_ROOT_REMEDIATION},label,"the repository root could not be resolved to an absolute path");let missing=[];for(let file of REQUIRED_REPO_FILES)await fileExists(path36.join(repoRoot,file),deps.fs)||missing.push(file);return await fileExists(path36.join(repoRoot,"mcp_server"),deps.fs)||missing.push("mcp_server/"),missing.length===0?planeCheckPassed("repository-root",label,"all Bridge API root marker files are present"):planeCheckFinding({check:"repository-root",severity:"blocking",message:`this does not look like the Bridge API repository root \u2014 missing ${missing.join(", ")}. Run \`plane up\` from the repository root.`,remediation:PLANE_REPOSITORY_ROOT_REMEDIATION},label,`missing root marker(s): ${missing.join(", ")}`)}async function checkRepositoryWorktree(repoRoot,deps){let label="Git work tree",result=await deps.execFile("git",["rev-parse","--is-inside-work-tree"],{cwd:repoRoot});return!result.ok||result.stdout.trim()!=="true"?planeCheckFinding({check:"worktree-presence",severity:"blocking",message:`${repoRoot} is not inside a Git work tree (git rev-parse --is-inside-work-tree did not report 'true'). No worktree was created \u2014 ${PLANE_WORKTREE_REMEDIATION}`,remediation:PLANE_WORKTREE_REMEDIATION},label,"this directory is not inside a Git work tree"):planeCheckPassed("worktree-presence",label,"inside a Git work tree")}async function checkClaudeLogin(deps){let label="Claude login",result=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.fs.readFile});return result.detected?planeCheckPassed("claude-login",label,"a local Claude login marker was detected"):planeCheckFinding({check:"claude-login",severity:"warning",message:formatClaudeLoginAdvisory(result),remediation:PLANE_CLAUDE_LOGIN_REMEDIATION},label,"no local Claude login marker was detected")}function credentialPermissionsOutcome(advisories){return advisories.length===0?{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.permissions,label:"Bridge credential file permissions",status:"pass",detail:"the credential resolver raised no permission advisory"}:{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.permissions,label:"Bridge credential file permissions",status:"warn",detail:`the credential resolver raised ${advisories.length} permission advisory line(s)`,remediation:PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION}}async function checkBridgeCredentials(repoRoot,deps){let repo=await deps.resolveRepoName({env:deps.env,cwd:repoRoot,readFile:deps.fs.readFile});if(!repo.ok)return{ok:!1,advisories:[],diagnostic:{check:"bridge-credentials",severity:"blocking",message:"the Bridge repository identity could not be resolved \u2014 set BAPI_REPO_NAME or add a valid .bridge/config at the repository root.",remediation:PLANE_REPO_IDENTITY_REMEDIATION},outcomes:[{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.identity,label:"Bridge repository identity",status:"fail",detail:"the Bridge repository identity could not be resolved",remediation:PLANE_REPO_IDENTITY_REMEDIATION},{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"skip",detail:"not resolved \u2014 there is no repository identity to resolve a credential for"},credentialPermissionsOutcome([])]};let warnings=createResolverWarningBuffer(),result;try{result=await deps.resolveCredentials(repo.repoName,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.fs.readFile,stat:async filePath=>{let statResult=await deps.fs.stat(filePath);return{mode:statResult.mode!==void 0?statResult.mode:511}},stderr:warnings.write})}catch{let advisories2=warnings.drain();return{ok:!1,advisories:advisories2,diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (resolver unavailable).`,remediation:PLANE_CREDENTIAL_REMEDIATION},outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"fail",detail:"the credential resolver was unavailable",remediation:PLANE_CREDENTIAL_REMEDIATION},credentialPermissionsOutcome(advisories2)]}}if(!result.ok){let advisories2=warnings.drain();return{ok:!1,advisories:advisories2,diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (${result.kind}). Set BAPI_API_KEY in this shell, or store it with \`mcp-server credentials\`. A spawned shell never sees .mcp.json env.`,remediation:PLANE_CREDENTIAL_REMEDIATION},outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"fail",detail:`no Bridge API credential resolved (${result.kind})`,remediation:PLANE_CREDENTIAL_REMEDIATION},credentialPermissionsOutcome(advisories2)]}}let advisories=warnings.drain();return{ok:!0,repoName:repo.repoName,apiKey:result.credentials.apiKey,source:result.credentials.source,advisories,outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"pass",detail:`resolved from ${result.credentials.source} (the value is never read into a report)`},credentialPermissionsOutcome(advisories)]}}function identityResolvedOutcome(){return{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.identity,label:"Bridge repository identity",status:"pass",detail:"the Bridge repository identity resolved"}}function createResolverWarningBuffer(){let lines=[],pending="",push=line=>{let trimmed=line.replace(/\r$/,"");trimmed.length!==0&&lines.push(trimmed)};return{write:message=>{pending+=message;let segments=pending.split(`
3844
+ `)),overrides.installStatus!==!1)try{let injectedFs=deps,installDeps={env:overrides.installStatus?.env??deps.env,cwd:overrides.installStatus?.cwd??deps.cwd,platform:overrides.installStatus?.platform??deps.platform,homedir:overrides.installStatus?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.installStatus?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.installStatus?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.installStatus?.fetch??((...args)=>fetch(...args))},checks=await collectInstallStatusChecks(installDeps);log(formatInstallStatusReport(checks))}catch{log(formatInstallStatusFallbackReport())}if(parsed.options.agentName==="codex"){let readCodexFile=deps.readFile??(p=>readFile9(p,"utf-8")),profile=await inspectBridgeCodexProfile(codexHome(deps.env),readCodexFile);log(profile.status==="active"?"Codex Bridge approval profile installed; spawned workers use --profile bridge.":profile.status==="stale"?"Codex Bridge approval profile is an older unmodified managed version; the next spawned worker refreshes it before launch.":`Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`)}if(log(formatDoctorPrereqSection(deps.platform,collection)),overrides.executorAdapter!==!1)try{let adapterDeps={platform:overrides.executorAdapter?.platform??deps.platform,env:overrides.executorAdapter?.env??deps.env,runCommand:overrides.executorAdapter?.runCommand??(async(command,args)=>{let probe=await deps.runCommand(command,args);return{stdout:probe.stdout??"",stderr:probe.stderr??"",exitCode:probe.exitCode??1}}),...overrides.executorAdapter?.resolveAdapter?{resolveAdapter:overrides.executorAdapter.resolveAdapter}:{}},inspection=await collectExecutorAdapterDiagnostic(agent.name,adapterDeps);log(formatExecutorAdapterDiagnosticReport(inspection))}catch{}let duplicateRegistrationFault=!1;try{let registrationFindings=await inspectRegistrationStatus({cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8"))});duplicateRegistrationFault=hasDuplicateRegistrationFault(registrationFindings),log(formatRegistrationStatusReport(registrationFindings))}catch{log(formatRegistrationStatusReport([]))}try{let launcherDeps={cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8")),probeNpxNoInstall:overrides.launcherProbe?.probeNpxNoInstall??probeNpxNoInstallDefault},launcherInspections=await inspectLauncherCache(launcherDeps);log(formatLauncherCacheReport(launcherInspections))}catch{}if(overrides.localCliOverride!==!1)try{let localCliDeps={env:overrides.localCliOverride?.env??deps.env,runCommand:overrides.localCliOverride?.runCommand??(async(command,args)=>deps.runCommand(command,args))},diagnostic=await collectLocalCliOverrideDiagnostic(localCliDeps);log(formatLocalCliOverrideDiagnosticReport(diagnostic))}catch{}if(overrides.workerContainment!==!1)try{log(formatWorkerContainmentDiagnosticReport(collectWorkerContainmentDiagnostic()))}catch{try{log(formatWorkerContainmentDiagnosticReport(void 0))}catch{}}if(overrides.toolSurface!==!1)try{let injectedFs=deps,toolSurfaceDeps={env:overrides.toolSurface?.env??deps.env,cwd:overrides.toolSurface?.cwd??deps.cwd,platform:overrides.toolSurface?.platform??deps.platform,homedir:overrides.toolSurface?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.toolSurface?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.toolSurface?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.toolSurface?.fetch??((...args)=>fetch(...args))},diagnostic=await collectToolSurfaceDiagnostic(toolSurfaceDeps);log(formatToolSurfaceDiagnosticReport(diagnostic))}catch{log(formatToolSurfaceDiagnosticReport({enabled:parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),reason:"malformed",subtype:"unexpected"}))}if(overrides.executorService!==!1)try{let injectedFs=deps,executorDeps={platform:overrides.executorService?.platform??deps.platform,env:overrides.executorService?.env??deps.env,homedir:overrides.executorService?.homedir??injectedFs.homedir??os12.homedir,readdir:overrides.executorService?.readdir??(p=>readdir5(p)),readFile:overrides.executorService?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.executorService?.stat??injectedFs.stat??(p=>stat5(p)),resolveCredentials:overrides.executorService?.resolveCredentials},executorDiagnostics=await collectExecutorServiceDiagnostics(executorDeps);log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));let loginResult=await detectClaudeLogin({homedir:executorDeps.homedir,readFile:executorDeps.readFile});log(`
3845
+ ${formatClaudeLoginAdvisory(loginResult)}`)}catch{log(formatExecutorServiceDiagnosticsReport({status:"skipped",reason:"executor service units could not be enumerated on this host."}))}if(overrides.planeCredentials!==!1)try{let injectedFs=deps,planeDeps={env:overrides.planeCredentials?.env??deps.env,cwd:overrides.planeCredentials?.cwd??deps.cwd,platform:overrides.planeCredentials?.platform??deps.platform,homedir:overrides.planeCredentials?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.planeCredentials?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.planeCredentials?.stat??injectedFs.stat??(p=>stat5(p)),resolveRepoName:overrides.planeCredentials?.resolveRepoName,resolveCredentials:overrides.planeCredentials?.resolveCredentials,readPlaneManifest:overrides.planeCredentials?.readPlaneManifest};log(formatPlaneCredentialDiagnosticReport(await collectPlaneCredentialDiagnostic(planeDeps)))}catch{log(formatPlaneCredentialDiagnosticReport({repo:null,target:null,bridgeCredentialResolved:!1}))}if(overrides.conductEpic!==!1&&collection.ok)try{let injectedFs=deps,conductDeps={env:overrides.conductEpic?.env??deps.env,cwd:overrides.conductEpic?.cwd??deps.cwd,platform:overrides.conductEpic?.platform??deps.platform,homedir:overrides.conductEpic?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.conductEpic?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.conductEpic?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.conductEpic?.readdir??(p=>readdir5(p)),runCommand:overrides.conductEpic?.runCommand??deps.runCommand,resolveAccess:overrides.conductEpic?.resolveAccess,isProcessAlive:overrides.conductEpic?.isProcessAlive};log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)))}catch{log(formatConductEpicDiagnosticReport({ghPresent:!1,ghAuthenticated:!1,worktrunkBinary:resolveWorktrunkBinary(deps.platform,deps.env),worktrunkAvailable:!1,repo:null,bridgeCredentialResolved:!1,stateDirectory:null,stateDirectoryReadable:!1,checkpoints:[],locks:[]}))}if(overrides.operationalAdvisories!==!1&&collection.ok)try{let injectedFs=deps,advisoryDeps={env:overrides.operationalAdvisories?.env??deps.env,cwd:overrides.operationalAdvisories?.cwd??deps.cwd,platform:overrides.operationalAdvisories?.platform??deps.platform,homedir:overrides.operationalAdvisories?.homedir??injectedFs.homedir??os12.homedir,readFile:overrides.operationalAdvisories?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.operationalAdvisories?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.operationalAdvisories?.readdir??(p=>readdir5(p)),resolveAccess:overrides.operationalAdvisories?.resolveAccess,isProcessAlive:overrides.operationalAdvisories?.isProcessAlive,fetchImpl:overrides.operationalAdvisories?.fetchImpl,now:overrides.operationalAdvisories?.now};log(formatOperationalAdvisoryReport(await collectOperationalAdvisories(advisoryDeps)))}catch{log(formatOperationalAdvisoryReport({dispatcher:null,conductorObservation:"unavailable",conductors:[],indeterminateLocks:0,legacyDriveHelpers:[]}))}if(overrides.staleBranch!==!1&&parsed.options.staleBranchKeys.length>0&&parsed.options.staleBranchBase!==null){let base=parsed.options.staleBranchBase,staleDeps={runCommand:overrides.staleBranch?.runCommand??deps.runCommand,platform:overrides.staleBranch?.platform??deps.platform,env:overrides.staleBranch?.env??deps.env,cwd:overrides.staleBranch?.cwd??deps.cwd},targets=parsed.options.staleBranchKeys.map(key=>({key,branch:resolveBranchForTicket(key,parsed.options.staleBranchOverrides),baseStartPoint:base}));try{log(formatStaleWorktreeDiagnosticReport(await diagnoseStaleWorktreeBranches(staleDeps,targets)))}catch{log(formatStaleWorktreeDiagnosticReport([]))}}return!collection.ok||duplicateRegistrationFault||collection.results.some(r=>!r.found)?1:0}var DOCTOR_REPORT_TITLE,LEGACY_REGISTRATION_INFO_TEXT,LOCAL_CLI_OVERRIDE_ENV,LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT,SHELL_OPERATOR_PATTERN,LEGACY_DRIVE_HELPER_RELATIVE_PATHS,init_doctor=__esm({"src/doctor.ts"(){"use strict";init_start_tickets();init_version_generated();init_install_doctor();init_codex_bridge_profile();init_executor_adapter_inspection();init_executor_adapter_registry();init_env_flags();init_bridge_api_urls();init_tool_surface_gating();init_credential_store();init_bridge_api_client();init_bridge_client();init_checkpoint_store();init_lock();init_start_tickets_repo();init_worker_containment_diagnostic();init_launcher_config_inspection();init_mcp_identity();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();init_executor();init_credentials();init_claude_login();init_base_ref();init_manifest();init_ticket_key_utils();init_worktree_core();init_stale_worktree_doctor();DOCTOR_REPORT_TITLE="bridge doctor \u2014 read-only diagnostics";LEGACY_REGISTRATION_INFO_TEXT=`This registration remains supported. New installs use \`${MCP_SERVER_NAME}\`. No action is required.`;LOCAL_CLI_OVERRIDE_ENV="BAPI_MCP_CLI",LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT=200,SHELL_OPERATOR_PATTERN=/[;&|<>$`\n\r]/;LEGACY_DRIVE_HELPER_RELATIVE_PATHS=[".local/bin/bapi-runtime-up",".local/bin/bapi-drive"]}});function canonicalizePlanDAG(plan){let nodes=plan.nodes.map(node=>({...node,ticket_key:node.ticket_key.trim(),depends_on:[...node.depends_on].map(k=>k.trim()).sort(),...node.touched_files?{touched_files:[...node.touched_files].sort()}:{}})).sort((a,b)=>a.ticket_key.localeCompare(b.ticket_key)),edges=[...plan.edges].map(e=>({from:e.from.trim(),to:e.to.trim(),...e.kind?{kind:e.kind}:{},...e.overlap_files?{overlap_files:[...e.overlap_files].sort()}:{}})).sort((a,b)=>{let cmp=a.from.localeCompare(b.from);return cmp!==0?cmp:a.to.localeCompare(b.to)});return{plan_version:plan.plan_version,nodes,edges}}function hashPlan(plan){return stableJsonHash(canonicalizePlanDAG(plan))}var init_plan=__esm({"src/conductor/plan.ts"(){"use strict";init_git_ci_types()}});function unreported(id){return{id,state:"unreported",detail:UNREPORTED_DETAIL,remediation:BRING_UP_UNREPORTED_REMEDIATION,refusal:"bring-up-facts-unreported"}}function classifyDatabaseMigration(bringUp){let id="database-migration-current";return bringUp===null?unreported(id):bringUp.migration_readable?bringUp.database_migration_current?{id,state:"ok",detail:"the database is at (or ahead of) the code migration head"}:{id,state:"bad",detail:"the database is BEHIND the server's code migration head",remediation:DATABASE_MIGRATION_BEHIND_REMEDIATION,refusal:"database-migration-behind"}:{id,state:"unreadable",detail:"the migration state could not be read (unknown, not confirmed behind)",remediation:DATABASE_MIGRATION_UNREADABLE_REMEDIATION,refusal:"database-migration-unreadable"}}function classifyWorkerPresent(bringUp){let id="worker-present";return bringUp===null?unreported(id):bringUp.heartbeat_readable?bringUp.worker_present?{id,state:"ok",detail:"a reconciler heartbeat is fresh"}:{id,state:"bad",detail:"0 fresh reconcilers \u2014 no worker is running against this deployment",remediation:WORKER_NOT_PRESENT_REMEDIATION,refusal:"worker-not-present"}:{id,state:"unreadable",detail:"the reconciler heartbeat could not be read (unknown, not confirmed absent)",remediation:WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,refusal:"worker-heartbeat-unreadable"}}function classifySingleTransitionAuthority(bringUp){let id="single-transition-authority";return bringUp===null?unreported(id):bringUp.heartbeat_readable?bringUp.fresh_reconciler_count>=2?{id,state:"bad",detail:"2+ fresh reconcilers \u2014 more than one transition authority is ticking",remediation:SECOND_RECONCILER_REMEDIATION,refusal:"second-reconciler-detected"}:bringUp.fresh_reconciler_count===0?{id,state:"bad",detail:"0 fresh reconcilers \u2014 no transition authority is ticking",remediation:WORKER_NOT_PRESENT_REMEDIATION,refusal:"worker-not-present"}:{id,state:"ok",detail:"exactly 1 fresh reconciler"}:{id,state:"unreadable",detail:"the reconciler heartbeat could not be read (unknown, not confirmed)",remediation:WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,refusal:"worker-heartbeat-unreadable"}}function classifyBringUpFacts(bringUp){return[classifyDatabaseMigration(bringUp),classifyWorkerPresent(bringUp),classifySingleTransitionAuthority(bringUp)]}function firstBringUpRefusal(bringUp){let reasons=new Set(classifyBringUpFacts(bringUp).map(verdict=>verdict.refusal).filter(reason=>reason!==void 0));return BRING_UP_REFUSAL_REASONS.find(reason=>reasons.has(reason))??null}function capitalize(text3){return text3.length===0?text3:text3[0].toUpperCase()+text3.slice(1)}var BRING_UP_RUNBOOK,BRING_UP_REFUSAL_REASONS,BRING_UP_UNREPORTED_REMEDIATION,DATABASE_MIGRATION_UNREADABLE_REMEDIATION,DATABASE_MIGRATION_BEHIND_REMEDIATION,WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,WORKER_NOT_PRESENT_REMEDIATION,SECOND_RECONCILER_REMEDIATION,UNREPORTED_DETAIL,BRING_UP_REFUSAL_MESSAGES,init_bring_up_facts=__esm({"src/conductor/bring-up-facts.ts"(){"use strict";BRING_UP_RUNBOOK="docs/claude/epic-conductor-v2-operator-runbook.md",BRING_UP_REFUSAL_REASONS=["bring-up-facts-unreported","database-migration-unreadable","database-migration-behind","worker-heartbeat-unreadable","worker-not-present","second-reconciler-detected"],BRING_UP_UNREPORTED_REMEDIATION="upgrade the Bridge API deploy to a build that reports the bring-up safety facts; their state is unknown here, not healthy.",DATABASE_MIGRATION_UNREADABLE_REMEDIATION=`retry; the migration state could not be read, which is not the same as confirmed-behind. If it persists, check the applied revision with \`alembic current\` (${BRING_UP_RUNBOOK} \xA73d).`,DATABASE_MIGRATION_BEHIND_REMEDIATION=`confirm the applied revision with \`alembic current\` (not \`alembic heads\`, which reads the migration scripts), apply this checkout's migrations with \`alembic -c alembic.ini upgrade head\`, then retry (${BRING_UP_RUNBOOK} \xA73d).`,WORKER_HEARTBEAT_UNREADABLE_REMEDIATION="retry once the Bridge API database is reachable; the reconciler heartbeat could not be read, so worker presence is unknown, not confirmed absent.",WORKER_NOT_PRESENT_REMEDIATION=`start a worker (\`python worker.py\`, or bring up a local plane) \u2014 a server started with DISABLE_SCHEDULER=true runs none. Create and approve only against a server that has a worker (${BRING_UP_RUNBOOK} \xA73d).`,SECOND_RECONCILER_REMEDIATION=`find and stop the extra reconciler: check \`ps aux | grep worker.py\` for a reparented survivor of an earlier plane (${BRING_UP_RUNBOOK} \xA76), and note that a dev server started without DISABLE_SCHEDULER=true runs a reconciler too. A reconciler stopped in the last 120 seconds still counts \u2014 heartbeat rows are not deleted on shutdown \u2014 so wait and retry.`,UNREPORTED_DETAIL="not reported by this Bridge API deploy";BRING_UP_REFUSAL_MESSAGES={"bring-up-facts-unreported":"the Bridge API did not report the bring-up safety facts (database migration, worker presence, single reconciler), so none of them could be confirmed. "+capitalize(BRING_UP_UNREPORTED_REMEDIATION),"database-migration-unreadable":"database migration: the server could not read whether its database is at the migration head, so this is unknown, not confirmed behind. "+capitalize(DATABASE_MIGRATION_UNREADABLE_REMEDIATION),"database-migration-behind":"database migration: the server's database is BEHIND its code's migration head, so the code would run against a schema that was never applied. "+capitalize(DATABASE_MIGRATION_BEHIND_REMEDIATION),"worker-heartbeat-unreadable":"worker presence: the durable reconciler heartbeat could not be read, so whether a worker is running is unknown, not confirmed absent. "+capitalize(WORKER_HEARTBEAT_UNREADABLE_REMEDIATION),"worker-not-present":"worker presence: no reconciler heartbeat is fresh, so nothing would drive this run. "+capitalize(WORKER_NOT_PRESENT_REMEDIATION),"second-reconciler-detected":"single transition authority: two or more reconcilers are ticking the shared database, and two transition authorities wedge an epic. "+capitalize(SECOND_RECONCILER_REMEDIATION)}}});import{createWriteStream,realpathSync,statSync}from"fs";import{mkdir as mkdir8,open as open3,readdir as readdir6,readFile as readFile11,rename as rename3,stat as stat7,unlink as unlink3,writeFile as writeFile7}from"fs/promises";import{execFile as execFileCb,spawn as nodeSpawn}from"child_process";import net from"net";import os14 from"os";import path32 from"path";import{fileURLToPath as fileURLToPath3}from"url";function createPlaneFsDeps(){return{readFile:filePath=>readFile11(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile7(filePath,data,"utf-8"),createExclusive:async(filePath,data)=>{let handle=await open3(filePath,"wx");try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(from,to)=>rename3(from,to),unlink:filePath=>unlink3(filePath),mkdir:async dirPath=>{await mkdir8(dirPath,{recursive:!0})},stat:async filePath=>{let result=await stat7(filePath);return{mtimeMs:result.mtimeMs,mode:result.mode}},readdir:async dirPath=>(await readdir6(dirPath,{withFileTypes:!0})).map(entry=>({name:entry.name,isDirectory:()=>entry.isDirectory(),isFile:()=>entry.isFile()}))}}function createPlaneClock(){return{now:()=>new Date,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms))}}function createPlaneProcessDeps(){return{isAlive:pid=>isProcessAlive(pid,(target,signal)=>process.kill(target,signal)),signal:(pid,signal)=>{try{return process.kill(pid,signal),"sent"}catch(err){let code=err?.code;return code==="ESRCH"?"no-such-process":code==="EPERM"?"permission-denied":"error"}}}}function probeTcpPort(host,port,timeoutMs){return new Promise(resolve2=>{let socket=new net.Socket,settled=!1,finish=result=>{settled||(settled=!0,socket.destroy(),resolve2(result))};socket.setTimeout(timeoutMs),socket.once("connect",()=>finish({kind:"connected"})),socket.once("timeout",()=>finish({kind:"error",error:"probe timed out"})),socket.once("error",err=>{err.code==="ECONNREFUSED"?finish({kind:"refused"}):finish({kind:"error",error:err.code??"probe failed"})}),socket.connect(port,host)})}function createPlaneExecFile(){return(file,args,options)=>new Promise(resolve2=>{execFileCb(file,args,{cwd:options.cwd,shell:!1,windowsHide:!0,timeout:3e4},(error,stdout,stderr)=>{if(error){let code=error.code;resolve2({ok:!1,error:typeof code=="string"?code:"command failed"});return}resolve2({ok:!0,stdout:String(stdout),stderr:String(stderr)})})})}function createPlaneSpawn(){return(command,args,options)=>nodeSpawn(command,args,{cwd:options.cwd,env:options.env,detached:options.detached,shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function createMemberLogDeps(){return{openAppendStream:absolutePath=>{let stream=createWriteStream(absolutePath,{flags:"a"});return stream.on("error",()=>{}),{write:chunk=>{stream.write(chunk)},end:()=>stream.end()}}}}function registerSignalHandler(signal,handler){return process.on(signal,handler),()=>{process.off(signal,handler)}}function resolvePackageEntrypoint(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath,isFile:overrides.isFile??defaultIsFile},moduleFile=moduleFilePath(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=path32.dirname(moduleFile);path32.basename(moduleFile)==="index.js"&&candidates.push(moduleFile),candidates.push(path32.join(dir,"index.js")),candidates.push(path32.join(dir,"..","index.js"))}typeof deps.argv1=="string"&&deps.argv1.length>0&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical=deps.realpath(candidate);if(canonical!==null&&!seen.has(canonical)&&(seen.add(canonical),deps.isFile(canonical)))return{ok:!0,entrypoint:canonical}}return{ok:!1,candidatesChecked:seen.size}}function moduleFilePath(moduleUrl){try{return fileURLToPath3(moduleUrl)}catch{return null}}function defaultRealpath(filePath){try{return realpathSync(filePath)}catch{return null}}function defaultIsFile(filePath){try{return statSync(filePath).isFile()}catch{return!1}}async function probeAutomationHealth(request){let url;try{let target=new URL("/automation/health",request.baseUrl);target.searchParams.set("repo_name",request.repoName),request.instanceId!==void 0&&(target.searchParams.set("component",request.component),target.searchParams.set("instance_id",request.instanceId)),url=target.toString()}catch{return unclassified({kind:"malformed"})}let doFetch=request.fetchImpl??fetch,response;try{response=await doFetch(url,{method:"GET",headers:{"X-API-Key":request.apiKey,Accept:"application/json"},signal:AbortSignal.timeout(request.timeoutMs??PLANE_HEALTH_PROBE_TIMEOUT_MS)})}catch{return unclassified({kind:"unavailable"})}if(response.status===401||response.status===403)return unclassified({kind:"unauthorized"});if(!response.ok)return unclassified({kind:"unavailable"});let body;try{body=await response.json()}catch{return unclassified({kind:"malformed"})}let reading=readComponentObservation(body,request.component);return reading===null?unclassified({kind:"malformed"}):{result:{kind:"state",state:reading.state},observedAtMs:reading.observedAtMs}}function unclassified(result){return{result,observedAtMs:null}}function readComponentObservation(body,component){if(typeof body!="object"||body===null)return null;let block,timestampKey;if(component==="reconciler")block=body.reconciler,timestampKey="heartbeat_last_seen_at";else{let executor=body.executor;if(typeof executor!="object"||executor===null)return null;block=executor.process_heartbeat,timestampKey="last_seen_at"}if(typeof block!="object"||block===null)return null;let state=block.state;if(typeof state!="string"||!PLANE_HEARTBEAT_HEALTH_STATES.includes(state))return null;let known=state,raw=block[timestampKey];if(raw==null)return known==="fresh"||known==="stale"?null:{state:known,observedAtMs:null};if(typeof raw!="string")return null;let observedAtMs=Date.parse(raw);return Number.isNaN(observedAtMs)?null:{state:known,observedAtMs}}function resolveHostname(){return os14.hostname()}function resolveNodeExecutable(){return process.execPath}function resolveHomedir(){return os14.homedir()}var PLANE_HEALTH_PROBE_TIMEOUT_MS,init_defaults=__esm({"src/plane/defaults.ts"(){"use strict";init_types3();init_manifest();PLANE_HEALTH_PROBE_TIMEOUT_MS=5e3}});function isPlainObject4(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function inactiveConfig(reason){return{enabled:!1,valid:!1,reason,conditions:[],config_hash:null,gate_name:DEFAULT_GATE_NAME}}function coerceConfigObject(value){if(value==null)return{kind:"unset"};if(typeof value=="string"){let trimmed=value.trim();if(trimmed.length===0)return{kind:"unset"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"invalid"}}return isPlainObject4(parsed)?Object.keys(parsed).length===0?{kind:"unset"}:{kind:"object",object:parsed}:{kind:"invalid"}}return isPlainObject4(value)?Object.keys(value).length===0?{kind:"unset"}:{kind:"object",object:value}:{kind:"invalid"}}function parseCiChecksCondition(entry){let rawChecks=entry.required_checks;if(!Array.isArray(rawChecks)||rawChecks.length===0)return null;let normalized=[],seen=new Set;for(let raw of rawChecks){let name=normalizeCheckName(raw);if(name===null||seen.has(name))return null;seen.add(name),normalized.push(name)}return{type:REQUIRED_CI_CHECKS_GREEN,required_checks:normalized}}function normalizeReviewSource(source){return REVIEW_SOURCE_ALIASES[source]??source}function parseReviewStateCondition(entry){let rawSource=entry.source;if(typeof rawSource!="string")return null;let source=normalizeReviewSource(rawSource);if(!VALID_REVIEW_SOURCES.has(source))return null;let condition={type:REVIEW_STATE,source};if(entry.require_sticky_verdict!==void 0){if(typeof entry.require_sticky_verdict!="boolean")return null;condition.require_sticky_verdict=entry.require_sticky_verdict}if(entry.require_native_decision!==void 0){if(typeof entry.require_native_decision!="boolean")return null;condition.require_native_decision=entry.require_native_decision}if(entry.min_approvals!==void 0){if(typeof entry.min_approvals!="number"||!Number.isInteger(entry.min_approvals)||entry.min_approvals<0)return null;condition.min_approvals=entry.min_approvals}if(entry.logic!==void 0){if(entry.logic!=="and")return null;condition.logic="and"}if(entry.verdictless_disposition!==void 0){if(typeof entry.verdictless_disposition!="string"||!VERDICTLESS_DISPOSITIONS.includes(entry.verdictless_disposition))return null;condition.verdictless_disposition=entry.verdictless_disposition}if(condition.source==="combination"){let hasSticky=condition.require_sticky_verdict===!0,hasNative=condition.require_native_decision===!0,hasMin=typeof condition.min_approvals=="number"&&condition.min_approvals>0;if(!hasSticky&&!hasNative&&!hasMin)return null}return condition}function parseConditions(object){let raw=object.conditions;if(!Array.isArray(raw)||raw.length===0)return null;let seenTypes=new Set,parsed=[];for(let entry of raw){if(!isPlainObject4(entry))return null;let type=entry.type;if(typeof type!="string"||seenTypes.has(type))return null;if(type===REQUIRED_CI_CHECKS_GREEN){let condition=parseCiChecksCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else if(type===REVIEW_STATE){let condition=parseReviewStateCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else return null}return parsed}function parseDoneGateConfig(value){let coerced=coerceConfigObject(value);if(coerced.kind==="unset")return inactiveConfig("unset");if(coerced.kind==="invalid")return inactiveConfig("malformed");let object=coerced.object;if(object.enabled!==!0)return object.enabled===!1?inactiveConfig("disabled"):inactiveConfig("invalid: 'enabled' must be the boolean true");let conditions=parseConditions(object);if(conditions===null)return inactiveConfig("invalid: conditions must be a non-empty array of valid, non-duplicate condition objects");let gateName=DEFAULT_GATE_NAME,configHash=stableJsonHash({gate_name:gateName,conditions:conditions.map(c=>{if(c.type===REQUIRED_CI_CHECKS_GREEN)return{type:c.type,required_checks:c.required_checks};let r={type:c.type,source:c.source};return c.require_sticky_verdict!==void 0&&(r.require_sticky_verdict=c.require_sticky_verdict),c.require_native_decision!==void 0&&(r.require_native_decision=c.require_native_decision),c.min_approvals!==void 0&&(r.min_approvals=c.min_approvals),c.logic!==void 0&&(r.logic=c.logic),c.verdictless_disposition!==void 0&&(r.verdictless_disposition=c.verdictless_disposition),r})});return{enabled:!0,valid:!0,reason:"active",conditions,config_hash:configHash,gate_name:gateName}}function asLowerString(value){return typeof value=="string"&&value.trim().length>0?value.trim().toLowerCase():void 0}function normalizeOneCheck(name,raw){let checkName=normalizeCheckName(name);if(checkName===null)return null;if(!isPlainObject4(raw))return{name:checkName,complete:!1,green:!1};let status=asLowerString(raw.status),conclusion=asLowerString(raw.conclusion),explicitComplete=typeof raw.complete=="boolean"?raw.complete:void 0,explicitPassed=typeof raw.passed=="boolean"?raw.passed:void 0,complete=!1;explicitComplete!==void 0?complete=explicitComplete:(conclusion!==void 0&&COMPLETE_STATES.has(conclusion)||status!==void 0&&COMPLETE_STATES.has(status))&&(complete=!0);let green=!1;complete&&(explicitPassed===!0||conclusion!==void 0&&SUCCESS_STATES.has(conclusion)||conclusion===void 0&&explicitPassed===void 0&&status!==void 0&&SUCCESS_STATES.has(status))&&(green=!0),explicitPassed===!1&&(green=!1);let state=conclusion??status??(explicitPassed===!0?"passed":void 0),check={name:checkName,complete,green};return state!==void 0&&(check.state=state),typeof raw.started_at=="string"&&(check.started_at=raw.started_at),typeof raw.completed_at=="string"&&(check.completed_at=raw.completed_at),typeof raw.recency_id=="number"&&Number.isFinite(raw.recency_id)&&(check.recency_id=raw.recency_id),check}function normalizeCiSnapshot(response){let checks=[],byName=new Map,source=isPlainObject4(response)?response:void 0,detail=source&&isPlainObject4(source.detail)?source.detail:void 0;if(source){let rawChecks=source.checks??detail?.checks;if(Array.isArray(rawChecks)){let{selected,ambiguousNames}=selectLatestChecks(rawChecks);for(let entry of selected){let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(ambiguousNames.has(normalized.name)&&(normalized.complete=!1,normalized.green=!1,normalized.recency_ambiguous=!0),byName.set(normalized.name,normalized),checks.push(normalized))}}else if(isPlainObject4(rawChecks))for(let[name,value]of Object.entries(rawChecks)){let normalized=normalizeOneCheck(name,value);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}}let unknownChecks=[],rawUnknown=source?source.unknown_checks??detail?.unknown_checks:void 0;if(Array.isArray(rawUnknown))for(let raw of rawUnknown){let name=normalizeCheckName(raw);name!==null&&!unknownChecks.includes(name)&&unknownChecks.push(name)}let allComplete=checks.length>0&&checks.every(c=>c.complete),allPassed=checks.length>0&&checks.every(c=>c.green)&&unknownChecks.length===0,hashInput={checks:[...checks].sort((a,b)=>a.name.localeCompare(b.name)).map(c=>({name:c.name,complete:c.complete,green:c.green})),unknown_checks:[...unknownChecks].sort()};return{checks,unknown_checks:unknownChecks,check_state_hash:stableJsonHash(hashInput),all_complete:allComplete,all_passed:allPassed}}function normalizeReviewSnapshot(raw){if(!isPlainObject4(raw)||raw.available===!1)return null;let detail=isPlainObject4(raw.detail)?raw.detail:null;if(detail===null)return null;let reviewDecision=typeof detail.review_decision=="string"&&detail.review_decision.length>0?detail.review_decision:null,approvals=typeof detail.approvals=="number"&&Number.isInteger(detail.approvals)&&detail.approvals>=0?detail.approvals:0,rawVerdict=detail.sticky_verdict,stickyVerdict;rawVerdict===REVIEW_VERDICT_APPROVED?stickyVerdict="approved":rawVerdict===REVIEW_VERDICT_CHANGES_REQUESTED?stickyVerdict="changes_requested":rawVerdict===REVIEW_VERDICT_UNKNOWN?stickyVerdict="unknown":stickyVerdict=null;let headSha=typeof detail.head_sha=="string"&&detail.head_sha.trim().length>0?detail.head_sha.trim():null,reviewStateHash=stableJsonHash({review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha});return{review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha,review_state_hash:reviewStateHash}}function evaluateReviewCondition(condition,snapshot){if(snapshot===null)return{passed:!1,changesRequested:!1,reason:"review snapshot unavailable"};let source=condition.source;if(source==="verdict_protocol")return snapshot.sticky_verdict==="approved"?{passed:!0,changesRequested:!1,reason:"sticky verdict approved"}:snapshot.sticky_verdict==="changes_requested"?{passed:!1,changesRequested:!0,reason:"sticky verdict requests changes"}:{passed:!1,changesRequested:!1,reason:`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`};if(source==="native_review_decision"){let dec=snapshot.review_decision?.toUpperCase();return dec==="APPROVED"?{passed:!0,changesRequested:!1,reason:"native review decision approved"}:dec==="CHANGES_REQUESTED"?{passed:!1,changesRequested:!0,reason:"native review decision requests changes"}:{passed:!1,changesRequested:!1,reason:`native review decision not approved: ${snapshot.review_decision??"null"}`}}if(source==="min_approvals"){let required=typeof condition.min_approvals=="number"?condition.min_approvals:1;return snapshot.approvals>=required?{passed:!0,changesRequested:!1,reason:`approvals ${snapshot.approvals} >= ${required}`}:{passed:!1,changesRequested:!1,reason:`approvals ${snapshot.approvals} < ${required}`}}if(source==="combination"){let requireSticky=condition.require_sticky_verdict===!0,requireNative=condition.require_native_decision===!0,minApprovals=typeof condition.min_approvals=="number"?condition.min_approvals:0,failures=[],changesRequested=!1;if(requireSticky&&(snapshot.sticky_verdict==="changes_requested"&&(changesRequested=!0),snapshot.sticky_verdict!=="approved"&&failures.push(`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`)),requireNative){let dec=snapshot.review_decision?.toUpperCase();dec==="CHANGES_REQUESTED"&&(changesRequested=!0),dec!=="APPROVED"&&failures.push(`native decision not approved: ${snapshot.review_decision??"null"}`)}return minApprovals>0&&snapshot.approvals<minApprovals&&failures.push(`approvals ${snapshot.approvals} < ${minApprovals}`),failures.length>0?{passed:!1,changesRequested,reason:failures.join("; ")}:{passed:!0,changesRequested:!1,reason:"all combination sources satisfied"}}return{passed:!1,changesRequested:!1,reason:`unknown review source: ${source}`}}function failedEvaluation(reason){return{met:!1,reason}}function evaluateDoneGate(config,binding,snapshot,evaluatedAtIso,reviewSnapshot=null){if(!config.enabled||!config.valid||config.conditions.length===0)return failedEvaluation(`gate inactive: ${config.reason}`);let headSha=normalizeSha(binding.head_sha);if(headSha===null)return failedEvaluation("invalid binding: head_sha is not a valid SHA");let allFailureReasons=[],checkResults=[],ciConditionType,requiredChecks,reviewResult,byName=new Map;for(let check of snapshot.checks)byName.set(check.name,check);let unknownSet=new Set(snapshot.unknown_checks);for(let condition of config.conditions)if(condition.type===REQUIRED_CI_CHECKS_GREEN){ciConditionType=condition.type,requiredChecks=[...condition.required_checks],checkResults=[];let unmet=[];for(let name of condition.required_checks){let check=byName.get(name);if(!check){checkResults.push({name,present:!1,complete:!1,green:!1}),unmet.push(unknownSet.has(name)?`${name} (unknown)`:`${name} (missing)`);continue}checkResults.push({name,present:!0,complete:check.complete,green:check.green}),check.green||unmet.push(check.complete?`${name} (not green)`:`${name} (pending)`)}unmet.length>0&&allFailureReasons.push(`required checks not green: ${unmet.join(", ")}`)}else condition.type===REVIEW_STATE&&(reviewResult=evaluateReviewCondition(condition,reviewSnapshot),reviewResult.passed||allFailureReasons.push(`review condition not met: ${reviewResult.reason}`));if(allFailureReasons.length>0)return failedEvaluation(allFailureReasons.join("; "));let ciCheckStatus={};ciConditionType!==void 0&&(ciCheckStatus.condition_type=ciConditionType,ciCheckStatus.required_checks=requiredChecks,ciCheckStatus.check_results=checkResults);let reviewStatus={};reviewResult!==void 0&&(reviewStatus.passed=reviewResult.passed,reviewStatus.reason=reviewResult.reason);let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:headSha,gate_name:config.gate_name,config_hash:config.config_hash,evaluated_at:evaluatedAtIso,ci_check_status:ciCheckStatus};return reviewResult!==void 0&&(details.review_status=reviewStatus),{met:!0,reason:"met",gateEventData:{summary:`Done gate "${config.gate_name}" met for ${binding.subject}`,status:"met",details}}}var VALID_REVIEW_SOURCES,REVIEW_SOURCE_ALIASES,SUCCESS_STATES,COMPLETE_STATES,REVIEW_VERDICT_APPROVED,REVIEW_VERDICT_CHANGES_REQUESTED,REVIEW_VERDICT_UNKNOWN,init_done_gate=__esm({"src/conductor/done-gate.ts"(){"use strict";init_git_ci_types();init_latest_check_selector();VALID_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision","min_approvals","combination"]),REVIEW_SOURCE_ALIASES=Object.freeze({sticky_verdict:"verdict_protocol",claude_review_sticky:"verdict_protocol",github_review_decision:"native_review_decision"});SUCCESS_STATES=new Set(["success","passed","succeeded"]),COMPLETE_STATES=new Set(["completed","complete","success","passed","succeeded","failure","failed","error","cancelled","canceled","timed_out","action_required","neutral","skipped"]);REVIEW_VERDICT_APPROVED="approved",REVIEW_VERDICT_CHANGES_REQUESTED="changes_requested",REVIEW_VERDICT_UNKNOWN="unknown"}});import{execFileSync as execFileSync2}from"node:child_process";import{basename}from"node:path";function runGitCommand(args,options={}){try{let stdout=execFileSync2("git",args,{cwd:options.cwd,timeout:options.timeoutMs??GIT_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:GIT_COMMAND_MAX_BUFFER,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function firstLine(result){if(!result.ok)return null;let trimmed=result.stdout.trim();return trimmed.length>0?trimmed:null}function sanitizeGitRemoteUrl(url){if(typeof url!="string")return null;let trimmed=url.trim();if(trimmed.length===0)return null;if(/^https?:\/\//i.test(trimmed))try{let parsed=new URL(trimmed);return parsed.username="",parsed.password="",parsed.toString()}catch{return trimmed.replace(/^(https?:\/\/)[^/@]*@/i,"$1")}return trimmed}function getGitWorktreeContext(options={}){let cwd=options.cwd??process.cwd(),env=options.env??process.env,topLevel=firstLine(runGitCommand(["rev-parse","--show-toplevel"],{cwd})),isWorktree=topLevel!==null,worktreePath=topLevel??cwd,gitCommonDir=firstLine(runGitCommand(["rev-parse","--git-common-dir"],{cwd})),branchRaw=firstLine(runGitCommand(["rev-parse","--abbrev-ref","HEAD"],{cwd})),branch=branchRaw===null||branchRaw==="HEAD"?null:branchRaw,headSha=normalizeSha(firstLine(runGitCommand(["rev-parse","HEAD"],{cwd}))??""),remoteOrigin=sanitizeGitRemoteUrl(firstLine(runGitCommand(["config","--get","remote.origin.url"],{cwd}))??"");return{repo:normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME)??normalizeRepoName(env.BAPI_REPO_NAME)??normalizeRepoName(basename(worktreePath))??"unknown",worktree_path:worktreePath,git_common_dir:gitCommonDir,branch,head_sha:headSha,remote_origin:remoteOrigin,is_worktree:isWorktree}}function parseCoAuthoredByTrailers(message){if(typeof message!="string"||message.length===0)return[];let out=[];for(let line of message.split(/\r?\n/)){let match=CO_AUTHOR_RE.exec(line.trim());match&&out.push({name:match[1].trim(),email:match[2].trim()})}return out}function readHeadCommitMetadata(options={}){let ref=options.ref??"HEAD",result=runGitCommand(["show","-s",`--format=${COMMIT_FORMAT}`,ref],{cwd:options.cwd});if(!result.ok)return null;let fields=result.stdout.replace(/\n$/,"").split("");if(fields.length<10)return null;let[sha,parentsRaw,authorName,authorEmail,committerName,committerEmail,authoredAt,committedAt,subject,body]=fields,parents=parentsRaw.trim().split(/\s+/).map(p=>normalizeSha(p)).filter(p=>p!==null),coAuthors=parseCoAuthoredByTrailers(body);return{sha:normalizeSha(sha),parents,author_name:authorName,author_email:authorEmail,committer_name:committerName,committer_email:committerEmail,authored_at:authoredAt,committed_at:committedAt,subject,body,co_authors:coAuthors,attribution_source:coAuthors.length>0?"co-authored-by-trailer":"commit-author"}}function parseReferenceTransactionUpdates(stdin){if(typeof stdin!="string"||stdin.length===0)return[];let out=[];for(let line of stdin.split(/\r?\n/)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length!==3)continue;let oldSha=normalizeSha(parts[0]),newSha=normalizeSha(parts[1]),ref=parts[2];oldSha===null||newSha===null||ref.length===0||REF_CONTROL_CHAR_RE.test(ref)||out.push({old_sha:oldSha,new_sha:newSha,ref})}return out}var GIT_COMMAND_TIMEOUT_MS,GIT_COMMAND_MAX_BUFFER,CO_AUTHOR_RE,COMMIT_FORMAT,REF_CONTROL_CHAR_RE,init_git_inspection=__esm({"src/conductor/git-inspection.ts"(){"use strict";init_git_ci_types();GIT_COMMAND_TIMEOUT_MS=5e3,GIT_COMMAND_MAX_BUFFER=10*1024*1024;CO_AUTHOR_RE=/^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;COMMIT_FORMAT="%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";REF_CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/}});import{execFileSync as execFileSync3}from"node:child_process";function runGhCommand(args,options={}){try{let stdout=execFileSync3("gh",args,{cwd:options.cwd,timeout:GH_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:4194304,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function discoverPrWithGhCli(options={},deps={}){let result=(deps.runGh??runGhCommand)(GH_PR_VIEW_ARGS,{cwd:options.cwd});if(!result.ok)return null;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return null}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return null;let record=parsed,number=typeof record.number=="number"?record.number:null,state=typeof record.state=="string"?record.state:"";if(number===null||state.length===0)return null;let mergeability=parseGhPrMergeabilityFields(record),discovered={number,head_sha:normalizeSha(record.headRefOid),state,mergeable:mergeability.mergeable,mergeStateStatus:mergeability.mergeStateStatus};return typeof record.headRefName=="string"&&record.headRefName.trim().length>0&&(discovered.head_ref=record.headRefName.trim()),typeof record.baseRefName=="string"&&record.baseRefName.trim().length>0&&(discovered.base_ref=record.baseRefName.trim()),typeof record.url=="string"&&record.url.trim().length>0&&(discovered.url=record.url.trim()),discovered}function makeBinding(repo,prNumber,headSha,extra={}){let binding={repo,pr_number:prNumber,head_sha:headSha,subject:`${repo}#${prNumber}`};return extra.url!==void 0&&(binding.url=extra.url),extra.head_ref!==void 0&&(binding.head_ref=extra.head_ref),extra.base_ref!==void 0&&(binding.base_ref=extra.base_ref),binding}function resolvePrHeadBinding(input={},deps={}){let explicitRepo=input.repoName!==void 0?normalizeRepoName(input.repoName):null;if(input.prNumber!==void 0||input.headSha!==void 0){let prNumber2=normalizePrNumber(input.prNumber),headSha=normalizeSha(input.headSha);if(prNumber2===null||headSha===null)return{ok:!1,reason:"invalid explicit pr_number or head_sha"};if(input.repoName!==void 0&&explicitRepo===null)return{ok:!1,reason:"invalid explicit repo_name"};let repo2=explicitRepo??normalizeRepoName(deps.getContext?.({cwd:input.cwd,env:input.env})?.repo);return repo2===null?{ok:!1,reason:"could not resolve repo name"}:{ok:!0,binding:makeBinding(repo2,prNumber2,headSha)}}let context=(deps.getContext??getGitWorktreeContext)({cwd:input.cwd,env:input.env}),repo=explicitRepo??normalizeRepoName(context.repo),localSha=normalizeSha(context.head_sha??"");if(repo===null||localSha===null)return{ok:!1,reason:"no local repo/HEAD to bind"};let pr=discoverPrWithGhCli({cwd:input.cwd},deps);if(pr===null)return{ok:!1,reason:"gh unavailable or no PR for current branch"};if(pr.state.toUpperCase()!=="OPEN")return{ok:!1,reason:`PR is not open (state: ${pr.state})`};let prNumber=normalizePrNumber(pr.number);return prNumber===null?{ok:!1,reason:"discovered PR number is invalid"}:pr.head_sha!==null&&pr.head_sha!==localSha?{ok:!1,reason:"PR head SHA does not match local HEAD"}:{ok:!0,binding:makeBinding(repo,prNumber,localSha,{url:pr.url,head_ref:pr.head_ref,base_ref:pr.base_ref})}}var GH_COMMAND_TIMEOUT_MS,GH_PR_VIEW_ARGS,init_pr_discovery=__esm({"src/conductor/pr-discovery.ts"(){"use strict";init_git_ci_types();init_github_mergeability();init_git_inspection();GH_COMMAND_TIMEOUT_MS=5e3;GH_PR_VIEW_ARGS=["pr","view","--json","number,headRefOid,headRefName,baseRefName,url,state,mergeable,mergeStateStatus"]}});import path34 from"path";function isRelevantSourceFile(name){return!(!name.endsWith(".ts")||name.endsWith(".generated.ts"))}async function findNewestSourceMtime(srcDir,deps){let newest=null,walk=async dir=>{let entries;try{entries=await deps.fs.readdir(dir)}catch(err){return sanitize(err)}for(let entry of entries){if(entry.isDirectory()){if(EXCLUDED_DIR_NAMES.has(entry.name))continue;let error2=await walk(path34.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat13=await deps.fs.stat(path34.join(dir,entry.name));(newest===null||stat13.mtimeMs>newest)&&(newest=stat13.mtimeMs)}catch(err){return sanitize(err)}}return null},error=await walk(srcDir);return error?{ok:!1,error}:{ok:!0,mtimeMs:newest}}async function findBuildMtime(executorEntrypoint,deps){try{return{ok:!0,mtimeMs:(await deps.fs.stat(executorEntrypoint)).mtimeMs}}catch(err){return{ok:!1,error:sanitize(err)}}}async function checkPlaneBuildFreshness(repoRoot,deps){let label="Executor build freshness",packageRoot=path34.join(repoRoot,"mcp_server"),buildDir=path34.join(packageRoot,"build"),executorEntrypoint=path34.join(buildDir,"index.js"),srcDir=path34.join(packageRoot,"src"),buildMtime=await findBuildMtime(executorEntrypoint,deps);if(!buildMtime.ok)return planeCheckFinding({check:"executor-build",severity:"blocking",message:`the executor build entrypoint is missing or unreadable at mcp_server/build/index.js \u2014 run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"the executor build entrypoint is missing or unreadable");let sourceMtime=await findNewestSourceMtime(srcDir,deps);return sourceMtime.ok?sourceMtime.mtimeMs!==null&&sourceMtime.mtimeMs>buildMtime.mtimeMs?planeCheckFinding({check:"executor-build",severity:"blocking",message:`mcp_server/build/ is STALE \u2014 a source file under mcp_server/src is newer than mcp_server/build/index.js, so executors would run old code. Run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"mcp_server/build/ is stale \u2014 a source file is newer than the build entrypoint"):planeCheckPassed("executor-build",label,"the executor build is present and current"):planeCheckFinding({check:"executor-build",severity:"blocking",message:`could not read mcp_server/src to compare against the build (${sourceMtime.error}) \u2014 run \`${PLANE_BUILD_REMEDIATION}\``,remediation:PLANE_BUILD_FIX_REMEDIATION},label,`the executor sources could not be read for comparison (${sourceMtime.error})`)}function checkPlaneRuntimeEntrypoint(resolution){let label="Runtime re-exec entrypoint";return resolution.ok?planeCheckPassed("runtime-entrypoint",label,"the executing build can re-enter itself"):planeCheckFinding({check:"runtime-entrypoint",severity:"blocking",message:PLANE_RUNTIME_ENTRYPOINT_REFUSAL,remediation:PLANE_BUILD_FIX_REMEDIATION},label,"the currently executing MCP build could not be re-entered")}function sanitize(err){let code=err?.code;return typeof code=="string"?code:"unreadable"}var PLANE_BUILD_REMEDIATION,EXCLUDED_DIR_NAMES,PLANE_BUILD_FIX_REMEDIATION,PLANE_RUNTIME_ENTRYPOINT_REFUSAL,init_build_freshness=__esm({"src/plane/build-freshness.ts"(){"use strict";init_types3();PLANE_BUILD_REMEDIATION="cd mcp_server && npm run build",EXCLUDED_DIR_NAMES=new Set(["node_modules","build"]);PLANE_BUILD_FIX_REMEDIATION="run `cd mcp_server && npm run build` to rebuild the executor entrypoint, then retry.";PLANE_RUNTIME_ENTRYPOINT_REFUSAL=`the currently executing MCP build could not be re-entered \u2014 \`plane up\` re-execs this package's own compiled entrypoint to create the detached runtime, and no existing entrypoint belonging to it could be found. Run \`${PLANE_BUILD_REMEDIATION}\` and retry from the rebuilt CLI.`}});import path35 from"path";function resolveRepositoryAlembicCommand(repoRoot,platform){return platform==="win32"?path35.join(repoRoot,".venv","Scripts","alembic.exe"):path35.join(repoRoot,".venv","bin","alembic")}function parseAlembicRevisions(output){let revisions=new Set;for(let rawLine of output.split(/\r?\n/)){let line=rawLine.trim();if(line.length===0||LOG_LINE_PATTERN.test(line))continue;let token=line.split(/\s+/)[0];!token||NON_REVISION_TOKENS.has(token.toLowerCase())||REVISION_PATTERN.test(token)&&revisions.add(token)}return revisions.size===0?null:[...revisions].sort()}async function checkAlembicHead(repoRoot,deps){let alembic=resolveRepositoryAlembicCommand(repoRoot,deps.platform);if(!await deps.fileExists(alembic))return unverified("the repository virtualenv Alembic executable was not found","the repository virtualenv Alembic executable was not found");let heads=await deps.execFile(alembic,["-c","alembic.ini","heads"],{cwd:repoRoot});if(!heads.ok)return unverified(`\`alembic heads\` did not complete (${heads.error})`,"`alembic heads` did not complete");let current=await deps.execFile(alembic,["-c","alembic.ini","current"],{cwd:repoRoot});if(!current.ok)return unverified(`\`alembic current\` did not complete (${current.error})`,"`alembic current` did not complete");let headRevisions=parseAlembicRevisions(heads.stdout),currentRevisions=parseAlembicRevisions(current.stdout);return headRevisions===null||currentRevisions===null?unverified("Alembic output did not contain a readable revision","Alembic output did not contain a readable revision"):headRevisions.length===currentRevisions.length&&headRevisions.every((rev,i)=>rev===currentRevisions[i])?planeCheckPassed("alembic-head",ALEMBIC_CHECK_LABEL,"the database is at the repository migration head"):planeCheckFinding({check:"alembic-head",severity:"blocking",message:`the database is not at the migration head \u2014 heads [${headRevisions.join(", ")}], current [${currentRevisions.join(", ")}]. A database behind the head blocks conductor dispatch. Run \`${PLANE_ALEMBIC_REMEDIATION}\``,remediation:PLANE_ALEMBIC_MISMATCH_REMEDIATION},ALEMBIC_CHECK_LABEL,`database at [${currentRevisions.join(", ")}], repository head [${headRevisions.join(", ")}]`)}function unverified(messageDetail,outcomeDetail){let diagnostic={check:"alembic-head",severity:"warning",message:`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${messageDetail}. Startup continues \u2014 verify with \`alembic -c alembic.ini heads\` and \`alembic -c alembic.ini current\` if a run later reports CONTRACT_MIGRATION_BEHIND.`,remediation:PLANE_ALEMBIC_UNVERIFIED_REMEDIATION};return planeCheckFinding(diagnostic,ALEMBIC_CHECK_LABEL,`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${outcomeDetail}`)}var PLANE_ALEMBIC_REMEDIATION,PLANE_ALEMBIC_UNVERIFIED_PREFIX,PLANE_ALEMBIC_MISMATCH_REMEDIATION,PLANE_ALEMBIC_UNVERIFIED_REMEDIATION,LOG_LINE_PATTERN,REVISION_PATTERN,NON_REVISION_TOKENS,ALEMBIC_CHECK_LABEL,init_alembic_head=__esm({"src/plane/alembic-head.ts"(){"use strict";init_types3();PLANE_ALEMBIC_REMEDIATION="alembic -c alembic.ini upgrade head",PLANE_ALEMBIC_UNVERIFIED_PREFIX="could not verify database migration head",PLANE_ALEMBIC_MISMATCH_REMEDIATION=`run \`${PLANE_ALEMBIC_REMEDIATION}\` to bring the database up to the migration head, then retry.`,PLANE_ALEMBIC_UNVERIFIED_REMEDIATION="verify by hand with `alembic -c alembic.ini heads` and `alembic -c alembic.ini current`; startup continues, and the server reports CONTRACT_MIGRATION_BEHIND at dispatch if the database is behind.";LOG_LINE_PATTERN=/^(INFO|WARNING|ERROR|DEBUG|CRITICAL)\b|\[alembic/i,REVISION_PATTERN=/^[0-9A-Za-z][0-9A-Za-z_.-]*$/,NON_REVISION_TOKENS=new Set(["head","heads","base","none","(head)"]);ALEMBIC_CHECK_LABEL="Database migration head"}});import path36 from"path";function classifyLivePlaneForAttach(repoRoot,manifest,proc){if(manifest.repoRoot!==repoRoot)return{kind:"foreign-root"};let lifecycle=planeManifestLifecycle(manifest),probes=probeManifestMembers(manifest,proc),memberAlive=name=>probes.some(probe=>probe.member.name===name&&probe.liveness==="alive");return!memberAlive("server")||!memberAlive("worker")?{kind:"not-attachable",lifecycle}:lifecycle==="control-plane-ready"?{kind:"attachable",manifest}:BOUND_RUN_ATTACHABLE_PHASES.includes(lifecycle)&&manifest.epicRunId!==void 0&&manifest.laneScope!==void 0?{kind:"attachable",manifest}:{kind:"not-attachable",lifecycle}}function describePlanePortOccupant(observation){switch(observation?.kind){case"unvalidated":return{classification:"an existing plane manifest could not be validated",remediation:PLANE_MANIFEST_UNREADABLE_REMEDIATION};case"foreign-root":return{classification:"a live plane manifest names a different repository root",remediation:PLANE_FOREIGN_ROOT_REMEDIATION};case"not-attachable":return{classification:`this repository's plane is live but not attachable (lifecycle: ${observation.lifecycle})`,remediation:PLANE_NOT_ATTACHABLE_REMEDIATION};case"attachable":return{classification:"this repository's own healthy plane holds the port, but BAPI_BASE_URL does not name it",remediation:PLANE_ATTACHABLE_BASE_URL_REMEDIATION};default:return{classification:"the port is held by a listener that is not a live plane of this repository",remediation:PLANE_PORT_UNRELATED_LISTENER_REMEDIATION}}}async function runPlanePreflight(repoRoot,deps){let diagnostics=[],outcomes=[],add=diagnostic=>{diagnostic&&diagnostics.push(diagnostic)},record=report=>(add(report.diagnostic),outcomes.push(...report.outcomes),report);if(record(await checkRepositoryRoot(repoRoot,deps)).diagnostic){for(let[check,id,label]of ROOT_DEPENDENT_CHECKS)outcomes.push({check,id,label,status:"skip",detail:PLANE_ROOT_DEPENDENT_SKIP_DETAIL});return{ok:!1,diagnostics,outcomes}}record(await checkRepositoryWorktree(repoRoot,deps)),record(await checkClaudeLogin(deps));let credentials=await checkBridgeCredentials(repoRoot,deps);outcomes.push(...credentials.outcomes);for(let advisory of credentials.advisories)add(advisory);credentials.ok||add(credentials.diagnostic),record(await checkPlaneBuildFreshness(repoRoot,{fs:deps.fs}));let runtimeEntrypoint=deps.resolveRuntimeEntrypoint();record(checkPlaneRuntimeEntrypoint(runtimeEntrypoint));let observations={};if(deps.endpoint.ok){let port=await checkServerPort(deps,deps.endpoint.endpoint);record(port),observations.serverPort=port.probe}else record(planeCheckFinding({check:"server-port",severity:"blocking",message:deps.endpoint.message,remediation:PLANE_SERVER_ENDPOINT_REMEDIATION},"Local server port","the configured local server endpoint could not be resolved"));record(await checkAlembicHead(repoRoot,{execFile:deps.execFile,fileExists:filePath=>fileExists(filePath,deps.fs),platform:deps.platform}));let existing=await checkExistingPlane(repoRoot,deps);return record(existing),observations.existingPlane=existing.observation,diagnostics.filter(d=>d.severity==="blocking").length>0||!credentials.ok||!runtimeEntrypoint.ok||!deps.endpoint.ok?{ok:!1,diagnostics,outcomes,observations}:{ok:!0,diagnostics,outcomes,observations,context:{repoRoot,repoName:credentials.repoName,endpoint:deps.endpoint.endpoint,executorEntrypoint:path36.join(repoRoot,"mcp_server","build","index.js"),runtimeEntrypoint:runtimeEntrypoint.entrypoint,bridgeApiKey:credentials.apiKey,bridgeCredentialSource:credentials.source}}}async function checkRepositoryRoot(repoRoot,deps){let label="Repository root";if(!path36.isAbsolute(repoRoot))return planeCheckFinding({check:"repository-root",severity:"blocking",message:"the repository root could not be resolved to an absolute path",remediation:PLANE_REPOSITORY_ROOT_REMEDIATION},label,"the repository root could not be resolved to an absolute path");let missing=[];for(let file of REQUIRED_REPO_FILES)await fileExists(path36.join(repoRoot,file),deps.fs)||missing.push(file);return await fileExists(path36.join(repoRoot,"mcp_server"),deps.fs)||missing.push("mcp_server/"),missing.length===0?planeCheckPassed("repository-root",label,"all Bridge API root marker files are present"):planeCheckFinding({check:"repository-root",severity:"blocking",message:`this does not look like the Bridge API repository root \u2014 missing ${missing.join(", ")}. Run \`plane up\` from the repository root.`,remediation:PLANE_REPOSITORY_ROOT_REMEDIATION},label,`missing root marker(s): ${missing.join(", ")}`)}async function checkRepositoryWorktree(repoRoot,deps){let label="Git work tree",result=await deps.execFile("git",["rev-parse","--is-inside-work-tree"],{cwd:repoRoot});return!result.ok||result.stdout.trim()!=="true"?planeCheckFinding({check:"worktree-presence",severity:"blocking",message:`${repoRoot} is not inside a Git work tree (git rev-parse --is-inside-work-tree did not report 'true'). No worktree was created \u2014 ${PLANE_WORKTREE_REMEDIATION}`,remediation:PLANE_WORKTREE_REMEDIATION},label,"this directory is not inside a Git work tree"):planeCheckPassed("worktree-presence",label,"inside a Git work tree")}async function checkClaudeLogin(deps){let label="Claude login",result=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.fs.readFile});return result.detected?planeCheckPassed("claude-login",label,"a local Claude login marker was detected"):planeCheckFinding({check:"claude-login",severity:"warning",message:formatClaudeLoginAdvisory(result),remediation:PLANE_CLAUDE_LOGIN_REMEDIATION},label,"no local Claude login marker was detected")}function credentialPermissionsOutcome(advisories){return advisories.length===0?{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.permissions,label:"Bridge credential file permissions",status:"pass",detail:"the credential resolver raised no permission advisory"}:{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.permissions,label:"Bridge credential file permissions",status:"warn",detail:`the credential resolver raised ${advisories.length} permission advisory line(s)`,remediation:PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION}}async function checkBridgeCredentials(repoRoot,deps){let repo=await deps.resolveRepoName({env:deps.env,cwd:repoRoot,readFile:deps.fs.readFile});if(!repo.ok)return{ok:!1,advisories:[],diagnostic:{check:"bridge-credentials",severity:"blocking",message:"the Bridge repository identity could not be resolved \u2014 set BAPI_REPO_NAME or add a valid .bridge/config at the repository root.",remediation:PLANE_REPO_IDENTITY_REMEDIATION},outcomes:[{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.identity,label:"Bridge repository identity",status:"fail",detail:"the Bridge repository identity could not be resolved",remediation:PLANE_REPO_IDENTITY_REMEDIATION},{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"skip",detail:"not resolved \u2014 there is no repository identity to resolve a credential for"},credentialPermissionsOutcome([])]};let warnings=createResolverWarningBuffer(),result;try{result=await deps.resolveCredentials(repo.repoName,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.fs.readFile,stat:async filePath=>{let statResult=await deps.fs.stat(filePath);return{mode:statResult.mode!==void 0?statResult.mode:511}},stderr:warnings.write})}catch{let advisories2=warnings.drain();return{ok:!1,advisories:advisories2,diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (resolver unavailable).`,remediation:PLANE_CREDENTIAL_REMEDIATION},outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"fail",detail:"the credential resolver was unavailable",remediation:PLANE_CREDENTIAL_REMEDIATION},credentialPermissionsOutcome(advisories2)]}}if(!result.ok){let advisories2=warnings.drain();return{ok:!1,advisories:advisories2,diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (${result.kind}). Set BAPI_API_KEY in this shell, or store it with \`mcp-server credentials\`. A spawned shell never sees .mcp.json env.`,remediation:PLANE_CREDENTIAL_REMEDIATION},outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"fail",detail:`no Bridge API credential resolved (${result.kind})`,remediation:PLANE_CREDENTIAL_REMEDIATION},credentialPermissionsOutcome(advisories2)]}}let advisories=warnings.drain();return{ok:!0,repoName:repo.repoName,apiKey:result.credentials.apiKey,source:result.credentials.source,advisories,outcomes:[identityResolvedOutcome(),{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.credential,label:"Bridge API credential",status:"pass",detail:`resolved from ${result.credentials.source} (the value is never read into a report)`},credentialPermissionsOutcome(advisories)]}}function identityResolvedOutcome(){return{check:"bridge-credentials",id:PLANE_CREDENTIAL_OUTCOME_IDS.identity,label:"Bridge repository identity",status:"pass",detail:"the Bridge repository identity resolved"}}function createResolverWarningBuffer(){let lines=[],pending="",push=line=>{let trimmed=line.replace(/\r$/,"");trimmed.length!==0&&lines.push(trimmed)};return{write:message=>{pending+=message;let segments=pending.split(`
3846
3846
  `);pending=segments.pop()??"";for(let segment of segments)push(segment)},drain:()=>(pending.length>0&&(push(pending),pending=""),lines.map(message=>({check:"bridge-credentials",severity:"warning",message})))}}async function checkServerPort(deps,endpoint){let report=await checkServerPortReport(deps,endpoint);return{...report.report,probe:report.probe}}async function checkServerPortReport(deps,endpoint){let label="Local server port",target=`${endpoint.host}:${endpoint.port}`,result=await deps.probePort(endpoint.host,endpoint.port,PLANE_PORT_PROBE_TIMEOUT_MS);return result.kind==="refused"?{report:planeCheckPassed("server-port",label,`${target} is free`),probe:"refused"}:result.kind==="connected"?{report:planeCheckFinding({check:"server-port",severity:"blocking",message:`${target} is already accepting connections. That port may belong to a SIBLING WORKTREE's server \u2014 check before you kill it. Stop the existing server (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} to a free port.`,remediation:PLANE_SERVER_PORT_OCCUPIED_REMEDIATION},label,`${target} is already accepting connections`),probe:"connected"}:{report:planeCheckFinding({check:"server-port",severity:"warning",message:`could not determine whether ${target} is free (${result.error}); startup continues and uvicorn will fail loudly if the port is taken.`,remediation:PLANE_SERVER_PORT_UNKNOWN_REMEDIATION},label,`could not determine whether ${target} is free`),probe:"error"}}async function checkExistingPlane(repoRoot,deps){let label="Existing plane",read=await readPlaneManifest(repoRoot,deps.fs);return read.kind==="missing"?{...planeCheckPassed("existing-plane",label,"no plane manifest is present"),observation:{kind:"none"}}:read.kind!=="valid"?{...planeCheckFinding({check:"existing-plane",severity:"blocking",message:`an existing .bridge/plane/plane.json could not be validated (${read.error}). No process was signalled and the file was left untouched \u2014 inspect it, then remove it by hand if no plane is running.`,remediation:PLANE_MANIFEST_UNREADABLE_REMEDIATION},label,"an existing plane manifest could not be validated"),observation:{kind:"unvalidated"}}:deps.ownPlaneId!==void 0&&read.manifest.planeId===deps.ownPlaneId?{...planeCheckPassed("existing-plane",label,"the existing manifest carries this launch's own plane id"),observation:{kind:"own-plane"}}:manifestHasLiveProcess(read.manifest,deps.proc)?{...planeCheckFinding({check:"existing-plane",severity:"blocking",message:`a plane is already running (supervisor pid ${read.manifest.supervisorPid}). Run \`plane status\` to inspect it, or \`plane down\` to wind it down first.`,remediation:PLANE_ALREADY_RUNNING_REMEDIATION},label,"a plane is already running for this repository"),observation:classifyLivePlaneForAttach(repoRoot,read.manifest,deps.proc)}:{observation:{kind:"stale"},diagnostic:{check:"existing-plane",severity:"warning",message:"a previous plane manifest is present but every recorded process is gone; it will be replaced after a final liveness re-check."},outcomes:[{check:"existing-plane",id:"existing-plane",label,status:"warn",detail:"a stale plane manifest is present and will be replaced after a liveness re-check"}]}}async function fileExists(filePath,fs2){try{return await fs2.stat(filePath),!0}catch{return!1}}function mapPlanePreflightToReadinessChecks(result){let byId=new Map;for(let outcome2 of result?.outcomes??[])byId.has(outcome2.id)||byId.set(outcome2.id,outcome2);return PLANE_READINESS_DESCRIPTORS.map(({id,label})=>{let outcome2=byId.get(id);return outcome2?createReadinessCheckSafely({id:`plane.${id}`,source:"plane",label:outcome2.label||label,status:outcome2.status,...outcome2.detail?{detail:outcome2.detail}:{},...outcome2.status==="fail"?{remediation:outcome2.remediation??PLANE_UNREPORTED_REMEDIATION}:outcome2.status!=="pass"&&outcome2.remediation?{remediation:outcome2.remediation}:{}}):createReadinessCheck({id:`plane.${id}`,source:"plane",label,status:"fail",detail:PLANE_UNREPORTED_DETAIL,remediation:PLANE_UNREPORTED_REMEDIATION})})}var REQUIRED_REPO_FILES,BOUND_RUN_ATTACHABLE_PHASES,PLANE_PORT_UNRELATED_LISTENER_REMEDIATION,PLANE_FOREIGN_ROOT_REMEDIATION,PLANE_NOT_ATTACHABLE_REMEDIATION,PLANE_ATTACHABLE_BASE_URL_REMEDIATION,PLANE_REPOSITORY_ROOT_REMEDIATION,PLANE_REPO_IDENTITY_REMEDIATION,PLANE_CREDENTIAL_REMEDIATION,PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION,PLANE_CLAUDE_LOGIN_REMEDIATION,PLANE_SERVER_PORT_OCCUPIED_REMEDIATION,PLANE_SERVER_PORT_UNKNOWN_REMEDIATION,PLANE_SERVER_ENDPOINT_REMEDIATION,PLANE_MANIFEST_UNREADABLE_REMEDIATION,PLANE_ALREADY_RUNNING_REMEDIATION,PLANE_CREDENTIAL_OUTCOME_IDS,PLANE_ROOT_DEPENDENT_SKIP_DETAIL,ROOT_DEPENDENT_CHECKS,PLANE_PORT_PROBE_TIMEOUT_MS,PLANE_WORKTREE_REMEDIATION,PLANE_READINESS_DESCRIPTORS,PLANE_UNREPORTED_DETAIL,PLANE_UNREPORTED_REMEDIATION,init_preflight2=__esm({"src/plane/preflight.ts"(){"use strict";init_types3();init_build_freshness();init_alembic_head();init_manifest();init_claude_login();init_readiness_check();REQUIRED_REPO_FILES=["main.py","worker.py","alembic.ini"],BOUND_RUN_ATTACHABLE_PHASES=["lanes-starting","lanes-ready","ready"];PLANE_PORT_UNRELATED_LISTENER_REMEDIATION="the listener is not a live plane of this repository \u2014 it is often a SIBLING WORKTREE's server. Stop it (or wind down its plane from its own worktree) and run drive-epic again.",PLANE_FOREIGN_ROOT_REMEDIATION="the plane manifest here names a different repository root (copied or symlinked). Inspect .bridge/plane/plane.json, wind that plane down from its own repository, and run drive-epic again.",PLANE_NOT_ATTACHABLE_REMEDIATION="this repository's plane is running but cannot take a new run \u2014 it is still starting, its server or reconciler is gone, its lanes failed, or its lanes were started by hand with their own claim scope. Inspect it with `plane status`; wait for it to finish starting, or wind it down with `plane down` and run drive-epic again to cold-start.",PLANE_ATTACHABLE_BASE_URL_REMEDIATION=`do not stop it \u2014 point BAPI_BASE_URL at the plane endpoint (http://127.0.0.1:<port>, the port ${PLANE_SERVER_PORT_ENV_VAR} or the default, with no trailing slash) and run drive-epic again to attach.`;PLANE_REPOSITORY_ROOT_REMEDIATION="run `plane up` from the Bridge API repository root (the directory holding main.py, worker.py, and alembic.ini).",PLANE_REPO_IDENTITY_REMEDIATION="set BAPI_REPO_NAME in this shell, or add a valid .bridge/config at the repository root.",PLANE_CREDENTIAL_REMEDIATION="set BAPI_API_KEY in this shell, or store it with `mcp-server credentials`; a spawned shell never sees .mcp.json env.",PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION="restrict the Bridge credentials file to owner-only permissions (chmod 600), then rerun.",PLANE_CLAUDE_LOGIN_REMEDIATION="run `claude login` on this host so plane members inherit an authenticated session; startup continues either way.",PLANE_SERVER_PORT_OCCUPIED_REMEDIATION=`stop the server that owns the port (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} to a free port \u2014 the port may belong to a SIBLING WORKTREE.`,PLANE_SERVER_PORT_UNKNOWN_REMEDIATION=`retry, or set ${PLANE_SERVER_PORT_ENV_VAR} to a port you know is free; startup continues and uvicorn fails loudly if the port is taken.`,PLANE_SERVER_ENDPOINT_REMEDIATION=`set ${PLANE_SERVER_PORT_ENV_VAR} to a valid port number (1-65535), or unset it to use the default.`,PLANE_MANIFEST_UNREADABLE_REMEDIATION="inspect .bridge/plane/plane.json and remove it by hand once you have confirmed no plane is running; preflight signalled no process and changed no file.",PLANE_ALREADY_RUNNING_REMEDIATION="run `plane status` to inspect the running plane, or `plane down` to wind it down first.",PLANE_CREDENTIAL_OUTCOME_IDS={identity:"repo-identity",credential:"bridge-credentials",permissions:"credential-permissions"},PLANE_ROOT_DEPENDENT_SKIP_DETAIL="not run \u2014 every remaining check is relative to a valid repository root",ROOT_DEPENDENT_CHECKS=[["worktree-presence","worktree-presence","Git work tree"],["claude-login","claude-login","Claude login"],["bridge-credentials",PLANE_CREDENTIAL_OUTCOME_IDS.identity,"Bridge repository identity"],["bridge-credentials",PLANE_CREDENTIAL_OUTCOME_IDS.credential,"Bridge API credential"],["bridge-credentials",PLANE_CREDENTIAL_OUTCOME_IDS.permissions,"Bridge credential file permissions"],["executor-build","executor-build","Executor build freshness"],["runtime-entrypoint","runtime-entrypoint","Runtime re-exec entrypoint"],["server-port","server-port","Local server port"],["alembic-head","alembic-head","Database migration head"],["existing-plane","existing-plane","Existing plane"]],PLANE_PORT_PROBE_TIMEOUT_MS=1500;PLANE_WORKTREE_REMEDIATION="cd into the intended worktree (or create it with `git worktree add`) and rerun.";PLANE_READINESS_DESCRIPTORS=[{id:"repository-root",label:"Repository root"},{id:"worktree-presence",label:"Git work tree"},{id:"claude-login",label:"Claude login"},{id:"repo-identity",label:"Bridge repository identity"},{id:"bridge-credentials",label:"Bridge API credential"},{id:"credential-permissions",label:"Bridge credential file permissions"},{id:"executor-build",label:"Executor build freshness"},{id:"runtime-entrypoint",label:"Runtime re-exec entrypoint"},{id:"server-port",label:"Local server port"},{id:"alembic-head",label:"Database migration head"},{id:"existing-plane",label:"Existing plane"}],PLANE_UNREPORTED_DETAIL="not reported \u2014 the plane preflight produced no outcome for this prerequisite",PLANE_UNREPORTED_REMEDIATION="run `plane up` (or `epic-implementer readiness` again) to re-collect the plane preflight; this prerequisite's state is unknown, not healthy."}});import path37 from"path";function resolvePythonExecutable(env){let configured=env.BAPI_PLANE_PYTHON;return typeof configured=="string"&&configured.trim().length>0?configured.trim():"python"}function resolveUvicornExecutable(env){let configured=env.BAPI_PLANE_UVICORN;return typeof configured=="string"&&configured.trim().length>0?configured.trim():"uvicorn"}function endpointForPort(port){return{host:PLANE_SERVER_HOST,port,baseUrl:`http://${PLANE_SERVER_HOST}:${port}`}}function resolvePlaneServerEndpoint(env){let configured=env[PLANE_SERVER_PORT_ENV_VAR];if(typeof configured!="string"||configured.trim().length===0)return{ok:!0,endpoint:endpointForPort(PLANE_SERVER_PORT)};let raw=configured.trim();if(!/^[0-9]+$/.test(raw))return{ok:!1,message:PLANE_SERVER_PORT_INVALID_MESSAGE};let port=Number(raw);return!Number.isInteger(port)||port<1||port>MAX_TCP_PORT?{ok:!1,message:PLANE_SERVER_PORT_INVALID_MESSAGE}:{ok:!0,endpoint:endpointForPort(port)}}function buildPlaneChildEnv(parentEnv,context){let env={};for(let[key,value]of Object.entries(parentEnv))value!==void 0&&(env[key]=value);return env.BAPI_REPO_NAME=context.repoName,env.BAPI_BASE_URL=context.endpoint.baseUrl,env.BAPI_API_KEY=context.bridgeApiKey,delete env.CONDUCTOR_DEAD_MAN_ONLY,env}function buildPlaneObserverEnv(childEnv){return{...childEnv,[PLANE_OBSERVER_MODE_ENV]:PLANE_OBSERVER_MODE_VALUE}}function buildPlaneMemberRoster(params){let{context,executors,parentEnv,nodeExecutable}=params,env=buildPlaneChildEnv(parentEnv,context),cwd=context.repoRoot,endpoint=context.endpoint,server2={name:"server",command:resolveUvicornExecutable(parentEnv),args:["main:app","--host",endpoint.host,"--port",String(endpoint.port)],cwd,env,logPath:relativeLogPathFor("server"),readiness:{kind:"tcp",host:endpoint.host,port:endpoint.port,timeoutMs:PLANE_READINESS_TIMEOUT_MS}},observer={name:"observer",command:resolvePythonExecutable(parentEnv),args:["worker.py"],cwd,env:buildPlaneObserverEnv(env),logPath:relativeLogPathFor("observer"),readiness:null},worker={name:"worker",command:resolvePythonExecutable(parentEnv),args:["worker.py"],cwd,env,logPath:relativeLogPathFor("worker"),readiness:{kind:"process-heartbeat",baseUrl:endpoint.baseUrl,repoName:context.repoName,component:"reconciler",timeoutMs:PLANE_RECONCILER_READINESS_TIMEOUT_MS}};if(executors>0&&params.claimScope===void 0)throw new Error(EXECUTOR_CLAIM_SCOPE_REQUIRED_MESSAGE);let scopeArgs=params.claimScope===void 0?[]:executorClaimScopeArgs(params.claimScope),executorMembers=[];for(let lane=1;lane<=executors;lane+=1){let name=`executor-${lane}`,executorId=buildExecutorId(context.repoName,lane);executorMembers.push({name,command:nodeExecutable,args:[context.executorEntrypoint,"executor","--repo",context.repoName,"--base-url",endpoint.baseUrl,"--executor-id",executorId,...scopeArgs],cwd,env,logPath:relativeLogPathFor(name),readiness:{kind:"process-heartbeat",baseUrl:endpoint.baseUrl,repoName:context.repoName,component:"executor",instanceId:executorId,timeoutMs:PLANE_EXECUTOR_READINESS_TIMEOUT_MS}})}return[server2,worker,...executorMembers,observer]}function isExecutorLaneName(name){return name.startsWith("executor-")}function buildPlaneControlPlaneRoster(params){return buildPlaneMemberRoster({...params,executors:0}).filter(member=>member.name==="server"||member.name==="worker")}function buildPlaneExecutorLaneRoster(params){return buildPlaneMemberRoster(params).filter(member=>isExecutorLaneName(member.name))}function buildPlaneObserverRoster(params){return buildPlaneMemberRoster({...params,executors:0}).filter(member=>member.name==="observer")}function buildExecutorId(repoName,lane){return`plane-${repoName.replace(/[^A-Za-z0-9_-]/g,"-").slice(0,40)||"repo"}-${lane}`}function describePlaneMember(spec){return`${spec.name}: ${[spec.command,...spec.args].join(" ")}`}function absoluteLogPath(repoRoot,spec){return path37.join(repoRoot,spec.logPath)}var PLANE_READINESS_TIMEOUT_MS,PLANE_RECONCILER_READINESS_TIMEOUT_MS,PLANE_EXECUTOR_READINESS_TIMEOUT_MS,MAX_TCP_PORT,PLANE_SERVER_PORT_INVALID_MESSAGE,init_member_roster=__esm({"src/plane/member-roster.ts"(){"use strict";init_types3();init_manifest();init_claim_scope();PLANE_READINESS_TIMEOUT_MS=6e4,PLANE_RECONCILER_READINESS_TIMEOUT_MS=12e4,PLANE_EXECUTOR_READINESS_TIMEOUT_MS=9e4;MAX_TCP_PORT=65535,PLANE_SERVER_PORT_INVALID_MESSAGE=`${PLANE_SERVER_PORT_ENV_VAR} must be a whole TCP port number in the range 1..${MAX_TCP_PORT}. Unset it to use the default port ${PLANE_SERVER_PORT}, or export a valid port and retry.`}});async function getPlaneStatus(repoRoot,deps){let read=await readPlaneManifest(repoRoot,deps.fs);if(read.kind==="missing")return{ok:!0,kind:"no-plane"};if(read.kind!=="valid")return{ok:!1,error:`.bridge/plane/plane.json could not be validated (${read.error}). No process was probed. Inspect the file and remove it by hand once you have confirmed no plane is running.`};let manifest=read.manifest;return{ok:!0,kind:"plane",planeId:manifest.planeId,supervisorPid:manifest.supervisorPid,supervisorPgid:manifest.supervisorPgid,supervisorLiveness:deps.proc.isAlive(manifest.supervisorPid),unrecognizedMemberNames:read.unrecognizedMemberNames,lifecycle:planeManifestLifecycle(manifest),epicRunId:manifest.epicRunId??null,createdAt:manifest.createdAt,updatedAt:manifest.updatedAt,members:probeManifestMembers(manifest,deps.proc).map(probe=>({name:probe.member.name,pid:probe.member.pid,state:probe.member.state,liveness:probe.liveness,exitCode:probe.member.exitCode,exitSignal:probe.member.exitSignal,logPath:probe.member.logPath}))}}function describeLifecycle(phase){switch(phase){case"control-plane-starting":return"control plane starting (server, reconciler)";case"control-plane-ready":return"control plane ready \u2014 waiting for a scoped executor-lane request";case"lanes-starting":return"executor lanes starting \u2014 each waits for its own heartbeat gate";case"lanes-ready":return"executor lanes ready \u2014 starting the dead-man observer";case"ready":return"plane ready (this reports PROCESSES, not whether the run is dispatchable)";case"lanes-failed":return"executor lanes FAILED \u2014 the lane cohort was stopped and the control plane was left running; nothing is claiming jobs. Wind it down with `plane down`."}}function formatPlaneStatus(result){if(!result.ok)return`plane status FAILED: ${result.error}`;if(result.kind==="no-plane")return`No plane is running (no ${PLANE_RUNTIME_DIR}/plane.json).`;let lines=[`Plane ${result.planeId}`,` supervisor pid ${result.supervisorPid} (group ${result.supervisorPgid}) \u2014 ${result.supervisorLiveness}`,` lifecycle ${describeLifecycle(result.lifecycle)}`,...result.epicRunId!==null?[` epic run ${result.epicRunId}`]:[],` started ${result.createdAt}`,` updated ${result.updatedAt}`,""];for(let member of result.members)lines.push(` ${member.name.padEnd(12)} ${describeMemberState(member,result.supervisorLiveness)} ${member.logPath}`);result.unrecognizedMemberNames.length>0&&(lines.push(""),lines.push(`${result.unrecognizedMemberNames.length} member(s) are NOT SHOWN above \u2014 this build does not recognize the name(s): ${result.unrecognizedMemberNames.join(", ")}. The manifest was written by a NEWER build; use that build's \`plane status\` to see them, and its \`plane down\` to stop the plane.`));let neverStarted=result.members.filter(m=>isPreStart(m)&&result.supervisorLiveness==="dead");neverStarted.length>0&&(lines.push(""),lines.push(`${neverStarted.length} member(s) were NEVER STARTED: the supervisor died before it reached them, so their log files above may be empty or missing entirely.`),lines.push(`Read the runtime trace instead: ${PLANE_RUNTIME_LOG_PATH}`));let holes=result.members.filter(m=>m.liveness!=="alive"&&!neverStarted.includes(m));return holes.length>0&&(lines.push(""),lines.push(`${holes.length} member(s) are not running. They are NOT restarted automatically \u2014 read the log(s) above, then \`plane down\` and \`plane up\` when you have a fix.`)),lines.join(`
3847
3847
  `)}function isPreStart(member){return member.state==="spawning"&&member.pid===null}function describeMemberState(member,supervisorLiveness){return member.liveness==="alive"?`running (pid ${member.pid??"?"})`.padEnd(34):member.exitSignal!==null?`exited (signal ${member.exitSignal})`.padEnd(34):member.exitCode!==null?`exited (code ${member.exitCode})`.padEnd(34):member.liveness==="unknown"?`unknown (pid ${member.pid??"?"}, liveness unverifiable)`.padEnd(34):isPreStart(member)?supervisorLiveness==="alive"?"starting (not yet spawned)".padEnd(34):supervisorLiveness==="dead"?"never started".padEnd(34):"unknown (supervisor liveness unverifiable)".padEnd(34):"exited (no recorded exit status)".padEnd(34)}var init_status=__esm({"src/plane/status.ts"(){"use strict";init_manifest();init_types3()}});async function shutdownPlane(repoRoot,deps){let graceMs=deps.graceMs??PLANE_SHUTDOWN_GRACE_MS,killWaitMs=deps.killWaitMs??PLANE_SHUTDOWN_KILL_WAIT_MS,pollMs=deps.pollIntervalMs??PLANE_SHUTDOWN_POLL_MS,read=await readPlaneManifest(repoRoot,deps.fs);if(read.kind==="missing")return{ok:!0,outcome:"no-plane",escalated:!1,manifestCleared:!1,members:[],messages:["No plane manifest found \u2014 nothing to wind down."],runShutdown:{kind:"unbound"}};if(read.kind!=="valid")return{ok:!1,reason:"unvalidated-manifest",message:`.bridge/plane/plane.json could not be validated (${read.error}). No process was signalled, because an unvalidated record must never become a signal target.
3848
3848
  If this manifest was written by a NEWER build, wind the plane down with THAT build's \`plane down\` \u2014 do not delete the file, which would orphan every member with no record of what to stop.
@@ -3858,7 +3858,7 @@ ${formatClaudeLoginAdvisory(loginResult)}`)}catch{log(formatExecutorServiceDiagn
3858
3858
  `),stderr:line=>process.stderr.write(`${line}
3859
3859
  `)};try{let parsed=parsePlaneArgs(argv);switch(parsed.kind){case"help":return sinks.stdout(getPlaneUsage()),0;case"error":return sinks.stderr(`Error: ${parsed.message}`),sinks.stderr(""),sinks.stderr(getPlaneUsage()),1;case"status":return await runStatusAction(sinks,overrides);case"down":return await runDownAction(sinks,overrides);case"up":return await runUpAction(parsed.options,sinks,overrides);case"runtime":return await runRuntimeAction(parsed.options,overrides);case"entrypoint":return runEntrypointDiagnosticAction(sinks,overrides)}}catch{return sinks.stderr("plane failed: an unexpected internal error occurred."),1}}function runEntrypointDiagnosticAction(sinks,overrides={}){let resolution=(overrides.resolveRuntimeEntrypoint??(()=>resolvePackageEntrypoint()))();return resolution.ok?(sinks.stdout(resolution.entrypoint),0):(sinks.stderr(`plane entrypoint UNRESOLVED: ${PLANE_RUNTIME_ENTRYPOINT_REFUSAL}`),1)}async function runStatusAction(sinks,overrides){let repoRoot=overrides.cwd??process.cwd();if(overrides.status)return sinks.stdout(await overrides.status(repoRoot)),0;let result=await getPlaneStatus(repoRoot,{fs:createPlaneFsDeps(),proc:createPlaneProcessDeps()}),text3=formatPlaneStatus(result);return result.ok?(sinks.stdout(text3),0):(sinks.stderr(text3),1)}async function runDownAction(sinks,overrides){let repoRoot=overrides.cwd??process.cwd();if(overrides.down){let injected=await overrides.down(repoRoot);return(injected.ok?sinks.stdout:sinks.stderr)(injected.text),injected.ok?0:1}let result=await shutdownPlane(repoRoot,{fs:createPlaneFsDeps(),proc:createPlaneProcessDeps(),clock:createPlaneClock(),stopBoundRun:stopBoundEpicRun}),text3=formatPlaneShutdown(result);return(result.ok?sinks.stdout:sinks.stderr)(text3),result.ok?0:1}async function stopBoundEpicRun(epicRunId,manifest){try{let{resolveConductorBridgeApiAccess:resolveConductorBridgeApiAccess2}=await Promise.resolve().then(()=>(init_bridge_api_client(),bridge_api_client_exports)),{stopEpicRunRecovery:stopEpicRunRecovery2}=await Promise.resolve().then(()=>(init_recovery_operations(),recovery_operations_exports)),accessResult=await resolveConductorBridgeApiAccess2({cwd:manifest.repoRoot});if(!accessResult.ok)return{kind:"unavailable",epicRunId,message:accessResult.error};let result=await stopEpicRunRecovery2(accessResult.access,{epicRunId});return result.ok?result.kind==="committed"?{kind:"committed",epicRunId}:{kind:"already-stopped",epicRunId}:result.kind==="terminal"?{kind:"terminal",epicRunId,status:result.status}:{kind:"unavailable",epicRunId,message:result.message}}catch(err){return{kind:"unavailable",epicRunId,message:err instanceof Error?err.constructor.name:"run-stop failed unexpectedly"}}}async function runUpAction(options,sinks,overrides){let executors=options.executors,repoRoot=overrides.cwd??process.cwd(),env=overrides.env??process.env,preflight=overrides.preflight?await overrides.preflight(repoRoot):await runDefaultPlanePreflight(repoRoot,env);for(let diagnostic of preflight.diagnostics.filter(d=>d.severity==="warning"))sinks.stdout(formatDiagnostic(diagnostic));if(!preflight.ok){for(let line of formatPlanePreflightRefusal(preflight))sinks.stderr(line);return 1}let planeId=(overrides.newPlaneId??randomUUID3)(),launchOptions={...options.claimScope?{claimScope:options.claimScope}:{}},launch=overrides.launch?await overrides.launch(preflight,executors,launchOptions):await launchPlane(preflight,executors,planeId,sinks,env,launchOptions);if(!launch.ok){for(let line of formatPlaneLaunchFailure(launch))sinks.stderr(line);return 1}return launch.exitCode}function formatPlanePreflightRefusal(preflight){return["","plane up REFUSED \u2014 preflight found blocking problems:",...preflight.diagnostics.filter(d=>d.severity==="blocking").map(d=>formatDiagnostic(d)),"",`Fix every item above and retry. ${PLANE_NOTHING_STARTED}.`]}function formatPlaneLaunchFailure(launch){if(!launch.startup)return[`plane up FAILED: ${launch.error}`,` trace ${PLANE_RUNTIME_LOG_PATH} (may be empty \u2014 the runtime never started)`,"The plane is not running. `no processes were started`."];let{exitCode,signal,stderrExcerpt,tracePath}=launch.startup,status=signal!==null?`signal ${signal}`:`exit code ${exitCode??"unknown"}`,lines=[`${PLANE_STARTUP_FAILED}: ${launch.error} (${status}).`,` trace ${tracePath}`];if(stderrExcerpt.trim().length>0){lines.push(" runtime stderr:");for(let line of stderrExcerpt.split(`
3860
3860
  `))line.length>0&&lines.push(` ${line}`)}return lines.push("The plane is not running. Read the trace above, then retry `plane up`."),lines}async function launchPlane(preflight,executors,planeId,sinks,env,launchOptions={}){let fs2=createPlaneFsDeps(),clock=createPlaneClock(),context=preflight.context,nodeExecutable=resolveNodeExecutable(),roster=launchOptions.twoPhase?buildPlaneControlPlaneRoster({context,parentEnv:env,nodeExecutable}):buildPlaneMemberRoster({context,executors,parentEnv:env,nodeExecutable,...launchOptions.claimScope?{claimScope:launchOptions.claimScope}:{}}),timestamp=clock.now().toISOString(),claim=await claimPlaneManifest({manifest:{schemaVersion:3,planeId,repoRoot:context.repoRoot,supervisorPid:process.pid,supervisorPgid:process.pid,createdAt:timestamp,updatedAt:timestamp,members:roster.map(spec=>({name:spec.name,pid:null,state:"spawning",exitCode:null,exitSignal:null,logPath:relativeLogPathFor(spec.name)}))},fs:fs2,proc:createPlaneProcessDeps()});return claim.ok?launchPlaneSupervisor({context,manifest:claim.manifest,executors,...launchOptions.claimScope?{claimScope:launchOptions.claimScope}:{},...launchOptions.twoPhase?{twoPhase:!0}:{}},{fs:fs2,clock,spawn:createPlaneSpawn(),sinks,log:createMemberLogDeps(),onSignal:registerSignalHandler,nodeExecutable,entrypoint:context.runtimeEntrypoint,parentEnv:env,onReady:({supervisorPid,supervisorPgid})=>{printStartupBanner(sinks,context,launchOptions.twoPhase?roster.length+executors+1:roster.length,executors,{supervisorPid,supervisorPgid}),launchOptions.onPlaneReady?.({planeId})},...launchOptions.onControlPlaneReady?{onControlPlaneReady:()=>launchOptions.onControlPlaneReady?.({planeId})}:{},...launchOptions.onLanesFailed?{onLanesFailed:()=>launchOptions.onLanesFailed?.({planeId})}:{}}):{ok:!1,error:claim.message,startup:null}}function printStartupBanner(sinks,context,memberCount,executors,launch){sinks.stdout(""),sinks.stdout(`Plane up \u2014 ${memberCount} members (${executors} executor lane(s))`),sinks.stdout(` repository ${context.repoName} (${context.repoRoot})`),sinks.stdout(` server ${context.endpoint.baseUrl} (no --reload)`),sinks.stdout(` supervisor pid ${launch.supervisorPid}, process group ${launch.supervisorPgid}`),sinks.stdout(` logs ${PLANE_RUNTIME_DIR}/`),sinks.stdout(" observer dead-man-only worker.py, its own OS process (same host)"),sinks.stdout(" crash policy members are NOT restarted; a member exit is reported loudly."),sinks.stdout("")}async function launchPlaneControlPlane(args){if(!args.preflight.ok)return{ok:!1,lines:formatPlanePreflightRefusal(args.preflight)};let preflight=args.preflight,planeId=(args.newPlaneId??randomUUID3)(),signalControlReady,controlReady=new Promise(resolve2=>{signalControlReady=resolve2}),signalPlaneReady,planeReady=new Promise(resolve2=>{signalPlaneReady=resolve2}),signalLanesFailed,lanesFailed=new Promise(resolve2=>{signalLanesFailed=resolve2}),lifetime=launchPlane(preflight,args.executors,planeId,args.sinks,args.env,{twoPhase:!0,onControlPlaneReady:()=>signalControlReady?.(),onPlaneReady:()=>signalPlaneReady?.(),onLanesFailed:()=>signalLanesFailed?.()}),outcome2=await Promise.race([controlReady.then(()=>({kind:"ready"})),lifetime.then(result=>({kind:"settled",result}))]);return outcome2.kind==="settled"?{ok:!1,lines:outcome2.result.ok?["plane up FAILED: the runtime exited before the control plane was ready"]:formatPlaneLaunchFailure(outcome2.result)}:{ok:!0,planeId,lifetime,awaitPlaneReady:async()=>{let settled=await Promise.race([planeReady.then(()=>({kind:"ready"})),lanesFailed.then(()=>({kind:"held"})),lifetime.then(result=>({kind:"settled",result}))]);return settled.kind==="ready"?{ok:!0}:settled.kind==="held"?{ok:!1,lines:["the plane's executor lanes did not start; its control plane is still running"]}:{ok:!1,lines:settled.result.ok?["the plane runtime exited before its executor lanes became ready"]:formatPlaneLaunchFailure(settled.result)}}}}async function requestPlaneScopedLanes(args){let result=await requestPlaneManifestLanes(args.repoRoot,args.planeId,args.epicRunId,args.claimScope,createPlaneFsDeps());return result.ok?{ok:!0}:{ok:!1,reason:result.reason,message:result.message}}function createDetachedRuntimeSinks(){for(let stream of[process.stdout,process.stderr])stream.on("error",()=>{});let write=(stream,line)=>{try{stream.write(`${line}
3861
- `)}catch{}};return{stdout:line=>write(process.stdout,line),stderr:line=>write(process.stderr,line)}}async function runRuntimeAction(options,overrides){let executors=options.executors,repoRoot=overrides.cwd??process.cwd();if(overrides.runtime)return overrides.runtime(repoRoot,executors);let env=overrides.env??process.env,planeId=env[PLANE_ID_ENV_VAR],sinks=overrides.sinks??createDetachedRuntimeSinks();if(typeof planeId!="string"||planeId.length===0)return sinks.stderr(`plane runtime refused: ${PLANE_ID_ENV_VAR} was not provided by the launcher.`),1;let preflight=overrides.preflight?await overrides.preflight(repoRoot):await runPlanePreflight(repoRoot,{...buildPreflightDeps(env),ownPlaneId:planeId});if(!preflight.ok){sinks.stderr(""),sinks.stderr("plane runtime REFUSED \u2014 preflight found blocking problems:");for(let diagnostic of preflight.diagnostics.filter(d=>d.severity==="blocking"))sinks.stderr(formatDiagnostic(diagnostic));return sinks.stderr(""),sinks.stderr(`${PLANE_NOTHING_STARTED}.`),1}let nodeExecutable=resolveNodeExecutable(),rosterBase={context:preflight.context,parentEnv:env,nodeExecutable},roster=options.twoPhase?buildPlaneControlPlaneRoster(rosterBase):buildPlaneMemberRoster({...rosterBase,executors,...options.claimScope?{claimScope:options.claimScope}:{}});return(await runPlaneRuntime(repoRoot,roster,{fs:createPlaneFsDeps(),clock:createPlaneClock(),proc:createPlaneProcessDeps(),spawn:createPlaneSpawn(),sinks,log:createMemberLogDeps(),onSignal:registerSignalHandler,probePort:probeTcpPort,probeHealth:probeAutomationHealth,hostname:resolveHostname,selfPid:process.pid,planeId,...options.twoPhase?{twoPhase:{buildLaneRoster:scope=>buildPlaneExecutorLaneRoster({...rosterBase,executors,claimScope:scope}),observerRoster:buildPlaneObserverRoster(rosterBase)}}:{}})).exitCode}function buildPreflightDeps(env){return{env,endpoint:resolvePlaneServerEndpoint(env),platform:process.platform,homedir:resolveHomedir,fs:createPlaneFsDeps(),clock:createPlaneClock(),proc:createPlaneProcessDeps(),execFile:createPlaneExecFile(),probePort:probeTcpPort,resolveRuntimeEntrypoint:()=>resolvePackageEntrypoint(),resolveRepoName:resolveRequiredStartTicketsRepoName,resolveCredentials:resolveBapiCredentials}}function runDefaultPlanePreflight(repoRoot,env=process.env){return runPlanePreflight(repoRoot,buildPreflightDeps(env))}function formatPlaneDiagnostic(diagnostic){return formatDiagnostic(diagnostic)}function formatDiagnostic(diagnostic){return` ${diagnostic.severity==="blocking"?"BLOCKED":"WARN "} [${diagnostic.check}] ${diagnostic.message}`}var PLANE_MAX_EXECUTORS,PLANE_DEFAULT_EXECUTORS,PLANE_NOTHING_STARTED,PLANE_STARTUP_FAILED,init_cli2=__esm({"src/plane/cli.ts"(){"use strict";init_types3();init_claim_scope();init_build_freshness();init_manifest();init_manifest();init_preflight2();init_member_roster();init_status();init_shutdown();init_supervisor();init_defaults();init_credential_store();init_start_tickets_repo();PLANE_MAX_EXECUTORS=32,PLANE_DEFAULT_EXECUTORS=1;PLANE_NOTHING_STARTED="no processes were started";PLANE_STARTUP_FAILED="plane up FAILED \u2014 the runtime never came up"}});import{chmodSync,existsSync as existsSync5,mkdirSync,readFileSync,realpathSync as realpathSync2,statSync as statSync2,writeFileSync}from"node:fs";import{basename as basename2,dirname as dirname2,isAbsolute,join as join4,resolve}from"node:path";import{fileURLToPath as fileURLToPath3}from"node:url";function resolveGitHooksDirectory(deps={}){let runGit2=deps.runGit??runGitCommand,cwd=deps.cwd??process.cwd(),result=runGit2(["rev-parse","--git-common-dir"],{cwd});if(!result.ok)return{is_worktree:!1,hooks_dir:null};let commonDir=result.stdout.trim();if(commonDir.length===0)return{is_worktree:!1,hooks_dir:null};let absoluteCommonDir=isAbsolute(commonDir)?commonDir:resolve(cwd,commonDir);return{is_worktree:!0,hooks_dir:resolve(absoluteCommonDir,"hooks")}}function resolveConductorHookBin(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath2,isFile:overrides.isFile??defaultIsFile2},moduleFile=moduleFilePath2(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=dirname2(moduleFile);candidates.push(join4(dir,CONDUCTOR_BIN_FILENAME)),candidates.push(join4(dir,"..",CONDUCTOR_BIN_FILENAME))}typeof deps.argv1=="string"&&deps.argv1.length>0&&basename2(deps.argv1)===CONDUCTOR_BIN_FILENAME&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical;try{canonical=deps.realpath(candidate)}catch{continue}if(canonical!==null&&!seen.has(canonical)){seen.add(canonical);try{if(deps.isFile(canonical))return{ok:!0,path:canonical}}catch{continue}}}return{ok:!1,reason:CONDUCTOR_HOOK_BIN_UNRESOLVED_REASON}}function moduleFilePath2(moduleUrl){try{return fileURLToPath3(moduleUrl)}catch{return null}}function defaultRealpath2(filePath){try{return realpathSync2(filePath)}catch{return null}}function defaultIsFile2(filePath){try{return statSync2(filePath).isFile()}catch{return!1}}function wrapManagedBlock(body){return[BRIDGE_CONDUCTOR_HOOK_START,body,BRIDGE_CONDUCTOR_HOOK_END].join(`
3861
+ `)}catch{}};return{stdout:line=>write(process.stdout,line),stderr:line=>write(process.stderr,line)}}async function runRuntimeAction(options,overrides){let executors=options.executors,repoRoot=overrides.cwd??process.cwd();if(overrides.runtime)return overrides.runtime(repoRoot,executors);let env=overrides.env??process.env,planeId=env[PLANE_ID_ENV_VAR],sinks=overrides.sinks??createDetachedRuntimeSinks();if(typeof planeId!="string"||planeId.length===0)return sinks.stderr(`plane runtime refused: ${PLANE_ID_ENV_VAR} was not provided by the launcher.`),1;let preflight=overrides.preflight?await overrides.preflight(repoRoot):await runPlanePreflight(repoRoot,{...buildPreflightDeps(env),ownPlaneId:planeId});if(!preflight.ok){sinks.stderr(""),sinks.stderr("plane runtime REFUSED \u2014 preflight found blocking problems:");for(let diagnostic of preflight.diagnostics.filter(d=>d.severity==="blocking"))sinks.stderr(formatDiagnostic(diagnostic));return sinks.stderr(""),sinks.stderr(`${PLANE_NOTHING_STARTED}.`),1}let nodeExecutable=resolveNodeExecutable(),rosterBase={context:preflight.context,parentEnv:env,nodeExecutable},roster=options.twoPhase?buildPlaneControlPlaneRoster(rosterBase):buildPlaneMemberRoster({...rosterBase,executors,...options.claimScope?{claimScope:options.claimScope}:{}});return(await runPlaneRuntime(repoRoot,roster,{fs:createPlaneFsDeps(),clock:createPlaneClock(),proc:createPlaneProcessDeps(),spawn:createPlaneSpawn(),sinks,log:createMemberLogDeps(),onSignal:registerSignalHandler,probePort:probeTcpPort,probeHealth:probeAutomationHealth,hostname:resolveHostname,selfPid:process.pid,planeId,...options.twoPhase?{twoPhase:{buildLaneRoster:scope=>buildPlaneExecutorLaneRoster({...rosterBase,executors,claimScope:scope}),observerRoster:buildPlaneObserverRoster(rosterBase)}}:{}})).exitCode}function buildPreflightDeps(env){return{env,endpoint:resolvePlaneServerEndpoint(env),platform:process.platform,homedir:resolveHomedir,fs:createPlaneFsDeps(),clock:createPlaneClock(),proc:createPlaneProcessDeps(),execFile:createPlaneExecFile(),probePort:probeTcpPort,resolveRuntimeEntrypoint:()=>resolvePackageEntrypoint(),resolveRepoName:resolveRequiredStartTicketsRepoName,resolveCredentials:resolveBapiCredentials}}function runDefaultPlanePreflight(repoRoot,env=process.env){return runPlanePreflight(repoRoot,buildPreflightDeps(env))}function formatPlaneDiagnostic(diagnostic){return formatDiagnostic(diagnostic)}function formatDiagnostic(diagnostic){return` ${diagnostic.severity==="blocking"?"BLOCKED":"WARN "} [${diagnostic.check}] ${diagnostic.message}`}var PLANE_MAX_EXECUTORS,PLANE_DEFAULT_EXECUTORS,PLANE_NOTHING_STARTED,PLANE_STARTUP_FAILED,init_cli2=__esm({"src/plane/cli.ts"(){"use strict";init_types3();init_claim_scope();init_build_freshness();init_manifest();init_manifest();init_preflight2();init_member_roster();init_status();init_shutdown();init_supervisor();init_defaults();init_credential_store();init_start_tickets_repo();PLANE_MAX_EXECUTORS=32,PLANE_DEFAULT_EXECUTORS=1;PLANE_NOTHING_STARTED="no processes were started";PLANE_STARTUP_FAILED="plane up FAILED \u2014 the runtime never came up"}});import{chmodSync,existsSync as existsSync5,mkdirSync,readFileSync,realpathSync as realpathSync2,statSync as statSync2,writeFileSync}from"node:fs";import{basename as basename2,dirname as dirname2,isAbsolute,join as join4,resolve}from"node:path";import{fileURLToPath as fileURLToPath4}from"node:url";function resolveGitHooksDirectory(deps={}){let runGit2=deps.runGit??runGitCommand,cwd=deps.cwd??process.cwd(),result=runGit2(["rev-parse","--git-common-dir"],{cwd});if(!result.ok)return{is_worktree:!1,hooks_dir:null};let commonDir=result.stdout.trim();if(commonDir.length===0)return{is_worktree:!1,hooks_dir:null};let absoluteCommonDir=isAbsolute(commonDir)?commonDir:resolve(cwd,commonDir);return{is_worktree:!0,hooks_dir:resolve(absoluteCommonDir,"hooks")}}function resolveConductorHookBin(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath2,isFile:overrides.isFile??defaultIsFile2},moduleFile=moduleFilePath2(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=dirname2(moduleFile);candidates.push(join4(dir,CONDUCTOR_BIN_FILENAME)),candidates.push(join4(dir,"..",CONDUCTOR_BIN_FILENAME))}typeof deps.argv1=="string"&&deps.argv1.length>0&&basename2(deps.argv1)===CONDUCTOR_BIN_FILENAME&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical;try{canonical=deps.realpath(candidate)}catch{continue}if(canonical!==null&&!seen.has(canonical)){seen.add(canonical);try{if(deps.isFile(canonical))return{ok:!0,path:canonical}}catch{continue}}}return{ok:!1,reason:CONDUCTOR_HOOK_BIN_UNRESOLVED_REASON}}function moduleFilePath2(moduleUrl){try{return fileURLToPath4(moduleUrl)}catch{return null}}function defaultRealpath2(filePath){try{return realpathSync2(filePath)}catch{return null}}function defaultIsFile2(filePath){try{return statSync2(filePath).isFile()}catch{return!1}}function wrapManagedBlock(body){return[BRIDGE_CONDUCTOR_HOOK_START,body,BRIDGE_CONDUCTOR_HOOK_END].join(`
3862
3862
  `)}function buildPostCommitHookSnippet(conductorBin){let body=["# Managed by Bridge conductor (BAPI-395). Local, unversioned, opportunistic, bypassable.",`( node ${JSON.stringify(conductorBin)} git-hook post-commit >/dev/null 2>&1 & ) || true`].join(`
3863
3863
  `);return wrapManagedBlock(body)}function buildReferenceTransactionHookSnippet(conductorBin){let body=["# Managed by Bridge conductor (BAPI-395). Local, unversioned, opportunistic, bypassable.",'__bridge_tmp="$(mktemp 2>/dev/null || echo "/tmp/bridge-conductor-refs.$$")"','cat > "$__bridge_tmp"',`( node ${JSON.stringify(conductorBin)} git-hook reference-transaction --phase "$1" --stdin-file "$__bridge_tmp" >/dev/null 2>&1 & ) || true`].join(`
3864
3864
  `);return wrapManagedBlock(body)}function looksBinary(content){return content.includes("\0")}function mergeManagedHookSnippet(existing,managedBlock){if(existing===null||existing.length===0)return{ok:!0,content:`#!/bin/sh
@@ -4268,7 +4268,7 @@ ${attempt(3)}
4268
4268
  `);SCOPE_REPARSE_VALUE_FLAGS=new Set(["--epic-run-id","--scope-id"]),DISPATCH_REMEDIATION_VALUE_FLAGS=new Set(["--epic-run-id","--ticket-key","--context-file"]),MERGE_CHILD_VALUE_FLAGS=new Set(["--epic-run-id","--pr"]);SCOPE_REPARSE_USAGE=["Usage: epic-implementer scope-reparse --epic-run-id <id> --scope-id <scope> [--json]","","Re-drive the run's OWN index scope through the retry-reparse lifecycle and","schedule its parse. The scope must be the one bound to the named run, or the","server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints the scope","lifecycle before \u2192 after; it does not wait for the parse to finish."].join(`
4269
4269
  `);DISPATCH_REMEDIATION_USAGE=["Usage: epic-implementer dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path> [--json]","","Dispatch a remediation job for a ticket at code_review (or parked there),","carrying your driver context to the worker. The SERVER chooses the job type","(remediate, or reconcile_overlap for a conflict/overlap ticket) and admits it","through the reconciler's own strict-serial lane, remediation budget, and","dedup key. A parked ticket is resumed in the same step.","","--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES",`(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,"printed, logged, or echoed \u2014 it reaches only the worker's prompt."].join(`
4270
4270
  `);MERGE_CHILD_USAGE=["Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]","","Merge the run's child pull request through the shared merge service. You give","ONLY the PR number: the server finds the one ticket in the run bound to it","(or refuses, TICKET_NOT_IN_RUN, with nothing written) and composes the head","commit and gate identity itself. Every merge guard applies, and a merge","advances the run's scope on the same path as the MCP merge tool. A PR that is","already merged is a successful no-op.","","When the repository merges locally (the default), your own `gh` performs the","merge \u2014 run this from inside the repository's checkout, with `gh auth status`","passing."].join(`
4271
- `)}});var init_pr_review_producer=__esm({"src/conductor/pr-review-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_bridge_api_client();init_producer_ledger()}});async function _fetchGateConfigDefault(access2){let setup=await fetchEffectiveSupervisorSetup(access2);if(setup.source!=="none")return setup.done_gate_config??void 0}function buildPrOpenedEventInput(binding,runId=null,workerId=null){let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};binding.head_ref!==void 0&&(details.head_ref=binding.head_ref);let data={summary:`PR ${binding.subject} observed`,status:"open",details};return binding.url!==void 0&&(data.references={url:binding.url}),{source:"git",type:"git.pr_opened",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data}}function buildCiObservationEventInput(binding,snapshot,runId=null,workerId=null){if(snapshot.checks.length===0||!snapshot.checks.every(c=>c.complete))return null;let allGreen=snapshot.checks.every(c=>c.green),type=allGreen?"ci.passed":"ci.failed",details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,checks:snapshot.checks,unknown_checks:snapshot.unknown_checks,check_state_hash:snapshot.check_state_hash};return{source:"ci",type,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{summary:allGreen?`CI passed for ${binding.subject}`:`CI failed for ${binding.subject}`,status:allGreen?"passed":"failed",details}}}function buildGateMetEventInput(binding,evaluation,runId=null,workerId=null){return!evaluation.met||!evaluation.gateEventData?null:{source:"conductor",type:"gate.met",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{...evaluation.gateEventData}}}async function observePrCiFromPollResponse(commitRef,pollResponse,deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,now=deps.now??(()=>new Date().toISOString()),bindingResult=resolveBinding({cwd:deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{binding:null,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:`no binding: ${bindingResult.reason}`};let binding=bindingResult.binding;if(commitRef.trim().toLowerCase()!==binding.head_sha)return{binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"commit ref does not match PR head"};let run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let snapshot=normalizeCiSnapshot(pollResponse),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)return result.ci_status="pending",result;result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted;let resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess2();if(accessResult.ok){let access2=accessResult.access;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let rawConfig;try{rawConfig=await fetchGateConfig(access2)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig),requiresReview=gateConfig.conditions.some(c=>c.type===REVIEW_STATE);if(gateConfig.enabled&&gateConfig.valid&&requiresReview&&(result.reason="review-gated config: gate.met deferred to wait_for_done_gate (poll path is CI-only)"),gateConfig.enabled&&gateConfig.valid&&!requiresReview){let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now());if(evaluation.met){result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}}}}}catch{}return result}function extractTicketKeyFromRef(headRef){if(!headRef)return null;let match=/([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);return match?match[1].toUpperCase():null}async function resolveDispatchRunIdForBinding(access2,binding,fetchImpl=fetch){let ticketKey=extractTicketKeyFromRef(binding.head_ref);if(!ticketKey)return null;let activeRuns;try{activeRuns=await fetchActiveEpicRuns(access2,fetchImpl)}catch{return null}for(let run of activeRuns)try{let dispatch=(await fetchEpicRunState(access2,run.epic_key,fetchImpl)).dispatches.find(d=>d.ticket_key===ticketKey&&d.run_id!==null);if(dispatch?.run_id)return dispatch.run_id}catch{}return null}var PRODUCER_OBSERVED_VIA,init_pr_ci_producer=__esm({"src/conductor/pr-ci-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_pr_review_producer();init_bridge_api_client();init_pr_discovery();init_producer_ledger();init_store();PRODUCER_OBSERVED_VIA="pr-ci-producer"}});import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as z17}from"zod";import{writeFile as writeFile15,mkdir as mkdir16,readFile as readFile21,stat as stat12,rename as rename7,chmod as chmod4,unlink as unlink7,mkdtemp as mkdtemp4,rm as rm7,readdir as readdir7,open as open6}from"fs/promises";import path58 from"path";import os26 from"os";import{fileURLToPath as fileURLToPath4}from"url";import{randomUUID as randomUUID5}from"node:crypto";var PIPELINES={"greenfield-setup":{name:"greenfield-setup",description:"Onboard a brand-new project that has no code to learn from yet. Offers a bounded interview (or generic defaults), ratifies the open non-functional requirements on a decision page, settles version control, convenes a general council, synthesizes one coherent set of project standards behind a single approval, writes the unset standards fields, and closes with the one authoritative install-manifest apply plus the read-after-write manifest /install-bridge resumes from.",variables:["docs_dir","repo_name"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directory by running: mkdir -p {docs_dir}/greenfield/{repo_name}/standards",description:"Create the greenfield working directory"},{type:"mcp_call",id:"read_install_manifest",tool:"get_install_manifest",params:{},description:"Read the install manifest once and keep its snapshot token for the final apply"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Check whether architecture_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Check whether review_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Check whether documentation_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Check whether unit_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Check whether e2e_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Check whether frontend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Check whether backend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Check whether template_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Check whether style_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Check whether design_principles already has content",on_error:"warn_and_continue"},{type:"agent_task",id:"greenfield_interview",instruction_file:"greenfield-interview.md",description:"Offer the interview, settle the project framing and version control, and commit the decisions"},{type:"agent_task",id:"synthesize_standards",instruction_file:"greenfield-synthesize-standards.md",description:"Draft the project standards from the committed framing and take one approval"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Save the architecture standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Save the code-review standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Save the documentation standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the unit-testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the end-to-end testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the frontend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the backend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the template correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the style correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/greenfield/{repo_name}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Save the design principles",on_error:"warn_and_continue"},{type:"agent_task",id:"apply_and_report",instruction_file:"greenfield-apply-and-report.md",description:"Save the project setup and report where the project stands"}]},"idea-to-ticket":{name:"Idea to Ticket",description:"Convert a short human idea into either a single Jira Task/Spike or a Jira Epic plus decomposed child tickets.",variables:["idea","idea_hash","slug","run_id","docs_dir","allow_duplicate","auto_approve_external","max_children"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"ping",params:{},description:"Resolve the docs directory for run artifacts: read docs_dir from the first (JSON) content item of this ping result"},{type:"mcp_call",tool:"get_project_standards",params:{},description:"Fetch project standards for screening (optional)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"preflight-and-readiness.md",description:"Initialize run directory, classify readiness and scope"},{type:"agent_task",instruction_file:"research-decision.md",description:"Decide which research tools to run for this idea"},{type:"agent_task",instruction_file:"execute-research.md",description:"Execute the planned research and produce the research pack",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"duplicate-and-context-scan.md",description:"Scan Jira for duplicate/related tickets"},{type:"agent_task",instruction_file:"screen-and-resolve.md",description:"Build standards checklist and resolve open questions"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{id:"comp-analysis",type:"agent_task",instruction_file:"comp-analysis.md",description:"Analyze any design comp and map it to existing codebase components/templates/tokens/routes (gated, backend-safe)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"draft-and-critique.md",description:"Draft the ticket(s) and run a hygiene/critique pass"},{type:"agent_task",instruction_file:"upload-and-track.md",description:"Upload to Jira and track the new ticket(s) idempotently"}]},"implement-ticket":{name:"implement-ticket",description:"Generate plan, produce artifacts, checkpoint them to origin, run the plan's remaining verification faithfully, commit, open PR, close post-PR gaps, and monitor CI. Re-entrant: recovery jobs (ci_fix/remediate/rebase) safely re-run this whole recipe from step 1 \u2014 the plan-generation step reuses a fresh existing plan (reused: true, no new run) and the produce phase recognizes work already recorded in the durable ledger instead of redoing it.",variables:["ticket_key","docs_dir","provider"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_plan_generation",params:{ticket_number:"{ticket_key}",wait_for_result:!0,save_locally:!0,provider:"{provider}"},description:"Generate implementation plan. Re-entry-safe: a non-forced request against a fresh existing plan returns reused: true and runs nothing new \u2014 the expected fast path on recovery re-entry. force is reserved for deliberate regeneration; this step never sets it. On a GATEWAY_TIMEOUT/504 response, poll get_plan (or the response's recovery_get URL) until a terminal result \u2014 never reissue this step."},{type:"agent_task",instruction_file:"execute-plan.md",description:"Produce the plan's implementation artifacts"},{type:"agent_task",instruction_file:"checkpoint-work.md",description:"Checkpoint produced work to origin before verification begins"},{type:"agent_task",instruction_file:"execute-plan-verification.md",description:"Execute the plan's remaining review, test, and rendered-UI steps before the PR opens"},{type:"agent_task",instruction_file:"commit-and-push.md",description:"Stage, commit, and push changes",requires_approval:!0},{type:"agent_task",instruction_file:"create-pr.md",description:"Create a pull request with a title derived from the commit subject"},{type:"agent_task",instruction_file:"verify-plan.md",description:"Close post-PR plan gaps and push any correction"},{type:"mcp_call",tool:"update_jira_status",params:{ticket_number:"{ticket_key}",target_status:"auto"},description:"Resolve and transition ticket to post-PR status",on_error:"warn_and_continue"},{type:"mcp_call",tool:"config_field",params:{operation:"get",field_name:"ci_followup_config"},description:"Fetch repo-specific CI follow-up configuration",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"monitor-ci-checks.md",description:"Monitor CI checks and report results"}]},"learn-repository":{name:"learn-repository",description:"Learn and document the repository's configuration fields. Researches every unlearned field in parallel by reading the local codebase, skips fields that are already populated, applies ordinary learned fields automatically with no mid-run approval prompts, and closes with a single confirmation round for the fields the server requires a human to confirm.",variables:["docs_dir"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directories by running: mkdir -p {docs_dir}/standards {docs_dir}/tmp",description:"Create output directories for standards drafts"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Fetch current architecture_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Fetch current review_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Fetch current documentation_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Fetch current unit_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Fetch current e2e_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Fetch current frontend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Fetch current backend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Fetch current template_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Fetch current style_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Fetch current design_principles value",on_error:"warn_and_continue"},{type:"agent_task",id:"research_fan_out",instruction_file:"learn-repository-fan-out.md",description:"Research all learned fields in parallel (one subagent per field)"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Upload architecture_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Upload review_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Upload documentation_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload unit_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload e2e_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload frontend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload backend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload template_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload style_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Upload design_principles to config",on_error:"warn_and_continue"},{type:"agent_task",id:"batched_confirmations",instruction_file:"learn-repository-confirmations.md",description:"Confirm and apply confirmation-required fields in one batched round"}]},"plan-epic":{name:"plan-epic",description:"Decompose a feature epic into sub-tasks with structured exploration documents for each.",variables:["epic_description","epic_slug","docs_dir","epic_key"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction_file:"assess-epic-research-needs.md",description:"Assess research needs for the epic"},{type:"agent_task",instruction_file:"execute-epic-research.md",description:"Execute research plan",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"explore-epic-codebase.md",description:"Perform holistic codebase exploration for the epic"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{type:"agent_task",instruction_file:"clarify-open-nfrs.md",description:"Clarify open non-functional requirements with the user via a planning decision page"},{type:"agent_task",instruction_file:"decompose-epic.md",description:"Decompose epic into sub-tasks and get user approval"},{type:"agent_task",instruction_file:"explore-epic-subtasks.md",description:"Perform focused exploration for each sub-task"},{type:"agent_task",instruction_file:"assess-conductor-eligibility.md",description:"Assessing conductor eligibility"},{type:"agent_task",instruction_file:"write-epic-summary.md",description:"Write epic overview and summary document"},{type:"agent_task",instruction_file:"store-and-approve-epic-plan.md",description:"Create epic run, store the plan DAG, and approve it in the backend"}]},"review-ticket":{name:"review-ticket",description:"Review a ticket, evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. A single request_ticket_review step runs the backend's resolved difficulty-adaptive review policy, which internally runs any second-opinion rounds server-side. Pass --rounds=1|2 to forward an explicit round count that forces the review shape (1 = single pass, 2 = full second-opinion review); omit it to let the backend policy executor decide adaptively. The round count is injected into the request_ticket_review step as a real integer by get_pipeline_recipe, not substituted as a recipe variable. BAPI-1121: this recipe has SEVEN steps, not nine \u2014 the former standalone retrieve-clarifying-questions and retrieve-ticket-critique MCP calls were removed because generation-aware persistence is performed by the shared artifact gate (ticket-review-artifact-gate.ts), which this recipe reaches through wait_for_ticket_review (see the wait step's instruction). BAPI-1129: that gate is the sole persistence implementation for the two review documents, shared by both calling conventions \u2014 blocking request_ticket_review(wait_for_result: true) reaches the same gate \u2014 so no step here may retrieve or save a review document on its own.",variables:["ticket_key","docs_dir","base_branch","base_sha","no_refresh_base"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_ticket_review",params:{ticket_number:"{ticket_key}",wait_for_result:!1,save_locally:!0,provider:"{provider}"},description:"Submit the combined clarify+critique review and RETURN IMMEDIATELY. The backend runs the resolved difficulty-adaptive policy (including any second-opinion rounds) server-side; an explicit rounds value (1|2) forces the shape. BAPI-1104: this step no longer blocks. Blocking here bound the whole review to the 900-second MCP client deadline, and a review that outran it killed the worker mid-wait \u2014 it exited without .conductor/result.json, produced MissingVerdictArtifact, and was requeued, spending a full attempt on work that had already been done. The wait moved to the following step, which is resumable.",on_error:"warn_and_continue"},{type:"agent_task",id:"wait-for-ticket-review",instruction:'FIRST, check whether a review was actually submitted. The previous step runs with `on_error: warn_and_continue`, so a rejected submission (a 402, 403 or 500 from the generate endpoint, or a dropped connection) lets the pipeline continue into this step. If that step returned an error message rather than its submission confirmation or a `recovery_get` envelope, do NOT wait: nothing is running, and `wait_for_ticket_review` cannot tell "never submitted" from "still running" \u2014 it would read `pending` in every window until the caller\'s own timeout. Treat a failed submission as a terminal failure, report the error the previous step gave, and take the verdict contract\'s `revise` path.\n\nOtherwise, wait for the review submitted by the previous step to finish, using repeated BOUNDED windows.\n\nCall `wait_for_ticket_review` with `ticket_number` set to the ticket key. Each call occupies at most 240 seconds and returns a JSON body with a `state` field:\n\n- `state: "pending"` \u2014 NORMAL and expected. The review is still running. Call the tool again immediately. Do NOT sleep, do NOT poll in a shell loop, and do NOT re-run `request_ticket_review`: re-submitting starts a second billable multi-model review and abandons the one already in flight.\n- `state: "ready"` \u2014 the review is over. BAPI-1121: a ready result has ALREADY persisted exactly the accepted documents and reconciled the canonical source files \u2014 it writes every leg in `saved_legs` to its canonical path and removes the stale canonical file for every leg in `withheld_legs`. Do NOT call `get_clarifying_questions` or `get_ticket_critique` after a ready result; those retrieval calls were removed from this recipe because reading them again could reintroduce exactly the stale content the wait just withheld. Treat `saved_legs` as the authoritative source set for the next steps, not file presence.\n - `freshness_basis: "generation"` is the normal case: `advanced_legs`/`saved_legs` are current-review evidence, proven newer than the submission. Any leg in `withheld_legs` was delivered but NOT proven current \u2014 surface its `reason` (`generation_not_advanced` or `identity_unreadable`) as a visible warning; do not describe the result as an ordinary cost-saved one-leg review when a withholding occurred. A leg in `absent_legs` with `reason: "not_delivered"` is an ordinary policy omission (e.g. an Easy-ticket clarify-only review) unless its status also flags it as policy-expected, in which case call that out too.\n - `freshness_basis: "terminality_only"` means no submission baseline was available (e.g. a resumed session) \u2014 `advanced_legs` is empty by construction and `saved_legs` was accepted on terminal delivery alone. Surface this prominently as a DEGRADED-freshness warning: the saved artifact(s) may be indistinguishable from a prior attachment, and the downstream identity-footer agreement check is the remaining backstop. Retain this warning when reporting to the next steps rather than treating a `terminality_only` ready result as equivalent to a `generation`-backed one.\n - If the result instead carries `state: "error"` alongside a persistence failure (an `ARTIFACT_PERSISTENCE_FAILED` `error`/`status`/`message` envelope), HALT rather than proceeding with whatever files happen to exist on disk \u2014 report the failure; the accepted legs already on disk before the failure are named in the envelope and are not lost, but nothing new was persisted for this window.\n- `state: "error"` \u2014 terminal. Report the `reason` and stop; the review will not complete on its own. `UPSTREAM_TERMINAL` means the review failed or expired server-side \u2014 take the verdict contract\'s `revise` path; do NOT open another wait window and do NOT re-run `request_ticket_review`. `INVALID_RESPONSE` means the delivered document could not be read.\n\nKeep looping on `pending` for as long as this session\'s own budget allows. There is no iteration cap here: the bound is the caller\'s \u2014 for the conductor\'s spec-review worker that is the existing 120-minute job timeout, which produces the ordinary liveness outcome if the review never lands. Never fabricate a verdict or a review result from a pending state.',description:"Wait for the submitted review in repeated bounded windows; a ready result has already persisted the accepted documents and reconciled the canonical files (BAPI-1121). Resumable: a pending result is a normal observation, not a failure.",on_error:"halt"},{type:"mcp_call",id:"materialize-fresh-base",tool:"fresh_base",params:{action:"materialize",base_branch:"{base_branch}",base_sha:"{base_sha}",no_refresh_base:"{no_refresh_base}"},description:"Materialize a freshly-fetched, immutably-pinned origin/<base_branch> tree in an isolated temp dir for grounding",on_error:"halt"},{type:"agent_task",instruction_file:"evaluate-and-recommend.md",description:"Evaluate ticket-review findings against the freshly-materialized codebase and produce the combined review-and-resolution document at {docs_dir}/review/{ticket_key}-review-and-resolution.md"},{type:"agent_task",instruction_file:"capture-review-decisions.md",description:"Generate HTML decision page, capture user decisions, and interpretively rewrite clarifying questions and critique docs"},{type:"mcp_call",id:"cleanup-fresh-base",tool:"fresh_base",params:{action:"cleanup",fresh_base_root:"<the fresh_base_root path returned by the earlier fresh_base materialize step's result>"},description:"Remove the temp workspace created by the earlier fresh_base materialize step. fresh_base_root is a RUNTIME value, not a static recipe variable: call this tool with the actual path string returned by that step's result, not the placeholder text shown in params above.",on_error:"warn_and_continue"}]}},INSTRUCTIONS={"assess-conductor-eligibility.md":'Assess conductor merge/review eligibility for the frozen epic child set.\n\n## Instructions\n\n1. Read the approved decomposition from `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` to\n obtain each sub-task\'s frozen title and scope. Do NOT re-split or rescope children here \u2014\n the decomposition is frozen; this stage only classifies it.\n\n2. For each sub-task, also read its exploration document\n `{docs_dir}/epic-plans/{epic_slug}/explorations/NN-{subtask-slug}.md` (written by\n `explore-epic-subtasks`) if it exists. Its Context, Relevant Code, and Recommendation\n sections are the sub-task\'s most complete available text \u2014 the closest thing to a\n description/requirements body that exists at this stage, since no Jira ticket has been\n created yet.\n\n3. Build one ordered child list, preserving decomposition order, where each child carries:\n - `id`: the sub-task\'s placeholder identifier (its position, e.g. `"1"`, or a slug \u2014 the\n real Jira key does not exist yet).\n - `title`: the sub-task title from `epic-plan.md`.\n - `scope`: the sub-task\'s Scope field from `epic-plan.md`.\n - `description`/`requirements`: the sub-task\'s exploration document content, when one\n exists; omit when it does not (missing text is treated as empty, never invented).\n\n4. Call the `assess_epic_conductor_eligibility` MCP tool exactly once with the complete\n ordered child list from step 3. Never call it per-child and never call it with a partial\n subset \u2014 a partial call cannot produce a meaningful epic-wide count.\n\n5. Write the tool\'s result verbatim as a structured JSON artifact to\n `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`. The artifact\'s shape mirrors\n the tool\'s own result type:\n - On `"status": "assessed"`: `totalChildren`, `predictedWorkflowChildren`, `reason`,\n `reviewSubsetChildren`, `containmentChildren`, and `affectedChildren` (each with `id`,\n `title`, `matchedPaths`, `requiresHandReview`, `requiresContainmentReview`,\n `matchedHazards`). `containmentChildren` is an independent ticket-text lower bound \u2014 a\n child whose text describes a destructive primitive or a change to conductor enforcement\n code \u2014 and is counted separately from `predictedWorkflowChildren`/`reviewSubsetChildren`.\n `affectedChildren` is the union of workflow-affected and containment-affected children, not\n a human-merge-only list: a containment-only child (no matched workflow paths) still appears\n here, with `requiresContainmentReview: true` and its bounded `matchedHazards`.\n - On `"status": "unavailable"`: exactly that status, and nothing else. **Never** substitute\n zero counts or an empty `affectedChildren` list for an unavailable assessment \u2014 an\n unavailable result and a genuine zero-workflow-children/zero-containment-children result are\n different facts, and collapsing them into the same shape is exactly the failure this stage\n exists to prevent.\n\n6. This stage performs no other action. It does not start, dispatch, or spawn any worker, and\n it makes no Jira call \u2014 it is read frozen inputs, classify, write one artifact.\n\n## Return\n\nConfirm the artifact was written to `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`\nand report its status: either the assessed counts (`N of M` children predicted to modify\nworkflow files, `K of those N` predicted to require hand review, and `P of M` predicted to\ndescribe a containment hazard \u2014 reported independently, since a child can be containment-flagged\nwithout being workflow-modifying) or that the assessment was unavailable.\n',"assess-epic-research-needs.md":`Analyze the epic description and build a structured research plan.
4271
+ `)}});var init_pr_review_producer=__esm({"src/conductor/pr-review-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_bridge_api_client();init_producer_ledger()}});async function _fetchGateConfigDefault(access2){let setup=await fetchEffectiveSupervisorSetup(access2);if(setup.source!=="none")return setup.done_gate_config??void 0}function buildPrOpenedEventInput(binding,runId=null,workerId=null){let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};binding.head_ref!==void 0&&(details.head_ref=binding.head_ref);let data={summary:`PR ${binding.subject} observed`,status:"open",details};return binding.url!==void 0&&(data.references={url:binding.url}),{source:"git",type:"git.pr_opened",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data}}function buildCiObservationEventInput(binding,snapshot,runId=null,workerId=null){if(snapshot.checks.length===0||!snapshot.checks.every(c=>c.complete))return null;let allGreen=snapshot.checks.every(c=>c.green),type=allGreen?"ci.passed":"ci.failed",details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,checks:snapshot.checks,unknown_checks:snapshot.unknown_checks,check_state_hash:snapshot.check_state_hash};return{source:"ci",type,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{summary:allGreen?`CI passed for ${binding.subject}`:`CI failed for ${binding.subject}`,status:allGreen?"passed":"failed",details}}}function buildGateMetEventInput(binding,evaluation,runId=null,workerId=null){return!evaluation.met||!evaluation.gateEventData?null:{source:"conductor",type:"gate.met",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{...evaluation.gateEventData}}}async function observePrCiFromPollResponse(commitRef,pollResponse,deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,now=deps.now??(()=>new Date().toISOString()),bindingResult=resolveBinding({cwd:deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{binding:null,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:`no binding: ${bindingResult.reason}`};let binding=bindingResult.binding;if(commitRef.trim().toLowerCase()!==binding.head_sha)return{binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"commit ref does not match PR head"};let run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let snapshot=normalizeCiSnapshot(pollResponse),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)return result.ci_status="pending",result;result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted;let resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess2();if(accessResult.ok){let access2=accessResult.access;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let rawConfig;try{rawConfig=await fetchGateConfig(access2)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig),requiresReview=gateConfig.conditions.some(c=>c.type===REVIEW_STATE);if(gateConfig.enabled&&gateConfig.valid&&requiresReview&&(result.reason="review-gated config: gate.met deferred to wait_for_done_gate (poll path is CI-only)"),gateConfig.enabled&&gateConfig.valid&&!requiresReview){let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now());if(evaluation.met){result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}}}}}catch{}return result}function extractTicketKeyFromRef(headRef){if(!headRef)return null;let match=/([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);return match?match[1].toUpperCase():null}async function resolveDispatchRunIdForBinding(access2,binding,fetchImpl=fetch){let ticketKey=extractTicketKeyFromRef(binding.head_ref);if(!ticketKey)return null;let activeRuns;try{activeRuns=await fetchActiveEpicRuns(access2,fetchImpl)}catch{return null}for(let run of activeRuns)try{let dispatch=(await fetchEpicRunState(access2,run.epic_key,fetchImpl)).dispatches.find(d=>d.ticket_key===ticketKey&&d.run_id!==null);if(dispatch?.run_id)return dispatch.run_id}catch{}return null}var PRODUCER_OBSERVED_VIA,init_pr_ci_producer=__esm({"src/conductor/pr-ci-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_pr_review_producer();init_bridge_api_client();init_pr_discovery();init_producer_ledger();init_store();PRODUCER_OBSERVED_VIA="pr-ci-producer"}});import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as z17}from"zod";import{writeFile as writeFile15,mkdir as mkdir16,readFile as readFile21,stat as stat12,rename as rename7,chmod as chmod4,unlink as unlink7,mkdtemp as mkdtemp4,rm as rm7,readdir as readdir7,open as open6}from"fs/promises";import path58 from"path";import os26 from"os";import{fileURLToPath as fileURLToPath5}from"url";import{randomUUID as randomUUID5}from"node:crypto";var PIPELINES={"greenfield-setup":{name:"greenfield-setup",description:"Onboard a brand-new project that has no code to learn from yet. Offers a bounded interview (or generic defaults), ratifies the open non-functional requirements on a decision page, settles version control, convenes a general council, synthesizes one coherent set of project standards behind a single approval, writes the unset standards fields, and closes with the one authoritative install-manifest apply plus the read-after-write manifest /install-bridge resumes from.",variables:["docs_dir","repo_name"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directory by running: mkdir -p {docs_dir}/greenfield/{repo_name}/standards",description:"Create the greenfield working directory"},{type:"mcp_call",id:"read_install_manifest",tool:"get_install_manifest",params:{},description:"Read the install manifest once and keep its snapshot token for the final apply"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Check whether architecture_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Check whether review_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Check whether documentation_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Check whether unit_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Check whether e2e_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Check whether frontend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Check whether backend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Check whether template_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Check whether style_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Check whether design_principles already has content",on_error:"warn_and_continue"},{type:"agent_task",id:"greenfield_interview",instruction_file:"greenfield-interview.md",description:"Offer the interview, settle the project framing and version control, and commit the decisions"},{type:"agent_task",id:"synthesize_standards",instruction_file:"greenfield-synthesize-standards.md",description:"Draft the project standards from the committed framing and take one approval"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Save the architecture standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Save the code-review standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Save the documentation standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the unit-testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the end-to-end testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the frontend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the backend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the template correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the style correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/greenfield/{repo_name}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Save the design principles",on_error:"warn_and_continue"},{type:"agent_task",id:"apply_and_report",instruction_file:"greenfield-apply-and-report.md",description:"Save the project setup and report where the project stands"}]},"idea-to-ticket":{name:"Idea to Ticket",description:"Convert a short human idea into either a single Jira Task/Spike or a Jira Epic plus decomposed child tickets.",variables:["idea","idea_hash","slug","run_id","docs_dir","allow_duplicate","auto_approve_external","max_children"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"ping",params:{},description:"Resolve the docs directory for run artifacts: read docs_dir from the first (JSON) content item of this ping result"},{type:"mcp_call",tool:"get_project_standards",params:{},description:"Fetch project standards for screening (optional)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"preflight-and-readiness.md",description:"Initialize run directory, classify readiness and scope"},{type:"agent_task",instruction_file:"research-decision.md",description:"Decide which research tools to run for this idea"},{type:"agent_task",instruction_file:"execute-research.md",description:"Execute the planned research and produce the research pack",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"duplicate-and-context-scan.md",description:"Scan Jira for duplicate/related tickets"},{type:"agent_task",instruction_file:"screen-and-resolve.md",description:"Build standards checklist and resolve open questions"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{id:"comp-analysis",type:"agent_task",instruction_file:"comp-analysis.md",description:"Analyze any design comp and map it to existing codebase components/templates/tokens/routes (gated, backend-safe)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"draft-and-critique.md",description:"Draft the ticket(s) and run a hygiene/critique pass"},{type:"agent_task",instruction_file:"upload-and-track.md",description:"Upload to Jira and track the new ticket(s) idempotently"}]},"implement-ticket":{name:"implement-ticket",description:"Generate plan, produce artifacts, checkpoint them to origin, run the plan's remaining verification faithfully, commit, open PR, close post-PR gaps, and monitor CI. Re-entrant: recovery jobs (ci_fix/remediate/rebase) safely re-run this whole recipe from step 1 \u2014 the plan-generation step reuses a fresh existing plan (reused: true, no new run) and the produce phase recognizes work already recorded in the durable ledger instead of redoing it.",variables:["ticket_key","docs_dir","provider"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_plan_generation",params:{ticket_number:"{ticket_key}",wait_for_result:!1,save_locally:!0,provider:"{provider}"},description:"Submit implementation-plan generation without waiting through an MCP client deadline. Re-entry-safe: the backend reuses a fresh existing plan (reused: true, no new run). force is reserved for deliberate regeneration; this step never sets it. On a dropped connection or other ambiguous submission outcome, use get_plan to inspect the existing run \u2014 never reissue this step automatically."},{type:"agent_task",instruction_file:"execute-plan.md",description:"Produce the plan's implementation artifacts"},{type:"agent_task",instruction_file:"checkpoint-work.md",description:"Checkpoint produced work to origin before verification begins"},{type:"agent_task",instruction_file:"execute-plan-verification.md",description:"Execute the plan's remaining review, test, and rendered-UI steps before the PR opens"},{type:"agent_task",instruction_file:"commit-and-push.md",description:"Stage, commit, and push changes",requires_approval:!0},{type:"agent_task",instruction_file:"create-pr.md",description:"Create a pull request with a title derived from the commit subject"},{type:"agent_task",instruction_file:"verify-plan.md",description:"Close post-PR plan gaps and push any correction"},{type:"mcp_call",tool:"update_jira_status",params:{ticket_number:"{ticket_key}",target_status:"auto"},description:"Resolve and transition ticket to post-PR status",on_error:"warn_and_continue"},{type:"mcp_call",tool:"config_field",params:{operation:"get",field_name:"ci_followup_config"},description:"Fetch repo-specific CI follow-up configuration",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"monitor-ci-checks.md",description:"Monitor CI checks and report results"}]},"learn-repository":{name:"learn-repository",description:"Learn and document the repository's configuration fields. Researches every unlearned field in parallel by reading the local codebase, skips fields that are already populated, applies ordinary learned fields automatically with no mid-run approval prompts, and closes with a single confirmation round for the fields the server requires a human to confirm.",variables:["docs_dir"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directories by running: mkdir -p {docs_dir}/standards {docs_dir}/tmp",description:"Create output directories for standards drafts"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Fetch current architecture_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Fetch current review_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Fetch current documentation_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Fetch current unit_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Fetch current e2e_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Fetch current frontend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Fetch current backend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Fetch current template_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Fetch current style_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Fetch current design_principles value",on_error:"warn_and_continue"},{type:"agent_task",id:"research_fan_out",instruction_file:"learn-repository-fan-out.md",description:"Research all learned fields in parallel (one subagent per field)"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Upload architecture_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Upload review_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Upload documentation_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload unit_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload e2e_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload frontend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload backend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload template_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload style_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Upload design_principles to config",on_error:"warn_and_continue"},{type:"agent_task",id:"batched_confirmations",instruction_file:"learn-repository-confirmations.md",description:"Confirm and apply confirmation-required fields in one batched round"}]},"plan-epic":{name:"plan-epic",description:"Decompose a feature epic into sub-tasks with structured exploration documents for each.",variables:["epic_description","epic_slug","docs_dir","epic_key"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction_file:"assess-epic-research-needs.md",description:"Assess research needs for the epic"},{type:"agent_task",instruction_file:"execute-epic-research.md",description:"Execute research plan",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"explore-epic-codebase.md",description:"Perform holistic codebase exploration for the epic"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{type:"agent_task",instruction_file:"clarify-open-nfrs.md",description:"Clarify open non-functional requirements with the user via a planning decision page"},{type:"agent_task",instruction_file:"decompose-epic.md",description:"Decompose epic into sub-tasks and get user approval"},{type:"agent_task",instruction_file:"explore-epic-subtasks.md",description:"Perform focused exploration for each sub-task"},{type:"agent_task",instruction_file:"assess-conductor-eligibility.md",description:"Assessing conductor eligibility"},{type:"agent_task",instruction_file:"write-epic-summary.md",description:"Write epic overview and summary document"},{type:"agent_task",instruction_file:"store-and-approve-epic-plan.md",description:"Create epic run, store the plan DAG, and approve it in the backend"}]},"review-ticket":{name:"review-ticket",description:"Review a ticket, evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. A single request_ticket_review step runs the backend's resolved difficulty-adaptive review policy, which internally runs any second-opinion rounds server-side. Pass --rounds=1|2 to forward an explicit round count that forces the review shape (1 = single pass, 2 = full second-opinion review); omit it to let the backend policy executor decide adaptively. The round count is injected into the request_ticket_review step as a real integer by get_pipeline_recipe, not substituted as a recipe variable. BAPI-1121: this recipe has SEVEN steps, not nine \u2014 the former standalone retrieve-clarifying-questions and retrieve-ticket-critique MCP calls were removed because generation-aware persistence is performed by the shared artifact gate (ticket-review-artifact-gate.ts), which this recipe reaches through wait_for_ticket_review (see the wait step's instruction). BAPI-1129: that gate is the sole persistence implementation for the two review documents, shared by both calling conventions \u2014 blocking request_ticket_review(wait_for_result: true) reaches the same gate \u2014 so no step here may retrieve or save a review document on its own.",variables:["ticket_key","docs_dir","base_branch","base_sha","no_refresh_base"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_ticket_review",params:{ticket_number:"{ticket_key}",wait_for_result:!1,save_locally:!0,provider:"{provider}"},description:"Submit the combined clarify+critique review and RETURN IMMEDIATELY. The backend runs the resolved difficulty-adaptive policy (including any second-opinion rounds) server-side; an explicit rounds value (1|2) forces the shape. BAPI-1104: this step no longer blocks. Blocking here bound the whole review to the 900-second MCP client deadline, and a review that outran it killed the worker mid-wait \u2014 it exited without .conductor/result.json, produced MissingVerdictArtifact, and was requeued, spending a full attempt on work that had already been done. The wait moved to the following step, which is resumable.",on_error:"warn_and_continue"},{type:"agent_task",id:"wait-for-ticket-review",instruction:'FIRST, check whether a review was actually submitted. The previous step runs with `on_error: warn_and_continue`, so a rejected submission (a 402, 403 or 500 from the generate endpoint, or a dropped connection) lets the pipeline continue into this step. If that step returned an error message rather than its submission confirmation or a `recovery_get` envelope, do NOT wait: nothing is running, and `wait_for_ticket_review` cannot tell "never submitted" from "still running" \u2014 it would read `pending` in every window until the caller\'s own timeout. Treat a failed submission as a terminal failure, report the error the previous step gave, and take the verdict contract\'s `revise` path.\n\nOtherwise, wait for the review submitted by the previous step to finish, using repeated BOUNDED windows.\n\nCall `wait_for_ticket_review` with `ticket_number` set to the ticket key. Each call uses the default 20-second window (set `budget_ms: 20000` explicitly if needed); do not use a longer window, because some MCP clients time out before a pending result arrives. The tool returns a JSON body with a `state` field:\n\n- `state: "pending"` \u2014 NORMAL and expected. The review is still running. Call the tool again immediately. Do NOT sleep, do NOT poll in a shell loop, and do NOT re-run `request_ticket_review`: re-submitting starts a second billable multi-model review and abandons the one already in flight.\n- `state: "ready"` \u2014 the review is over. BAPI-1121: a ready result has ALREADY persisted exactly the accepted documents and reconciled the canonical source files \u2014 it writes every leg in `saved_legs` to its canonical path and removes the stale canonical file for every leg in `withheld_legs`. Do NOT call `get_clarifying_questions` or `get_ticket_critique` after a ready result; those retrieval calls were removed from this recipe because reading them again could reintroduce exactly the stale content the wait just withheld. Treat `saved_legs` as the authoritative source set for the next steps, not file presence.\n - `freshness_basis: "generation"` is the normal case: `advanced_legs`/`saved_legs` are current-review evidence, proven newer than the submission. Any leg in `withheld_legs` was delivered but NOT proven current \u2014 surface its `reason` (`generation_not_advanced` or `identity_unreadable`) as a visible warning; do not describe the result as an ordinary cost-saved one-leg review when a withholding occurred. A leg in `absent_legs` with `reason: "not_delivered"` is an ordinary policy omission (e.g. an Easy-ticket clarify-only review) unless its status also flags it as policy-expected, in which case call that out too.\n - `freshness_basis: "terminality_only"` means no submission baseline was available (e.g. a resumed session) \u2014 `advanced_legs` is empty by construction and `saved_legs` was accepted on terminal delivery alone. Surface this prominently as a DEGRADED-freshness warning: the saved artifact(s) may be indistinguishable from a prior attachment, and the downstream identity-footer agreement check is the remaining backstop. Retain this warning when reporting to the next steps rather than treating a `terminality_only` ready result as equivalent to a `generation`-backed one.\n - If the result instead carries `state: "error"` alongside a persistence failure (an `ARTIFACT_PERSISTENCE_FAILED` `error`/`status`/`message` envelope), HALT rather than proceeding with whatever files happen to exist on disk \u2014 report the failure; the accepted legs already on disk before the failure are named in the envelope and are not lost, but nothing new was persisted for this window.\n- `state: "error"` \u2014 terminal. Report the `reason` and stop; the review will not complete on its own. `UPSTREAM_TERMINAL` means the review failed or expired server-side \u2014 take the verdict contract\'s `revise` path; do NOT open another wait window and do NOT re-run `request_ticket_review`. `INVALID_RESPONSE` means the delivered document could not be read.\n\nKeep looping on `pending` for as long as this session\'s own budget allows. There is no iteration cap here: the bound is the caller\'s \u2014 for the conductor\'s spec-review worker that is the existing 120-minute job timeout, which produces the ordinary liveness outcome if the review never lands. Never fabricate a verdict or a review result from a pending state.',description:"Wait for the submitted review in repeated bounded windows; a ready result has already persisted the accepted documents and reconciled the canonical files (BAPI-1121). Resumable: a pending result is a normal observation, not a failure.",on_error:"halt"},{type:"mcp_call",id:"materialize-fresh-base",tool:"fresh_base",params:{action:"materialize",base_branch:"{base_branch}",base_sha:"{base_sha}",no_refresh_base:"{no_refresh_base}"},description:"Materialize a freshly-fetched, immutably-pinned origin/<base_branch> tree in an isolated temp dir for grounding",on_error:"halt"},{type:"agent_task",instruction_file:"evaluate-and-recommend.md",description:"Evaluate ticket-review findings against the freshly-materialized codebase and produce the combined review-and-resolution document at {docs_dir}/review/{ticket_key}-review-and-resolution.md"},{type:"agent_task",instruction_file:"capture-review-decisions.md",description:"Generate HTML decision page, capture user decisions, and interpretively rewrite clarifying questions and critique docs"},{type:"mcp_call",id:"cleanup-fresh-base",tool:"fresh_base",params:{action:"cleanup",fresh_base_root:"<the fresh_base_root path returned by the earlier fresh_base materialize step's result>"},description:"Remove the temp workspace created by the earlier fresh_base materialize step. fresh_base_root is a RUNTIME value, not a static recipe variable: call this tool with the actual path string returned by that step's result, not the placeholder text shown in params above.",on_error:"warn_and_continue"}]}},INSTRUCTIONS={"assess-conductor-eligibility.md":'Assess conductor merge/review eligibility for the frozen epic child set.\n\n## Instructions\n\n1. Read the approved decomposition from `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` to\n obtain each sub-task\'s frozen title and scope. Do NOT re-split or rescope children here \u2014\n the decomposition is frozen; this stage only classifies it.\n\n2. For each sub-task, also read its exploration document\n `{docs_dir}/epic-plans/{epic_slug}/explorations/NN-{subtask-slug}.md` (written by\n `explore-epic-subtasks`) if it exists. Its Context, Relevant Code, and Recommendation\n sections are the sub-task\'s most complete available text \u2014 the closest thing to a\n description/requirements body that exists at this stage, since no Jira ticket has been\n created yet.\n\n3. Build one ordered child list, preserving decomposition order, where each child carries:\n - `id`: the sub-task\'s placeholder identifier (its position, e.g. `"1"`, or a slug \u2014 the\n real Jira key does not exist yet).\n - `title`: the sub-task title from `epic-plan.md`.\n - `scope`: the sub-task\'s Scope field from `epic-plan.md`.\n - `description`/`requirements`: the sub-task\'s exploration document content, when one\n exists; omit when it does not (missing text is treated as empty, never invented).\n\n4. Call the `assess_epic_conductor_eligibility` MCP tool exactly once with the complete\n ordered child list from step 3. Never call it per-child and never call it with a partial\n subset \u2014 a partial call cannot produce a meaningful epic-wide count.\n\n5. Write the tool\'s result verbatim as a structured JSON artifact to\n `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`. The artifact\'s shape mirrors\n the tool\'s own result type:\n - On `"status": "assessed"`: `totalChildren`, `predictedWorkflowChildren`, `reason`,\n `reviewSubsetChildren`, `containmentChildren`, and `affectedChildren` (each with `id`,\n `title`, `matchedPaths`, `requiresHandReview`, `requiresContainmentReview`,\n `matchedHazards`). `containmentChildren` is an independent ticket-text lower bound \u2014 a\n child whose text describes a destructive primitive or a change to conductor enforcement\n code \u2014 and is counted separately from `predictedWorkflowChildren`/`reviewSubsetChildren`.\n `affectedChildren` is the union of workflow-affected and containment-affected children, not\n a human-merge-only list: a containment-only child (no matched workflow paths) still appears\n here, with `requiresContainmentReview: true` and its bounded `matchedHazards`.\n - On `"status": "unavailable"`: exactly that status, and nothing else. **Never** substitute\n zero counts or an empty `affectedChildren` list for an unavailable assessment \u2014 an\n unavailable result and a genuine zero-workflow-children/zero-containment-children result are\n different facts, and collapsing them into the same shape is exactly the failure this stage\n exists to prevent.\n\n6. This stage performs no other action. It does not start, dispatch, or spawn any worker, and\n it makes no Jira call \u2014 it is read frozen inputs, classify, write one artifact.\n\n## Return\n\nConfirm the artifact was written to `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`\nand report its status: either the assessed counts (`N of M` children predicted to modify\nworkflow files, `K of those N` predicted to require hand review, and `P of M` predicted to\ndescribe a containment hazard \u2014 reported independently, since a child can be containment-flagged\nwithout being workflow-modifying) or that the assessment was unavailable.\n',"assess-epic-research-needs.md":`Analyze the epic description and build a structured research plan.
4272
4272
 
4273
4273
  ## Epic Description
4274
4274
 
@@ -5622,7 +5622,7 @@ Follow the branch that matches the mode above wherever the two are named.
5622
5622
 
5623
5623
  ## Step 1 \u2014 Retrieve the Plan and Your Routing
5624
5624
 
5625
- 1. Call the \`get_plan\` tool for \`{ticket_key}\` to retrieve the plan body.
5625
+ 1. Call \`get_plan\` for \`{ticket_key}\` with \`budget_ms: 20000\` and \`save_locally: true\`. This opens one bounded wait window for the plan submitted by the prior step. A JSON \`state: "pending"\` is normal: call \`get_plan\` again with the same arguments, without sleeping or re-running \`request_plan_generation\`. Repeat until the tool returns plan markdown. A JSON \`state: "error"\` is terminal; report its reason and stop. If the prior submission failed unambiguously, stop without waiting. Preserve the plan markdown as the source for the remaining steps.
5626
5626
  2. Derive the total number of steps from the returned plan.
5627
5627
  3. Determine your owned set, by mode:
5628
5628
  - **orchestrated** \u2014 read the **Routed phase context** block appended to this
@@ -7871,7 +7871,7 @@ status is unavailable \u2014 an unresolved assessment and a genuine zero are dif
7871
7871
  ## Return
7872
7872
 
7873
7873
  Confirm the overview was written to \`{docs_dir}/epic-plans/{epic_slug}/overview.md\` and report the total sub-task count along with a one-line summary of the epic plan. State whether the goals/NFRs + recommended order were posted as a comment on \`{epic_key}\` or skipped because no epic key was provided.
7874
- `,"phase1/capture-review-decisions.md":'Capture user decisions on review findings for {ticket_key} using the HTML decision page, then interpretively rewrite the clarifying questions and critique docs and upload both to Jira.\n\n<!-- BAPI-977 Phase-1 variant of capture-review-decisions.md. Selected only when\n REVIEW_SYNTHESIS_PHASE1_ENABLED is on. The legacy instruction at\n instructions/capture-review-decisions.md is the disabled path and is unchanged. -->\n\n## Step 1: Read source documents\n\nRead the combined review-and-resolution file:\n- `{docs_dir}/review/{ticket_key}-review-and-resolution.md`\n\nIf the file does not exist or is unreadable, stop and report: "Combined review-and-resolution file not found or unreadable. Run the earlier pipeline steps first."\n\nThe combined file existing but containing no actionable items (empty `Needs Scrutiny` and `Open Questions` sections) is **not** a failure condition \u2014 Step 4 handles the no-decisions-needed flow gracefully when `generate_decision_page` is called with empty `actionable_items`.\n\n## Step 2: Map evaluation items to decision page input\n\nTransform the combined review-and-resolution document into `generate_decision_page` JSON input using these mapping rules:\n\n| Evaluation Section | JSON Field | Mapping Rule |\n|---|---|---|\n| Open Questions (full actionable cards) | `actionable_items` | E-item title \u2192 `question`, `**Source**` \u2192 `source`, `**Original question**` \u2192 `original_question`, `**Why it matters**` \u2192 `why_it_matters`, decision tree branch labels \u2192 `options` (string array, labels only), `**Option consequences**` (parallel to branches) \u2192 `option_consequences`, `**Recommendation explanation**` \u2192 `recommendation_explanation`, `**Assessment**` paragraph plus the item\'s single `**Canonical citation**` \u2192 `codebase_evidence`, `**Recommendation Index**` \u2192 `recommendation_index` |\n| Needs Scrutiny (full actionable cards) | `actionable_items` | Same mapping as Open Questions above |\n| Confirmed Improvements (terse rows) | `clear_improvements` | E-item id \u2192 `id`, E-item title \u2192 `title`, recommended action \u2192 `action`, `Confidence` \u2192 `confidence`, `Source` \u2192 `source` |\n\n### `clear_improvements` carries exactly five fields\n\nA terse Confirmed Improvement row maps to a `clear_improvements` entry containing **only** these five fields, every one of them a non-empty string:\n\n- `id`\n- `title`\n- `action`\n- `confidence`\n- `source`\n\n**Never add anything else to a `clear_improvements` entry.** In particular, do not add `options`, `option_consequences`, `recommendation_index`, `recommendation_explanation`, `question`, `original_question`, `why_it_matters`, or `codebase_evidence` \u2014 not as real values, not as empty strings, not as empty arrays, and not as placeholders. Those fields belong to the actionable-card contract, and a confirmed improvement is not a decision the reviewer is being asked to make. A terse row that seems to need one of them was misclassified upstream; map it as it stands rather than inventing the missing decision data.\n\n### Only full decision cards map to `actionable_items`\n\nAn entry in `actionable_items` must carry the complete contract: `id`, `question`, `why_it_matters`, `recommendation_explanation`, `options` (2\u20134 labels), `option_consequences` (same length as `options`, in the same order), and a `recommendation_index` that is a 0-based integer within range of `options`. `original_question`, `codebase_evidence`, and `source` are optional display fields.\n\nDo not promote a terse Confirmed Improvement row into `actionable_items` by synthesizing branches, consequences, or a recommendation index for it. If the evaluation did not emit a decision tree for an item, that item is not an actionable decision.\n\n### `codebase_evidence` uses the single canonical citation\n\nMap the item\'s `**Assessment**` paragraph together with its one `**Canonical citation**` into `codebase_evidence`. There is exactly one citation per item; do not reconstruct a citation list, do not re-derive citations from the source documents, and do not repeat the canonical citation elsewhere in the payload.\n\n### Ignore the machine-only silent-validation records\n\nThe combined document carries a `**Silent-agreement accounting**` block (`silent_validated`, `silent_accepted`, `silent_demoted`) and a `<!-- silent-validation-ledger ... -->` HTML comment. Both are machine-only records of work already completed. **Ignore them entirely when building both decision-page arrays.** A silently accepted item is not a user decision and never becomes an `actionable_items` entry or a `clear_improvements` entry. A demoted item is already present as its own visible E-item; map that visible item, not the ledger line that references it.\n\n### Item ids\n\nPreserve the E-item id when mapping an actionable card (`E-5` \u2192 `id: "E-5"`). For terse Confirmed Improvement rows, use the E-item id the evaluation assigned; visible E-numbering is sequential and unique across the whole document, so those ids are already stable and distinct.\n\nFor each actionable item, the `options` array is a list of plain label strings extracted from the combined file\'s decision tree branches. The tool auto-generates value keys (`opt-0`, `opt-1`, etc.) and auto-appends a "None of these" option. Do not generate value keys yourself.\n\n## Step 2.5: Auto-approve fast path\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\nIf `auto_approve` is `true` and Step 2 produced at least one actionable item, skip Steps 3\u20136 entirely and synthesize the commit JSON directly:\n\n- `ticket_key`: `{ticket_key}`\n- `general_comment`: `""`\n- `decisions`: an object keyed by each `actionable_items[*].id` from Step 2\'s mapped input. For each item:\n - If `recommendation_index` is a non-negative integer within range of `options`: `choice = "opt-" + recommendation_index`, `chosen_label = options[recommendation_index]`, `comment = ""`, `source` copied from the item.\n - Otherwise (missing, null, or out of range): `choice = "opt-0"`, `chosen_label = options[0]`, `comment = ""`, `source` copied. Never emit `"none"` and never emit `"ask"`.\n\nPost a single chat acknowledgement listing each auto-approved item ID and chosen label, then proceed directly to Step 7 with the synthesized JSON. Step 7\'s "Hard rule" about resolving `ask` items does not apply because no item carries `choice === "ask"`.\n\nThe synthesized settled decisions still receive the implications review described under Step 6\'s "Implications review and proceed gate" before Step 7 runs \u2014 literal `auto_approve = true` only skips the human proceed gate, not the review itself. "Skip Steps 3\u20136" above means skipping their interactive portions (rendering the page, waiting on chat, the Q&A loop); it does not exempt this fast path from the review obligation.\n\nIf Step 2 produced zero actionable items, fall through to Step 3 \u2014 Step 4\'s existing `no_decisions_needed` branch handles the empty case correctly.\n\nOtherwise (any value of `auto_approve` other than the literal `true` \u2014 including empty, `false`, or missing), proceed to Step 3.\n\n## Step 3: Call the MCP tool\n\nCall `generate_decision_page` with `ticket_key` at the root and the review arrays nested under `content`:\n\n**Always pass `content`, even when both arrays are empty.** Send `"content": { "actionable_items": [], "clear_improvements": [] }` rather than omitting the key \u2014 that is what reaches the `no_decisions_needed` branch Step 2 relies on. Omitting `content` entirely is rejected with a `VALIDATION_ERROR`, because root-level arrays are silently dropped by the tool\'s lean input schema and a missing wrapper is far more often a mistake than a deliberate empty call.\n\n```typescript\ninterface ReviewDecisionsContent {\n actionable_items?: Array<{\n id: string;\n question: string;\n why_it_matters: string; // required \u2014 concrete one-sentence impact\n recommendation_explanation: string; // required \u2014 why the recommended branch is best\n options: string[]; // 2-4 option labels\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based index into options\n original_question?: string; // optional display field\n codebase_evidence?: string; // optional display field \u2014 assessment + the one canonical citation\n source?: string; // optional source reference\n }>;\n clear_improvements?: Array<{\n id: string;\n title: string;\n action: string;\n confidence: string;\n source: string; // required for clear_improvements\n }>; // exactly these five fields \u2014 nothing else\n}\n```\n\nExample call:\n```json\n{\n "ticket_key": "{ticket_key}",\n "content": {\n "actionable_items": [\n {\n "id": "E-1",\n "question": "Should we add a configurable timeout?",\n "why_it_matters": "Timeout behavior affects retry paths and user-visible latency.",\n "recommendation_explanation": "Configurable matches existing latency-branching code.",\n "options": ["Keep existing", "Add configurable timeout"],\n "option_consequences": ["No new work.", "Implementers add config + tests."],\n "recommendation_index": 1,\n "original_question": "Does the ticket specify timeout behavior?",\n "codebase_evidence": "The retry path already branches on latency. src/python/llms/ai_client.py:212",\n "source": "Clarifying Q1"\n }\n ],\n "clear_improvements": [\n { "id": "E-2", "title": "Tidy logging", "action": "Use the module logger.", "confidence": "High", "source": "Clarifying Q4 (prior round, concurrence inline)" }\n ]\n }\n}\n```\n\n## Step 4: Check tool response\n\nThe tool returns a JSON response with a `status` field:\n- If `status` is `"no_decisions_needed"`: skip Steps 5, 6, 7, and 8 entirely. Output a success message: "No actionable review decisions needed \u2014 skipping doc rewrite and upload." This covers both the case where every item was confirmed as a Confirmed Improvement and the case where no items were emitted (e.g., `saved_legs` was empty in the preceding wait result).\n- If `status` is `"decision_page_generated"`: continue to Step 5. The response includes `file_path`.\n\n## Step 5: Direct user to the decision page\n\nTell the user to open the generated HTML file in their browser. Provide the `file_path` from the tool response. Then say to the user, verbatim: `Open the page. For any item you\'re unsure about, choose "Ask about this" \u2014 when you submit, I\'ll talk through those before we proceed. You can also ask me questions in chat before submitting if you prefer.`\n\nThis step only directs the user to the page and explains the two allowed next actions (submit selections, or ask questions first). Do not describe Step 7\'s rewrite semantics here; that belongs to the rewrite step.\n\n## Step 6: Q&A loop and commit signal\n\nEnter an open-ended Q&A loop. There is no turn cap \u2014 the user may ask any number of questions in any number of turns. Do not stop and wait silently; engage with each user message as either a commit signal or a discussion turn.\n\n### Proceed signal (commit)\n\nTrim the full user message and attempt to parse the entire trimmed message as JSON. The message is a commit only when the parsed value is an object with all three of these top-level fields:\n\n- `ticket_key` \u2014 must be a string\n- `decisions` \u2014 must be an object\n- `general_comment` \u2014 must be a string\n\nThe first valid commit-shaped JSON paste commits immediately. Proceed to Step 7 without prompting for additional confirmation. Any combination of `decisions` keys is accepted (the page may submit a partial set if the user only resolved some items conversationally). Do not over-validate the per-card fields beyond the top-level commit-shape check \u2014 the page guarantees the per-card schema, and over-validating risks rejecting valid pastes if the page schema evolves.\n\n### Discussion signal (Q&A turn)\n\nAnything that is not commit-shaped JSON is a discussion turn. This includes:\n\n- Freeform questions (with or without other text).\n- Questions pasted alongside other text or alongside JSON.\n- Malformed JSON (parse failure).\n- Well-formed JSON missing one or more of the required top-level keys (`ticket_key`, `decisions`, `general_comment`).\n\nFor JSON-shaped input that is missing required top-level fields, call this out in the reply \u2014 explain which fields are missing and ask whether the user intended to submit or share partial state \u2014 rather than silently treating it as a freeform question.\n\nAnswer discussion turns using these sources, in priority order:\n\n1. The combined `{ticket_key}-review-and-resolution.md` file already read in Step 1.\n2. The original source document(s) for whichever leg(s) are in the preceding wait\'s `saved_legs` (BAPI-1121) \u2014 `{ticket_key}-clarifying-questions.md` and/or `{ticket_key}-ticket-quality-critique.md`. Never read a leg in `withheld_legs` or `absent_legs`, even if a file happens to exist at its canonical path.\n3. Codebase lookups when the question requires verifying current code state.\n\nIf a discussion turn asks about an item the reviewer cannot find in the visible sections, the silent-validation ledger is a legitimate place to *look it up* \u2014 it records which prior items were silently accepted and what grounded each one. Answer from it in chat when asked. It still never becomes a decision-page entry or a rewrite target.\n\nFallback: if running on a pre-PR1 branch where the combined review-and-resolution document does not exist, use the pre-PR1 `{ticket_key}-review-evaluation.md` and `{ticket_key}-resolution-guide.md` pair in its place.\n\nFor plain freeform questions, infer the item from chat context when possible.\n\n### In-flight decision state\n\nDuring the Q&A loop, maintain in-flight JSON state \u2014 agent-owned working memory representing the user\'s current intent for `decisions` and `general_comment`. This in-flight JSON state lives only in the agent\'s working memory for the duration of the loop; do not persist it server-side.\n\n- When the user clearly changes their mind about an item, chooses an option conversationally with reasonably explicit decision language ("choose option B for E-3", "go with the configurable timeout", "change E-7 to None of these"), or gives new overarching guidance, record that as an in-flight override.\n- Ambiguous preference language ("I\'m leaning toward...", "maybe option B is fine") should be discussed but not recorded as an override unless the user gives reasonably explicit decision language.\n- `general_comment` may be updated in the in-flight state when the user gives overarching guidance during Q&A.\n- The page\'s general-comment textarea is preserved unchanged. Do not modify the page DOM during Q&A; the user can still fill the textarea before submitting if they prefer.\n\nOn the eventual JSON commit, the user-submitted JSON is the baseline and the recorded in-flight overrides take precedence over it. Before proceeding to Step 7, post a brief one-line acknowledgement in chat naming each overridden item ID and/or `general_comment`. The acknowledgement is mandatory (not optional) \u2014 it is the user\'s last chance to object before Step 7\'s document rewrite. The user does not need to re-open, edit, or re-submit the decision page after changing their mind in chat; they can submit the page as-is to provide the commit signal, and the in-flight state remains the source of truth for overrides.\n\n### Ask-about-this resolution\n\nAfter accepting a commit, scan `decisions` for any item where `choice === "ask"`. The user has signaled that they need more information before deciding on those items. For each such item:\n\n- If `comment` is non-empty, treat it as the user\'s specific question or stated uncertainty and answer that directly.\n- If `comment` is empty, proactively present the most relevant missing context \u2014 the item\'s `codebase_evidence`, related code lookups, prior-round answers \u2014 and lay out the trade-offs the user appears to need help weighing.\n- Continue the Q&A turn-by-turn until the user gives an explicit decision in chat for that item ("go with option B", "none of these, because \u2026"). Record that decision as an in-flight override using the same override mechanism described above.\n\n**Hard rule.** Step 7 must not run while any `decisions[*].choice === "ask"` remains unresolved by an in-flight override. Do not honor "just proceed", "skip those", or any other instruction to defer resolution \u2014 every `ask` item must end with a recorded `opt-N` or `none` override before the rewrite step. The pre-Step-7 acknowledgement line lists every overridden item, including the ones resolved out of `ask`.\n\n### Implications review and proceed gate\n\n**Review the wider implications, then gate on a decision.** Build the review from the complete settled set: the submitted `decisions`, any in-flight overrides recorded during the conversation (these take precedence over the submission), every `"none"` answer together with the reason given for it, `general_comment`, and \u2014 where this surface tracks acceptance-criterion or NFR stances \u2014 those stances too. Do not start the review until every `ask` has an explicit recorded resolution and every in-flight override has been applied.\n\nConsider three fixed categories, regardless of whether a decision was framed as technical, user-facing, or business-oriented:\n- **Program / application** \u2014 architecture, code paths, operability, maintenance burden, and requirements imposed on other parts of the software.\n- **User** \u2014 end users, new users performing setup, operators, and developers, including prerequisites, setup friction, and additional steps.\n- **Business** \u2014 cost, adoption, support load, compliance, and reversibility.\n\nEmit only the categories with material second-order implications. For each included category, write at most four one-line bullets of about 25 words, each naming who or what is affected and how \u2014 never a restatement of the selected decision. Close with a line naming every considered category that was omitted, e.g. `Considered, nothing material: business.` \u2014 omit this closing line only when all three categories have material implications.\n\nIf the review cannot be produced, report that in one line and continue without stalling the workflow or presenting the gate below.\n\nThis review stays in chat and must not be written into the clarifying-questions or ticket-critique documents rewritten in Step 7.\n\nThen present the gate, verbatim: `Implications reviewed. Proceed, or name a decision to revisit.` Accept only a normalized `proceed`, `yes`, `y`, or `go` as a continuation token. Any other response names a decision to reopen: re-settle it in chat, record the new override, rerun the entire implications review against the changed settled set, and present the gate again.\n\nLiteral `auto_approve = true` emits the review but skips this gate entirely; a missing or non-true `auto_approve` value follows the human-in-the-loop path above.\n\nA decision named at this gate is re-settled in chat and recorded as an in-flight override using the same override mechanism as the rest of this step; the rerun review picks it up, and the pre-Step-7 acknowledgement line above also names it. Step 7 must not begin until every submitted `ask` is resolved (per the hard rule above) **and** this gate has accepted a proceed token \u2014 except when the review fails open or `auto_approve` is literal `true`.\n\n## Step 7: Interpretively rewrite source documents\n\nThe pasted JSON contains a `decisions` object keyed by item ID. Each decision includes `source`, `choice`, `chosen_label`, and `comment`. **Manifest authority (BAPI-1121)**: read the source document for a leg ONLY when that leg is in the preceding `wait_for_ticket_review` result\'s `saved_legs` \u2014 a canonical file existing on disk is a content LOCATION, never independent evidence that this review delivered it, and a leg in `withheld_legs` or `absent_legs` must not be read even if a file is present at its path. Define **generation-proven delivery** as the intersection of `saved_legs` and `advanced_legs` when `freshness_basis` is `"generation"` \u2014 the normal case; define **terminality-only fallback delivery** as `saved_legs` alone when `freshness_basis` is `"terminality_only"`, and treat that case as freshness-unproven even though it was saved (the identity-footer agreement check below is what backstops it). Canonical files are content locations and never independently establish delivery or current-review provenance on their own \u2014 the manifest is what does:\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` \u2014 when `clarify` is in `saved_legs`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` \u2014 when `critique` is in `saved_legs`\n\n**Write the rewrite to a DIFFERENT file; never overwrite the ones above** (BAPI-1097):\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions-reviewed.md`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique-reviewed.md`\n\nThose two source files are the **canonical** artifacts: their bytes are what the\ngeneration identity attests, and they are what the carry-through monitor and the\nhash verification read. The `-reviewed.md` files are the **human-facing** copies \u2014\nthe same findings, cleaned and read as if the decisions had always been made. Both\nsurvive; a consumer chooses between them deliberately rather than by reading\nwhichever one it finds.\n\nRewrite only the leg(s) in `saved_legs` and skip any leg not in it. A cost-saved review resolves zero critique rounds for an Easy ticket and ships clarifying questions alone, so critique appears in `absent_legs` rather than `saved_legs` \u2014 that is not an error. A leg in `withheld_legs` (delivered but not proven current \u2014 see the wait result\'s `freshness_basis` and per-leg `reason`) is likewise never rewritten; its canonical file was already removed by the wait, so there is nothing to read even if you tried.\n\n**Only submitted actionable decisions produce rewrites.** A silently accepted item generates no rewrite operation. Its presence in the machine-only ledger records that it was validated, not that a reviewer decided anything about it, and rewriting a document because of a ledger line would put a change into the source docs that no human ever approved.\n\nAfter a second-opinion run, each document has this shape:\n\n- A top-level H1 (`# Ticket Analysis` or `# Ticket Quality Critique`) followed by an italic provider-attribution line `_This analysis was generated by GPT|Claude|Gemini._` naming the first-round LLM family. **Preserve this attribution line verbatim** \u2014 do not move, edit, or remove it during the rewrite step.\n- The first-round questions / critique items, exactly as written by the first-round model.\n- **Inline second-opinion blockquotes** (`> **Second opinion (<provider>) - concurrence|refinement|disagreement.** ...`) nested directly under each prior item the second round addressed. The `(<provider>)` parenthetical is the second-round LLM family (`GPT|Claude|Gemini`). A `concurrence` blockquote may legitimately carry no body and no citations \u2014 that is a valid pure concurrence, not a malformed blockquote. Items the second round did not comment on have no blockquote at all \u2014 that is the silent-agreement signal.\n- A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 there is a second italic attribution line `_These additional points were raised by GPT|Claude|Gemini._` naming the second-round family \u2014 **also preserve this verbatim**. Then agent-specific sub-headings:\n - Clarifier docs: `### New Requirements Questions` / `### New Technical Questions` (numbering continues from the prior section).\n - Critique docs: `### New Requested Changes` / `### New Points to Consider` (numbering continues from the prior section).\n- A final **`## Second Opinion Summary`** footer. **This footer must be preserved verbatim** \u2014 under Phase 1 it is generated by the server from execution metadata and is the canonical record that the second round ran. Do not edit it, do not regenerate it, and do not remove it, even when individual items it accounts for have been resolved.\n\nThe `source` field on each decision tells you where the item lives:\n\n- `Clarifying Q3 (prior round, silent agreement \u2014 demoted)` \u2192 the prior section, no inline blockquote. Rewrite the prior item\'s answer.\n- `Clarifying Q9 (prior round, concurrence inline)` \u2192 the prior section, prior item carries an explicit `concurrence` blockquote (possibly an empty one). Rewrite the prior answer; the blockquote can be removed once the answer absorbs the resolution.\n- `Clarifying Q3 (prior round, refinement inline)` / `(prior round, disagreement inline)` \u2192 the prior section, prior item carries an explicit `refinement` or `disagreement` blockquote. Rewrite the prior answer to reconcile the dispute, then handle the blockquote per the rule below.\n- `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` \u2192 the `## New in Second Opinion > ### New Requirements Questions` sub-section. Rewrite the item in place inside that sub-section, not at the top of the prior analysis.\n- Equivalent forms for critique items: `Critique: Requested Change 2 (prior round, refinement inline)`, `Critique: Points to Consider N+1 (new in second opinion \u2192 New Points to Consider)`, etc.\n\n**Legacy fallback shape**: if the document instead ends with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section (because the JSON pipeline fell back), apply decisions to the equivalent location: `### Response to Prior Items` for inline-style responses, `### Additional Points > New X` for tail-style new items. Preserve the `\\n\\n---\\n\\n` separator and the `## Second Opinion` heading verbatim.\n\nApply the decision to the item in its home location. Then apply the decision:\n\n### Actionable item decisions\n\n- **Selected option** (`choice` is `opt-N`): Add `**Review Decision**: Accepted. <chosen_label>.` to the corresponding section. Integrate the selected direction into the section text so it reads as a final recommendation or resolved answer.\n- **None of these** (`choice` is `none`): Add `**Review Decision**: Rejected \u2014 none of the proposed options accepted.` Include the user\'s `comment` explaining why. Rewrite the section to reflect this decision.\n\nFor actionable items sourced from clarifying questions, rewrite the question\'s best-guess answer so it reads as the final resolved direction chosen by the reviewer. Do not leave the item framed as an unresolved accept/reject/modify prompt.\n\nFor items sourced from `(prior round, refinement inline)` or `(prior round, disagreement inline)` \u2014 disputes of a prior-round item carried in an inline blockquote \u2014 the prior-round item is the canonical home: rewrite its answer to absorb the resolution. Then handle the blockquote in one of two ways: (a) remove the blockquote outright if the rewritten answer fully absorbs the second-opinion content, or (b) shorten the blockquote to a single sentence noting the resolution while preserving the `(<provider>)` attribution (e.g. `> **Second opinion (Claude) - refinement.** Resolved by reviewer decision E-N.`). The strongest grounding reference from the original blockquote may be promoted into the rewritten prior-item answer if useful \u2014 keep one.\n\nFor items sourced from `(new in second opinion \u2192 ...)` \u2014 gap-captured items that received a decision \u2014 rewrite the item in place inside its tail-block sub-section (`## New in Second Opinion > ### New X`), not at the top of the prior analysis. Preserve the sub-section heading and continued numbering.\n\n### General comment handling\n\nTreat `general_comment` as overarching guidance that informs the tone and direction of both document rewrites. If it contains specific actionable feedback, weave it into the relevant sections. If it is broad or general, use it as context for how the rewrites should read. Do not create a separate "General Comment" or "Reviewer Notes" section \u2014 the goal is "final draft" form.\n\n### Rewrite principles\n\nThe goal is a **final draft** \u2014 the documents should read as if they were written with the decisions already made. Do not mechanically append decisions. Instead, lightly rewrite affected sections so they reflect the decisions naturally. Preserve all non-affected sections unchanged. The prior-round content should still read as coherent standalone analysis after integration. Preserve the `## New in Second Opinion` tail block intact for any items that weren\'t decided. **Always preserve the `## Second Opinion Summary` footer verbatim** \u2014 it is the server-generated record of the second round\'s execution and should not be edited even when individual items are resolved. **Do NOT carry the `bridge-review-decision-identity` footer into the rewritten copy.** That footer attests a SHA-256 taken over the delivered bytes, so leaving it on a rewritten document produces an attestation of bytes that no longer exist (BAPI-1097). Read the identity out of the untouched canonical document \u2014 Step 7.5 still needs it \u2014 and write the rewrite to its own file with no footer at all. The upload route strips a footer that arrives anyway and refuses a document carrying more than one. **Preserve every `<!-- bapi-item:... -->` marker exactly**: the route requires the rewritten leg\'s marker set to equal that leg\'s manifest ids, and refuses the upload otherwise.\n\n## Step 7.5: Record the decisions durably (AUTHORITATIVE)\n\n**This step, not the document rewrite, is what makes the decisions durable.** The\n`**Review Decision**:` markers Step 7 stamps into the two documents are a\nhuman-readable record only. Those documents are stored with `url=\'ai-generated\'`,\nand BAPI-738 deliberately excludes such rows from the description plan generation\nreads \u2014 so a decision that exists only as prose reaches nothing downstream. The\nstructured write below is the authoritative one; the prose is secondary.\n\n### Build one resolution per manifest id\n\nEvery critique finding and every clarifying question carries a stable id as an\nHTML comment on its item line: `<!-- bapi-item:crit-001 -->`,\n`<!-- bapi-item:clar-004 -->`. Preserve these markers through the Step 7 rewrite \u2014\nnever delete, renumber, or invent one.\n\nProduce **exactly one** resolution for **every** id present in the saved-leg\ndocuments, not just the ones the reviewer chose an option for. A cost-saved review\nsaves only the legs it ran \u2014 an Easy ticket resolves zero critique rounds and\nships clarifying questions alone \u2014 so build resolutions for the ids in whichever\ndocuments are in `saved_legs`. An id you omit is indistinguishable from\na decision that was lost, which is the exact failure this record exists to prevent.\nThe server refuses a payload with a missing, duplicate, or unknown id.\n\nMap each settled decision onto the id of the item it resolves:\n\n| Settled decision | `adjudication` | `effect` | `binding_instruction` |\n| --- | --- | --- | --- |\n| An option was chosen (`choice` is `opt-N`) and it obliges a change | `accepted` | `apply` | the ratified direction, non-empty |\n| Resolved, but nothing is to be done about it | `accepted` | `dismiss` | `null` |\n| None of these (`choice` is `none`) | `rejected` | `dismiss` | `null` |\n\nAn accepted dismissal is exactly `{adjudication: "accepted", effect: "dismiss",\nbinding_instruction: null}`. It means "this item is settled and requires no\nwork" \u2014 which is a different fact from "this item was declined", and the two must\nnot be collapsed onto one another.\n\n**Binding corrections go in `binding_instruction`, never in prose alone.** When\nthe reviewer\'s answer obliges an implementation to do something specific, that\nobligation belongs in this field on that item\'s id, even when the conversation\nproduced no `**Review Decision**:` marker for it. An unmarked prose correction is\nnot an independent requirement source and will not survive.\n\n### Read the generation identity from the saved-leg documents\n\nEvery saved review document carries one metadata footer naming the decision\ngeneration it belongs to:\n\n```\n<!-- bridge-review-decision-identity: {"generation":7,"source_bundle_sha256":"<64 lowercase hex>"} -->\n```\n\nRead it from whichever of these documents is in the preceding wait result\'s\n`saved_legs` (BAPI-1121) \u2014 **never** from a leg in `withheld_legs` or `absent_legs`,\neven if a file happens to exist at its canonical path:\n\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` \u2014 when `critique` is in `saved_legs`\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` \u2014 when `clarify` is in `saved_legs`\n\nA cost-saved review legitimately saves only one leg \u2014 an Easy ticket resolves zero\ncritique rounds and ships clarifying questions alone, so `critique` is in\n`absent_legs` rather than `saved_legs`. Read the footer from each document in\n`saved_legs`, and do not treat a leg absent from `saved_legs` as a failure.\n\nEvery saved-leg document\'s footer must be present and must agree \u2014 the same\npositive `generation` and the same 64-character lowercase-hex\n`source_bundle_sha256`. If a footer is missing or malformed on a document IN\n`saved_legs`, or two saved-leg documents disagree, **stop and report it**. Do not\nguess a generation, do not send one document\'s identity on behalf of a document\nthat carries none, and do not ask the server which generation is current. The\nvalue\'s entire worth is that it came from the documents you actually adjudicated\nrather than from whatever happens to be pending by the time you call \u2014 a review\nre-run between your read and this call is exactly what it exists to catch.\n\nThe footer-agreement check above is the downstream integrity backstop this ticket\nrelies on, and it matters MOST under `freshness_basis: "terminality_only"` (BAPI-1121):\nwhen no submission baseline was available to prove freshness, this is the one\nremaining check standing between an adjudication and a document that is not\nactually this review\'s output. Treat a missing/malformed/disagreeing footer with\nextra weight in that mode rather than assuming the wait\'s terminal-delivery\nacceptance already vouched for it.\n\nRead every saved-leg document\'s footer BEFORE the Step 7 rewrite, and read it from the canonical document, which Step 7 leaves untouched on disk. The rewritten copy deliberately carries no footer (BAPI-1097), so a footer read from it would either be absent or attest bytes that changed.\n\n**Degraded-run remediation.** If `freshness_basis` is `"terminality_only"` and freshness genuinely must be proven before adjudicating (rather than accepted on the terminality-only/footer-agreement basis above), submit the review again with `request_ticket_review` and wait again with `wait_for_ticket_review` within the SAME MCP session \u2014 a live submission establishes the baseline this backstop is standing in for.\n\n### Call the tool\n\nCall `capture_review_decisions` with:\n\n- `ticket_number`: `{ticket_key}`\n- `generation`: **required** \u2014 the integer read from the identity footer above.\n- `source_bundle_sha256`: **required** \u2014 the 64-character lowercase-hex digest\n read from that same footer. It is what proves these resolutions were authored\n against the exact bytes the generation was minted from.\n- `schema_version`: optional; omit it unless you actually know it.\n- `resolutions`: the array built above\n- `critique_document` / `clarify_document`: the **rewritten** markdown from\n Step 7, in memory, for each leg in `saved_legs` \u2014 send only those, omitting a\n leg absent from `saved_legs` (an ordinary policy omission) or withheld from it\n (unproven-freshness content that was never rewritten). These are transient \u2014\n the server counts `Review Decision` markers in them as a floor check and then\n discards them.\n- `request_id`: reuse the same value across retries of one logical capture\n\n### Do not continue past a refusal\n\nA refusal names a closed `reason`. Stop and report it; do not proceed to Step 8\nand do not describe the capture as successful.\n\n| `reason` | What it means |\n| --- | --- |\n| `manifest_coverage_mismatch` | An id is missing, duplicated, or unknown. Rebuild the resolution set against the ids actually in the documents. |\n| `marker_count_gap` | The prose records more decisions than the payload carries. Some decision was not captured \u2014 find it. |\n| `decision_set_too_large` | The set exceeds the record\'s bounds; nothing was truncated. Read `terminalized`: `true` means the generation is now `invalid` and closed, `false` means the invalidating write did not commit and the generation is still open \u2014 report it either way, and never assume the generation closed. |\n| `missing_identity` | The call carried no `generation` or no `source_bundle_sha256`. Stop and re-read the identity footer from each saved-leg document; never substitute a value you did not read. |\n| `stale_generation` | The review was re-run, so the documents you adjudicated are no longer the current generation. Wait for the review again to get a fresh `saved_legs` manifest, re-read the latest saved-leg documents, and re-establish the adjudication against them. Do not resubmit the same resolutions under a newly discovered generation number. |\n| `source_bundle_mismatch` | The submitted digest does not identify the current pending bundle. It is never adopted and never overwrites what is stored. Re-read the saved-leg documents and adjudicate the bundle that is actually pending. |\n| `invalid_decision_entry` | An `apply` has an empty instruction, or a `dismiss` has a non-null one. |\n| `conflicting_decision_set` | This generation was already captured with different decisions. Do not overwrite it. |\n| `generation_already_terminal` | The generation is already `complete` or `invalid`. A new review bundle is what produces a new generation; this one cannot be reopened. |\n| `no_pending_generation` | No decision generation exists for this ticket. The review was never delivered, or it was delivered before this record existed. |\n| `unsupported_schema` / `record_unreadable` | A record exists that this deployment cannot interpret. Report it; do not attempt to replace it. |\n\nA `409` or `422` here is a real failure of adjudication, not a formality to route\naround.\n\n## Step 8: Upload to Jira\n\nOnly after Step 7.5 succeeded. This upload and the prose markers it carries are\nthe human-readable record; the durable one is already written. A failure here is\nreported on its own and never invalidates the completed capture.\n\nUpload the **rewritten** copy of each document in `saved_legs`, using\n`attachment` (operation: `"upload"`) \u2014 a cost-saved Easy ticket has only `clarify`\nin `saved_legs`, so upload only that leg:\n\n1. Upload the rewritten clarifying questions **only when `clarify` is in `saved_legs`**:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions-reviewed.md`\n - `link_type`: `clarifying-questions.md`\n\n2. Upload the rewritten ticket quality critique **only when `critique` is in `saved_legs`** (skip this when critique is in `withheld_legs` or `absent_legs`):\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique-reviewed.md`\n - `link_type`: `ticket-quality-critique.md`\n\nThe `link_type` still names the CANONICAL leg. That is how the route identifies which\nreview leg the rewrite belongs to; it does not mean the canonical row is replaced. The\nroute stores the rewrite under its own human-facing type and its own Jira filename\n(`{ticket_key}-clarifying-questions-reviewed.md` /\n`{ticket_key}-ticket-quality-critique-reviewed.md`), and never touches the stamped\ncanonical row or its attachment.\n\n**Retry rules.** The route writes the rewrite to the database as pending, uploads to\nJira, then promotes. Only after promotion does it report success.\n- `503` \u2014 recorded as pending, but Jira or the promotion did not complete. **Retry**;\n no generation is minted either way. Byte-identical content resumes the same pending\n record, and different content supersedes it \u2014 a re-run of this workflow never\n reproduces the previous bytes exactly, so a differing retry is expected, not an error.\n- `409` \u2014 either the stored canonical bundle failed its own integrity check, or the\n rewritten markers do not match the manifest. Do not retry blindly; report it. On a\n ticket reviewed before BAPI-1097, a canonical-bundle `409` can mean the canonical row\n was already overwritten by the old workflow and no longer hashes to its footer.\n- `400` \u2014 the rewrite carries duplicate/malformed `bapi-item` markers, or more than one\n identity footer. Fix the document, then retry.\n\nReport the upload as successful only once the route returns success, which is the point\nat which both the Jira human-facing attachment and the current human-facing database row\ncarry the accepted bytes.\n\n## Step 9: Complete\n\nConfirm: "Review decisions captured and uploaded to {ticket_key}." State the\ngeneration that was recorded. Say this only when Step 7.5 returned a success\noutcome \u2014 an upload without a durable capture is not a completed adjudication.\n\n## Return\n\nConfirm "Review decisions captured and uploaded to {ticket_key}." and list the human-facing attachment(s) uploaded (`{ticket_key}-clarifying-questions-reviewed.md`, and `{ticket_key}-ticket-quality-critique-reviewed.md` when `critique` is in `saved_legs`). Say this only after each upload returned success \u2014 that is the point at which both the Jira attachment and the current human-facing database row carry the accepted bytes. Note any decisions that could not be applied, and any leg left pending after a `503`.\n\n### Summarize how the review improved {ticket_key}\n\nAfter the capture is confirmed, close with a short chat-only digest titled **How this review improved {ticket_key}** \u2014 three to six brief bullets, each naming one concrete way the review changed the ticket. Build it only from what was durably settled in Step 7.5, not from a fresh reading of the ticket:\n\n- One bullet per `accepted` + `apply` resolution, phrased as the ticket change it obliges. Compress its `binding_instruction` into the improvement \u2014 e.g. "Added a key-rotation prerequisite: the restricted key cannot read Subscriptions" or "Corrected the period-end read to `SubscriptionItem`."\n- One bullet for a Confirmed Improvement that closed a real gap the ticket was missing (an absent migration, an unhandled state, an assumption the codebase disproved), framed as the gap now closed.\n- Do not list `dismiss` or `rejected` items, or an item that only reworded existing text \u2014 those did not improve the ticket.\n\nLead each bullet with the improvement itself \u2014 a new requirement, a corrected assumption, a closed gap \u2014 not with where in the codebase it was found; the grounding is the evidence, not the point. Keep each bullet to one line. When every item settled as a dismissal or the review produced no actionable decisions, write one line instead of an empty list: "No ticket changes \u2014 the review confirmed the existing draft." This digest is chat-only; never write it into the clarifying-questions or ticket-critique documents.\n',"phase1/evaluate-and-recommend.md":'Evaluate the clarifying questions and ticket critiques generated for {ticket_key} against the actual codebase, then decorate every genuinely actionable item with the resolution guidance the reviewer will need on the decision page. The result is a single combined review-and-resolution document.\n\n<!-- BAPI-977 Phase-1 variant of evaluate-and-recommend.md. Selected only when\n REVIEW_SYNTHESIS_PHASE1_ENABLED is on. The legacy instruction at\n instructions/evaluate-and-recommend.md is the disabled path and is unchanged. -->\n\n## Phase 0 \u2014 Grounding & Audit Setup\n\nBefore gathering any source documents, extract the codebase-grounding context produced by the preceding `fresh_base` (`action: "materialize"`) pipeline step:\n\n- Read the `fresh_base` materialize tool result from earlier in this session. It returns JSON `{ base_sha, base_branch, fresh_base_root }` \u2014 or, when `no_refresh_base` was set, `{ base_sha: "local-stale", fresh_base_root: <original repo root> }`.\n- Retain `fresh_base_root` and `base_sha` for the rest of this procedure. Every codebase read in Phase 1 / Phase 2 below is grounded against `fresh_base_root`. `fresh_base_root` is also the exact value you must pass to the pipeline\'s later `fresh_base` (`action: "cleanup"`) step \u2014 it is a *runtime* value returned by the tool call, not a static recipe variable, so pass the real path string you captured here, not any placeholder text shown in the step\'s params.\n- If the materialize step\'s result contains an `error` field and `no_refresh_base` was NOT set, this is the fail-loud condition the recipe\'s `on_error: "halt"` exists for: stop here, do not fall back to grounding against your own working directory, and report the failure (name the attempted base branch and the remediation \u2014 retry, or rerun with `--no-refresh-base`).\n\n**Metadata Audit Header** \u2014 the very first content of the generated review-and-resolution document, before any other section, must be:\n\n```\n**Base SHA**: <base_sha>\n**Base Branch**: <base_branch, or "(local, in-place)" when base_sha is "local-stale">\n**Grounding Status**: <Freshly Materialized | Stale/In-Place Fallback>\n```\n\n- `Grounding Status` is **Freshly Materialized** whenever `base_sha` is a real commit SHA (the normal path).\n- `Grounding Status` is **Stale/In-Place Fallback** whenever `base_sha` is exactly `local-stale` (the `--no-refresh-base` opt-out path). In this case, immediately follow the header with a prominent, bold, high-contrast warning block, for example:\n\n > **\u26A0 STALE GROUNDING \u2014 `--no-refresh-base` was used.** This review evaluated the codebase as checked out locally, NOT a freshly-fetched `origin/<base>`. `file:line` citations may reflect uncommitted or unmerged local state.\n\n**Codebase grounding rule**: Ground ALL file reads and codebase searches exclusively against the `fresh_base_root` directory extracted above. Do NOT read codebase files from your default working directory or session cwd \u2014 `fresh_base_root` is the only trustworthy source of truth for `file:line` citations in this procedure.\n\n**Original-repo rule**: Ticket docs, `{docs_dir}` inputs, and ALL output paths stay in the ORIGINAL repository, never the `fresh_base_root` temp dir. This includes the ticket-fetch call below, the clarifying-questions / critique source documents, and the saved review-and-resolution output file (see the Save rule at the bottom). Do NOT redirect any of these into `fresh_base_root`.\n\n**Path hygiene rule**: Every `file:line` citation and every `Canonical citation` in the output document MUST be repo-relative \u2014 strip the `fresh_base_root` absolute-path prefix before writing it down. A citation must never contain a temp-dir / `/tmp/...`-style absolute path (write `src/foo.ts:10`, never `/tmp/bridge-review-.../src/foo.ts:10`).\n\n1. Fetch the current ticket description using the `get_ticket` tool with ticket_number `{ticket_key}` exactly once at the top of this procedure.\n\n2. **Manifest authority (BAPI-1121).** The preceding `wait_for_ticket_review` step\'s ready result carries the authoritative `saved_legs` (plus `advanced_legs`, `withheld_legs`, `absent_legs`, and `freshness_basis`) \u2014 that wait persisted exactly those documents and already removed the canonical file for any withheld leg. Determine which source documents to gather from `saved_legs`, NOT from checking which files exist on disk: a leg\'s canonical file existing is a content LOCATION, never independent evidence that this review delivered it.\n\n - Read the source document only for a leg present in `saved_legs`, from its canonical path: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` for `clarify`, `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` for `critique`.\n - A leg in `withheld_legs` or `absent_legs` must NOT be read or evaluated, even if a file happens to exist at its canonical path \u2014 a withheld leg\'s file was deliberately removed by the wait, and an absent leg\'s is either not present or, if one is, is not this review\'s output.\n - `advanced_legs` means strictly generation-proven current-review output. Under `freshness_basis: "terminality_only"` (no submission baseline was available to the wait), `advanced_legs` is intentionally empty even though `saved_legs` holds every terminally-delivered leg \u2014 surface this in the document\'s Round Agreement Summary as a degraded-freshness note (the artifacts may be indistinguishable from a prior attachment; the identity-footer agreement check downstream is the remaining backstop), rather than treating a `terminality_only` result as an ordinary fresh review.\n - **Stop and report** if a leg listed in `saved_legs` is missing or unreadable at its canonical content location \u2014 the wait\'s persisted manifest and the actual filesystem state have diverged, which is not a condition to paper over.\n - **Stop and report** if `saved_legs` is empty \u2014 no current-review source can be vouched for, so producing an evaluation from whatever happens to be on disk would silently source a review nobody delivered.\n\n After a second-opinion run, each document you DO gather has this shape:\n\n - A top-level H1 (`# Ticket Analysis` for clarifier docs, `# Ticket Quality Critique` for critique docs) followed by an italic provider-attribution line of the form `_This analysis was generated by GPT|Claude|Gemini._` The attribution names the LLM family that produced the **first round**.\n - The first-round questions / critique items, exactly as written by the first-round model.\n - **Inline second-opinion blockquotes** nested directly under each prior item the second round addressed. Each blockquote starts with `> **Second opinion (<provider>) - <stance>.**` where `<provider>` is `GPT|Claude|Gemini` and `<stance>` is `concurrence|refinement|disagreement`. The blockquote may be followed by `> *Citations: <comma-separated grounding refs>*`. Items the second round did **not** comment on have no blockquote \u2014 that is the "silent agreement" signal.\n\n **A `concurrence` blockquote may legitimately carry no body text and no citations.** Under Phase 1 the second round is instructed not to pad a pure agreement, so an empty concurrence blockquote is a deliberate, valid signal \u2014 it is an *explicit* concurrence, not a malformed one, and not the same thing as silence. Treat it as such; do not discard it and do not demand a citation from it.\n - A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 you will find a second italic attribution line of the form `_These additional points were raised by GPT|Claude|Gemini._` \u2014 this names the second-round LLM family. Sub-headings are agent-specific:\n - Clarifier docs: `### New Requirements Questions` and `### New Technical Questions` \u2014 numbering continues from the prior section.\n - Critique docs: `### New Requested Changes` and `### New Points to Consider` \u2014 numbering continues from the prior section.\n Each new item has its own `*Citations: ...*` line.\n - A final **`## Second Opinion Summary`** footer capturing the second round\'s execution outcome. Under Phase 1 this footer is generated by the server from execution metadata rather than written by the model, so it is a run record and NOT a source of analytical content. Do not mine it for findings and do not cite it.\n\n **Legacy fallback shape**: in rare cases (model lacks JSON-schema support, the JSON call failed, or the response could not be parsed), the document may instead end with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section containing `### Response to Prior Items` and `### Additional Points` subsections. If you detect this fallback shape, treat it equivalently: subsection responses tagged `concurrence` map to explicit concurrence, `refinement`/`disagreement` map to the disagree buckets, and items under `### Additional Points` map to the gap-captured bucket below.\n\n **Partial-source-doc tolerance (manifest-driven, BAPI-1121)**: a leg absent from `saved_legs` because it is in `absent_legs` (an ordinary policy omission \u2014 e.g. an Easy-ticket clarify-only review) is skipped silently; produce items only for the leg(s) actually in `saved_legs`. Do not fail. If `saved_legs` contains neither leg, still write the combined output file at `{docs_dir}/review/{ticket_key}-review-and-resolution.md` with the standard top-level sections (`Confirmed Improvements`, `Needs Scrutiny`, `Open Questions`, `Round Agreement Summary`) present, no emitted E-items in any section, and the Silent-Agreement Accounting block present with all totals at zero. This preserves downstream file-existence expectations for the capture-review-decisions step. This is different from the stop-and-report cases just above: an *absent* leg is a normal shape to omit, a `saved_legs`-listed leg that cannot be read is not.\n\n3. Determine **Round Agreement** for every clarifying question and critique point using these rules:\n\n - **Silent agreement** \u2014 the prior item has NO inline blockquote AND is not in `## New in Second Opinion`. The second round did not object to the point and did not consider it important enough to comment on. Handle it under **Phase 1S \u2014 Silent-agreement validation and accounting** below; it does not become a visible E-item unless it is demoted.\n - **Explicit concurrence** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - concurrence.** ...` blockquote, with or without body text and citations. The second round took the trouble to mark this item specifically. It stays a **retained visible item**. Establish its canonical citation yourself from the codebase \u2014 do not rely on the blockquote having supplied one.\n - **Rounds disagree (refinement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - refinement.** ...` blockquote. The second round modified or added detail. Apply full disagreement-depth analysis; the blockquote\'s citations are a starting point you must verify.\n - **Rounds disagree (disagreement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - disagreement.** ...` blockquote. The second round contradicts the prior. Apply full disagreement-depth analysis; categorize the outcome based on which position the codebase supports.\n - **Gap captured** \u2014 the item lives under `## New in Second Opinion > ### New <category>` (one of: New Requirements Questions, New Technical Questions, New Requested Changes, New Points to Consider). Apply the two-axis check below. The new item\'s citations are a starting point you must verify.\n - **Single round only** \u2014 the document has none of the above markers (no inline blockquotes, no `## New in Second Opinion` block, no `## Second Opinion Summary` footer). The pipeline ran only one round. Treat every item as a disagreement: cite the strongest codebase location and give full analytical depth.\n\n Apply these depth and categorization rules:\n\n - **Explicit concurrence**: 1-2 sentence assessment. Categorize as Confirmed Improvement unless your own validation contradicts it, in which case it becomes Needs Scrutiny at disagreement depth.\n - **Rounds disagree (refinement or disagreement)**: 3-4 sentence assessment that explicitly weighs the prior-round position against the second-opinion position. Categorize based on which position the evidence supports. Always include both positions in the Assessment.\n - **Gap captured \u2014 two-axis check** (for items in `## New in Second Opinion`):\n - If both the question is grounded in the codebase/standards AND the best-guess answer is sensible \u2192 **Confirmed Improvement** with a 1-2 sentence assessment.\n - If the question is genuine but the best-guess answer is flawed \u2192 **Needs Scrutiny**. Disagreement depth.\n - If the question itself does not hold up \u2192 **Needs Scrutiny** with evidence of what the code actually does. Disagreement depth.\n - If neither codebase nor standards can settle the question \u2192 **Open Questions**. Disagreement depth.\n - **Single round only**: Treat as a disagreement and give full analytical depth.\n\n For critique points (Requested Changes and Points to Consider), apply the same Round Agreement rules. The signal locations are inline `> **Second opinion (<provider>) - ...**` blockquotes nested under items in `### Requested Changes` / `### Points to Consider`, and gap-captured items under `## New in Second Opinion > ### New Requested Changes` / `### New Points to Consider`.\n\n **Source field conventions** \u2014 the `**Source**` string disambiguates where in the source doc the item lives so the downstream `capture-review-decisions` step can route the rewrite correctly. Use these forms:\n\n - **Silent agreement, demoted to visible**: `Clarifying Q3 (prior round, silent agreement \u2014 demoted)` or `Critique: Requested Change 2 (prior round, silent agreement \u2014 demoted)`.\n - **Explicit concurrence**: `Clarifying Q9 (prior round, concurrence inline)` or `Critique: Points to Consider 1 (prior round, concurrence inline)`.\n - **Refinement (inline blockquote)**: `Clarifying Q3 (prior round, refinement inline)`.\n - **Disagreement (inline blockquote)**: `Clarifying Q5 (prior round, disagreement inline)`.\n - **Gap captured (tail-block item)**: `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` or `Critique: Requested Change N+1 (new in second opinion \u2192 New Requested Changes)`. Always spell out the sub-section name after the arrow \u2014 capture-review-decisions uses it to find the rewrite target.\n - **Single round only**: `Clarifying Q3 (single round)`.\n\n## Phase 1S \u2014 Silent-agreement validation and accounting\n\nThis phase covers every prior-round item classified as **silent agreement** above. The second round\'s silence is not itself evidence, so each such item is still checked \u2014 but a check that passes produces a *count and a ledger entry*, not a reviewer-facing paragraph. Re-printing an item that two rounds and a codebase check all agreed on is the padding this phase exists to remove.\n\n**Validate every silent item individually.** Do not batch, sample, or spot-check. For each silent prior item, look at the actual code the item claims something about, in `fresh_base_root`, and decide whether the item\'s claim and its best-guess answer hold up. An item you did not individually validate may not be counted as accepted.\n\nThree counts govern this phase. Define them exactly as follows and emit all three:\n\n- `silent_validated` \u2014 the number of silent prior items you individually validated. This is every silent item, with no exceptions.\n- `silent_accepted` \u2014 the number of those whose claim and answer the codebase supports. These are **not** emitted as visible items.\n- `silent_demoted` \u2014 the number of those the codebase does **not** support. These **are** emitted as visible items.\n\nThe accounting must satisfy this invariant exactly, with no rounding, no "approximately", and no unaccounted remainder:\n\n```\nsilent_validated = silent_accepted + silent_demoted\n```\n\n### Accepted silent items \u2014 counted, not printed\n\nA silently accepted item is **omitted entirely from the reviewer-facing output**. It gets no E-item row, no bullet in any agreement list, and no entry in any visible section. It is recorded only in the machine-only ledger below.\n\n### Demoted silent items \u2014 the safety catch\n\nA silent item whose validation finds the claim or the answer to be wrong is **demoted**: emit it as a full **Needs Scrutiny** E-item at disagreement depth, with `**Source**` in the `(prior round, silent agreement \u2014 demoted)` form, and count it in `silent_demoted`. Silence must never be able to bury an incorrect item \u2014 the whole point of validating each one is that this catch fires.\n\n### The machine-only silent-validation ledger\n\nEmit exactly one ledger, as a single structured HTML comment, immediately after the Silent-Agreement Accounting block. Markdown renderers do not display HTML comments, so this record is available to tooling without adding a word to what the reviewer reads. Use this exact shape:\n\n```\n<!-- silent-validation-ledger\naccepted:\n- anchor: <source anchor, e.g. Clarifying Q4> | citation: <one repo-relative file:line>\n- anchor: <source anchor> | citation: <one repo-relative file:line>\ndemoted:\n- anchor: <source anchor> | item: <emitted E-item id, e.g. E-7>\n-->\n```\n\nRules for the ledger:\n\n- Every silently **accepted** item appears in `accepted:` with its source anchor and **one** canonical repo-relative `file:line` reference \u2014 the single grounding that made you accept it.\n- Every **demoted** item appears in `demoted:` with its source anchor and the id of the visible E-item it became. Its canonical citation lives on that visible item and is **not** repeated here; a demoted item\'s grounding is something the reviewer reads, so duplicating it into the ledger would be the same content twice.\n- Source anchors are unique across the whole ledger. The same anchor must never appear twice, and never in both lists.\n- The ledger is emitted even when both lists are empty (emit the two keys with no entries beneath them).\n- Explicit concurrences never appear here. They are retained visible items, not silent agreements.\n\n## Phase 1 \u2014 Evaluate and classify every visible item\n\nNumber every **visible** item sequentially across all sections (E-1, E-2, E-3, \u2026) with no gaps. Silently accepted items consume no number \u2014 they are not visible items. When the same underlying issue is raised in BOTH the clarifying-questions doc and the critique doc, consolidate it into a SINGLE E-item rather than emitting one per source, and cite both origins in its `**Source**` field (e.g. `Clarifying Q3 + Critique: Requested Change 2`); keep the numbering sequential with no gaps.\n\nClassify every visible item into exactly one of three buckets using the Round Agreement rules, codebase groundedness checks, and the `gap captured` two-axis check before producing any recommendation decoration:\n\n- **Confirmed Improvements**: Grounded suggestions that would genuinely improve the ticket. Includes explicit concurrences and `gap captured` items that passed both axes.\n- **Needs Scrutiny**: Suggestions based on inaccurate codebase assumptions, with evidence of the actual code behavior. Includes `gap captured` items that failed either axis, demoted silent items, and the loser of any rounds-disagree pair.\n- **Open Questions**: Legitimate ambiguities that require human input to resolve.\n\nPhase 1 must complete before Phase 2 begins \u2014 do not start decorating an item with a decision tree, recommendation index, or clarity fields until classification is final.\n\n## Canonical citation \u2014 exactly one grounding reference per retained item\n\nEvery retained visible item carries **one** `Canonical citation` field: a single repo-relative `file:line` reference that is the strongest grounding for the item. This replaces the previous practice of repeating grounding in three places \u2014 the decision-tree branches, the Assessment prose, and a Codebase Evidence list.\n\n- **One field, one reference.** Do not emit a list. If two locations genuinely matter, name the stronger one in `Canonical citation` and describe the second in the Assessment *without* a `file:line` reference.\n- **Do not repeat the canonical citation anywhere else in the item.** Not in the Assessment, not in a branch, not in an evidence block. It appears exactly once.\n- **Decision-tree branches carry no citations.** Do not append `See file:line` to a branch. A branch states a condition and an action; its grounding is the item\'s canonical citation.\n- If the item genuinely has no codebase grounding, write `Canonical citation: none \u2014 <one clause saying why>` and set `Confidence` to `Low`.\n\n## Phase 2 \u2014 Decorate genuinely actionable items\n\nPhase 2 applies **only** to items in the `Needs Scrutiny` and `Open Questions` buckets that represent a **genuine unresolved choice for the reviewer** \u2014 a decision a human still has to make. Confirmed Improvements stay terse (see "Confirmed Improvements output" below), and so does any Needs Scrutiny item whose resolution is already settled by the evidence.\n\n**A full decision card is only for a real choice.** If the evidence settles the matter and there is nothing for the reviewer to decide, the item does not get a card: render it as a terse Confirmed Improvement instead. Do not manufacture a second branch to justify a card.\n\nFor every genuinely actionable item, produce the following template using these stable labels:\n\n```\n### E-<sequential number>: <concise title>\n\n**Source**: <where this item lives in the source doc \u2014 see Source field conventions above>\n\n**Round Agreement**: <one of the values above> \u2014 <1 sentence on what the second round contributed>\n\n**Confidence**: <High|Medium|Low>\n\n**Canonical citation**: `path/to/file.ts:42`\n\n**Resolution path**: <"resolve at your desk" or "needs a conversation">\n\n**Decision tree**:\n- If <condition 1>, then <action 1>.\n- If <condition 2>, then <action 2>.\n\n**Recommendation Index**: <0-based index of the recommended branch in the decision tree above>\n\n**Recommendation**: <which branch the evidence best supports and why, 1-2 sentences>\n\n**Original question**: <the clarifying-question or critique point as it was originally raised, sourced verbatim or near-verbatim from the original clarifying-questions / critique docs. Light rephrasing is allowed; do NOT introduce new technical content. Soft cap ~30 words.>\n\n**Option consequences**:\n- <consequence for branch 1 \u2014 the behavioral consequence of choosing this option, not its rationale. ~25 words.>\n- <consequence for branch 2 \u2014 same shape. ~25 words.>\n\n**Why it matters**: <one concrete sentence on the impact this decision has on the ticket, the users, or the affected code paths. Soft cap ~40 words.>\n\n**Recommendation explanation**: <explain why the recommended branch is the best choice, tied to the evidence and the consequences of each option. Soft cap ~60 words.>\n\n**Assessment**: <what the code shows and what it implies, 3-4 sentences for a disagreement, 1-2 for a concurrence. State the evidence and the implication. Do NOT restate the original question \u2014 it is already in the Original question field. Do NOT restate the branch rationale \u2014 it is already in Recommendation explanation. Do NOT repeat the canonical citation.>\n```\n\n**Decision tree rules**:\n- Each decision tree must have **2\u20134 branches**, each a genuine, non-contrived alternative. Do not exceed 4 and do not produce only 1.\n- **Strict lower bound \u2014 reclassify on single-branch items**: If you can think of only one branch \u2014 that is, the resolution is effectively forced \u2014 reclassify the item as a **Confirmed Improvement** instead of emitting a single-branch decision tree. Do not stretch to a contrived second branch. If a single answer is genuinely the only path, the item belongs in Confirmed Improvements.\n- Each branch must end with a concrete, actionable step (not "investigate further").\n- Branches carry no `file:line` references. The item\'s `Canonical citation` is its grounding.\n- Cap each branch at one sentence.\n- `**Recommendation Index**` must be the 0-based index of the recommended branch in the decision tree above. The first branch is index 0, the second is index 1, etc. It must be within range of the branches actually emitted.\n- **Option consequences** must be a list parallel to the decision-tree branches: one entry per branch, in the same order, same length. Describe the behavioral consequence of choosing that option, not its rationale.\n- **"resolve at your desk"**: The item can be resolved through technical investigation \u2014 reading code, running tests, or checking configuration. No stakeholder input needed.\n- **"needs a conversation"**: The item involves a product decision, scope question, or cross-team dependency that cannot be resolved from the codebase alone.\n\n**Confidence Tags** \u2014 assign confidence based on codebase evidence strength:\n- **High**: The canonical citation directly supports the assessment.\n- **Medium**: The canonical citation is a related pattern or convention, not the exact code in question.\n- **Low**: No direct codebase grounding. Assessment rests on general reasoning or domain knowledge.\n\n### Confirmed Improvements output\n\nRender each Confirmed Improvement as a single terse row. No per-item headings, no decision trees, no clarity-field decoration:\n\n- **E-<number>: <title>** \u2014 Source: <source string>; Round Agreement: <value>; Confidence: <High|Medium|Low>; Canonical citation: `path/to/file.ts:42`. <recommended action, 1 sentence.>\n\nA Confirmed Improvement row never carries `options`, `option_consequences`, a decision tree, a recommendation index, or an empty placeholder for any of them. If an item seems to need one of those fields, it is not a Confirmed Improvement \u2014 reclassify it.\n\n## Silent-Agreement Accounting\n\nEmit this block in every run, including runs where one or both source documents were missing (in which case every total is `0`):\n\n```\n**Silent-agreement accounting**\n- silent_validated: <N>\n- silent_accepted: <N>\n- silent_demoted: <N>\n```\n\nThe three numbers must satisfy `silent_validated = silent_accepted + silent_demoted` exactly. Immediately after this block, emit the machine-only silent-validation ledger described in Phase 1S.\n\n## Round Agreement Summary\n\nAfter all items are processed, produce a summary section that groups items by round agreement status:\n\n### Points of Disagreement\nFor items where the evaluation marked `rounds disagree (refinement)`, `rounds disagree (disagreement)`, or `single round only` \u2014 including `gap captured` items that failed the two-axis check and landed in Needs Scrutiny \u2014 list as bullets with the E-number, the nature of the disagreement, and a 1-sentence explanation of why this disagreement matters for the ticket (e.g. it indicates an architectural ambiguity, a scope question, or a standards gap).\n\nIf no items were marked as disagreements, write: "All reviewed points had round consensus. No disagreement-driven risks identified."\n\n### Points of Agreement\n\n**Strong agreement** \u2014 items where the evaluation marked `explicit concurrence`. The second round took the trouble to mark this item specifically; that is a soft signal it is important enough to underline. List as bullets with the E-number and a half-sentence noting the shared conclusion.\n\n**Weak agreement** \u2014 represented by the accounting totals, not by an item list. Write a single line of the form: `<silent_accepted> prior item(s) were silently accepted after individual validation; <silent_demoted> were demoted to Needs Scrutiny (<E-numbers, or "none">).` Do not list the accepted items individually \u2014 they are recorded in the machine-only ledger, and re-printing them here is exactly the padding this variant removes.\n\nIf a sub-bullet has no items, omit it (rather than writing a "no items" note for each \u2014 keep the section tidy). The weak-agreement accounting line is always emitted, even when every total is zero.\n\n### Gaps Captured by Second Round\nFor items where the evaluation marked `gap captured` (sound second-opinion additions confirmed as Confirmed Improvements): list as bullets with the E-number and a half-sentence noting the gap the second round surfaced. These items did not require a decision \u2014 they are already in Confirmed Improvements \u2014 but are surfaced here so the reviewer sees what the second-round analysis added on top of the first round.\n\nIf no gaps were captured, write: "The second round did not surface any net-new confirmed improvements."\n\n## Edge Cases\n\n- If the evaluation contains zero items in Needs Scrutiny, write: "No items flagged for scrutiny. All reviewed suggestions were either confirmed or remain open questions."\n- If the evaluation contains zero items in Open Questions, write: "No open questions identified. All ambiguities were resolved through codebase analysis."\n- If both Needs Scrutiny and Open Questions are empty, include only the Confirmed Improvements section and add a summary: "All suggestions from the review were confirmed as grounded improvements. No decision trees are needed."\n- If `saved_legs` contains neither leg, still write the combined file with the standard top-level sections present, no emitted E-items, the Silent-Agreement Accounting block with all totals at zero, and the ledger with both lists empty \u2014 rather than failing.\n\n## Example of a Well-Written Terse Row (Explicit Concurrence \u2014 Confirmed Improvement)\n\n- **E-4: Sequential per-type review_repository fan-out** \u2014 Source: Clarifying Technical Q2 (prior round, concurrence inline); Round Agreement: explicit concurrence; Confidence: High; Canonical citation: `src/python/learn_repository/review_repository.py:369`. Keep the per-type fan-out sequential; the internal chunk-level gather already provides the concurrency.\n\n## Example of a Well-Written Actionable Card (Rounds Disagree \u2014 Needs Scrutiny)\n\n### E-5: Authentication middleware placement for new endpoint\n\n**Source**: Clarifying Q2 (prior round, disagreement inline)\n\n**Round Agreement**: rounds disagree (disagreement) \u2014 the prior round recommended adding auth at the router level; the second-opinion blockquote argued the existing middleware stack already covers it.\n\n**Confidence**: High\n\n**Canonical citation**: `api/routes/__init__.py:18`\n\n**Resolution path**: resolve at your desk\n\n**Decision tree**:\n- If routers each opt in to auth via dependencies, then add `Depends(require_api_key)` to the new endpoint.\n- If the global middleware stack already enforces auth on `/api/*`, then drop the explicit dependency.\n\n**Recommendation Index**: 0\n\n**Recommendation**: Routers opt in individually here, so the new endpoint must declare the dependency itself.\n\n**Original question**: Should the new `/api/exports` endpoint declare an explicit auth dependency, or is it covered by the global middleware?\n\n**Option consequences**:\n- Endpoint requires a valid API key, matching every other `/api/*` route.\n- Endpoint becomes publicly reachable; protected data leaks via the new path.\n\n**Why it matters**: Authentication on `/api/exports` decides whether protected data leaks; the wrong default is a security regression, not a stylistic choice.\n\n**Recommendation explanation**: Each router declares its own dependency and there is no catch-all auth middleware, so following the established pattern adds two lines and keeps auth uniform across endpoints.\n\n**Assessment**: The global middleware applies rate limiting and CORS but not authentication, and every existing router opts in explicitly. That contradicts the second opinion\'s claim of blanket coverage and supports the prior round\'s position. Omitting the dependency would leave the endpoint unauthenticated.\n\n## Example of the Accounting Block and Ledger\n\n```\n**Silent-agreement accounting**\n- silent_validated: 5\n- silent_accepted: 4\n- silent_demoted: 1\n```\n\n<!-- silent-validation-ledger\naccepted:\n- anchor: Clarifying Q4 | citation: src/python/learn_repository/__init__.py:14\n- anchor: Clarifying Q6 | citation: api/library/db/postgres_helpers.py:88\n- anchor: Critique: Requested Change 1 | citation: db/alembic/versions/0165_add_review_claims_and_result_cache.py:12\n- anchor: Critique: Points to Consider 3 | citation: main.py:45\ndemoted:\n- anchor: Clarifying Q8 | item: E-7\n-->\n\n## Save rule\n\nSave the combined review-and-resolution document to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`. Output only the combined review-and-resolution document \u2014 no meta-commentary.\n\n## Return\n\nConfirm "Review-and-resolution document written to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`." and report the total count of visible E-items captured together with the three accounting totals (`silent_validated`, `silent_accepted`, `silent_demoted`).\n'};init_version_generated();var README='# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Install\n\nFrom your **project root**, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\nThat is the whole setup command. It works whether or not you already have a Bridge\naccount \u2014 it will ask.\n\n> We recommend **the command above** instead of the `npm i @bridge_gpt/mcp-server`\n> one in npm\'s sidebar, because it **will make set up much easier**.\n\n**What it will do**\n\n- **Bootstraps the Bridge MCP for you** \u2014 one command and your editor\'s agent can\n use Bridge\'s tools and slash commands on this project.\n- Registers a `bridge` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates and updates the files it needs inside your project root: slash commands\n and agent definitions for your editor (`.claude/commands/`, `.cursor/commands/`,\n and the equivalents your editor uses), your editor\'s MCP config, and `.bridge/`\n for your project manifest and pipeline definitions.\n- Stores your Bridge credential outside the project, so the MCP server and the\n tooling that spawns its own shells can find it without you configuring anything.\n Re-running `install` still asks for the credential unless you supply it through\n `--api-key` or `BAPI_API_KEY` \u2014 the installer writes that store, it does not read\n it back.\n- Writes outside your project root only when you pick a host whose configuration is\n global: OpenAI Codex (`~/.codex/config.toml`).\n\n**Prerequisites**\n\n- **Node.js 18 or newer** (`node --version`), which is what provides `npx`.\n- **A project directory** \u2014 run the command from the folder your editor opens: your\n repository root, the one containing `.git`. No `package.json` is required \u2014 SFCC\n cartridge repos, Python, Go, Rust, and other non-Node projects work the same way.\n- **An MCP-capable editor or CLI**: Claude Code, GitHub Copilot in VS Code, Cursor,\n Windsurf, or OpenAI Codex.\n- **No Bridge account needed.** The installer can create one for you from just an\n email address.\n\n## Contents\n\n- [Install](#install)\n- [Installation details](#installation-details)\n - [Installing, step by step](#installing-step-by-step)\n - [What to expect](#what-to-expect)\n - [Troubleshooting](#troubleshooting)\n- [Usage Documentation](#usage-documentation)\n - [Regularly useful](#regularly-useful)\n - [Occasionally useful](#occasionally-useful)\n - [Now and then](#now-and-then)\n - [Workflow commands](#workflow-commands)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./docs/CONDUCTOR.md).\n\n## Installation details\n\n### Installing, step by step\n\n**1. Open a terminal in your project root.** This matters: the installer writes\nyour slash commands and MCP config relative to the directory you run it from. If\nyou run it in your home directory, your editor will not find any of it.\n\n**2. Run the command.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\n**3. Answer the sign-in question.** On a first run it asks whether you already have\na token:\n\n```\n1. I need a token\n2. I already have a token\n```\n\n- Choose **1** if you have nothing yet. It asks for your email address and a name\n for your new Bridge project, then creates both for you.\n- Choose **2** if someone gave you a token \u2014 either a Bridge API key or an invite\n code. Paste it at the hidden prompt; you do not have to say which kind it is,\n because the installer recognizes it. Nothing is echoed as you type.\n\nThere is no default answer, so pressing Enter alone selects nothing. If you would\nrather not be asked, pass the answer up front instead \u2014 see\n[Choosing how you sign in](#choosing-how-you-sign-in).\n\n**4. Pick which editors to configure.** The installer detects the MCP hosts on your\nmachine and asks which ones to set up. Pick every editor you actually use for this\nproject; you can re-run the command later to add another.\n\n**5. Reload your MCP host.** Editors read their MCP configuration at startup, so a\nfreshly written config is not live until you reload. Restart the editor, or use its\n"reload MCP servers" action. In Claude Code you will also be asked to trust the\nproject\'s `.mcp.json` the first time.\n\n**6. Finish in the agent session the installer opens \u2014 when it opens one.** The\nlast thing the installer does is offer to open a fresh agent session running\n`/install-bridge`, which reads your codebase, fills in the remaining project\nsettings, and prints a short report of what Bridge can help with. Let it finish.\n\nThree things all have to hold for that session to open: your selection has to\ninclude a host the installer can launch, the run has to be on an interactive\nterminal, and you have to accept the consent prompt (*"Bridge can configure and set\nup this project for you automatically. Open a `<tool>` session to do that now?\n(Y/n)"*). Claude Code is the only selection that launches on its own. A\nCursor-only, Copilot, Codex, or Windsurf selection, a non-interactive run, or a\ndeclined prompt all print the command to continue by hand instead. Pass\n`--agent claude` or `--agent cursor-agent` to override the decision outright.\n\n**7. Follow the next step the session shows you, if it shows one.** The installer\nasks the server what should happen next and shows that command only when there is\none to show \u2014 most often `/learn-repository`, which it recommends when the project\nstill needs its architecture, testing, review, and correctness standards documented\nand your key can run it. The installer deliberately does not run it for you. Those\nstandards are what make every later plan, critique, and review match how your\nproject actually works, and they only need to be gathered once per project \u2014 the\nresult is shared with everyone on the team. If the session shows no next step,\nthere is nothing for you to run.\n\nWant to see what would happen without changing anything? Add `--dry-run`.\n\n<details>\n<summary id="what-to-expect"><strong>What to expect</strong></summary>\n\n**Files that appear in your project**\n\n| Path | What it is | Commit it? |\n|---|---|---|\n| `.claude/commands/`, `.cursor/commands/`, `.github/prompts/`, `.github/skills/` | The generated workflow commands for supported agent hosts | Yes |\n| `.claude/agents/` and editor equivalents | Agent definitions used by those commands | Yes |\n| `.bridge/config` | Your project manifest \u2014 the repository name and which MCP targets to provision. Deliberately secret-free | Yes |\n| `.bridge/pipelines/`, `.bridge/instructions/` | Editable pipeline definitions | Yes |\n| `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` | MCP registrations for your editor. These can carry your key, so the installer git-ignores them | No |\n\nThe installer tells you which of these are safe to commit and never recommends\ncommitting anything that can hold a credential.\n\n**Prompts you will see.** More than the sign-in question, in three groups:\n\n- *Always on a first bare interactive run:* the sign-in question, a hidden prompt\n for a token (or a visible one for an email), a project name for a brand-new\n project, a picker for which editors to configure, and an offer to connect GitHub\n (`Connect GitHub? [y/N]:`).\n- *Conditional on your situation:* a confirmation when the directory has no `.git`\n (default **No**, and declining aborts); a *"Which tool should open? [1-N]"*\n chooser when your selection contains more than one launchable tool; and the\n consent prompt before the final agent session.\n- *Overwrite confirmations, each default **No** and each skippable with `--force`:*\n a saved key for this project already exists; a host config already contains a\n `BAPI_API_KEY`; a **git-tracked** config would receive your real key; a saved but\n expired self-serve signup would be discarded.\n\n**A fresh agent session opens at the end \u2014 if your selection can launch one.** See\nstep 6 above for the three conditions. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Selecting Windsurf prints instructions instead of writing config.** Windsurf\'s\nglobal `mcp_config.json` is never modified automatically; the installer reports the\nentry for you to paste yourself. Codex *is* written automatically, even though its\nfile is global too.\n\n**Your key is stored for the tools that read the store.** The MCP server and the\nshell-spawned tooling (`start-tickets` and its model routing) resolve it from\n`~/.config/bridge/credentials.json` on their own. The **installer** does not: a\nrepeat `install` prompts for the credential again unless you pass `--api-key` or\nset `BAPI_API_KEY` in the environment.\n\n**A next step, when the project needs one.** The session closes with whatever\ncommand the server says comes next, and stays quiet when there is nothing to\nrecommend. `/learn-repository` is the usual one: it is recommended when the project\nstill needs its conventions documented and your key can run it. It is never\nautomatic \u2014 until someone runs it, Bridge\'s agents work from your code alone rather\nthan from your project\'s documented conventions.\n\n**Indexing happens on its own.** There is no "index my repository?" question. Once\nyour project has the settings it needs, indexing starts server-side. You never have\nto ask for it.\n\n</details>\n\n<details>\n<summary id="troubleshooting"><strong>Troubleshooting</strong></summary>\n\n**"My editor doesn\'t see any Bridge tools."** Two usual causes. First, the config\nwas written somewhere your editor is not looking \u2014 re-run the installer from the\ndirectory your editor actually opens, and check that a `bridge` entry exists in\nthat project\'s MCP config. Second, the editor has not been reloaded since the file\nwas written; restart it. In Claude Code, also confirm you accepted the trust prompt\nfor the project\'s `.mcp.json`.\n\n**"I ran it in the wrong folder."** Nothing is broken. Depending on which editors\nwere detected, a run can leave `.bridge/`, `.bridge/install-state.json`, `.claude/`,\n`.cursor/commands/`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.github/agents/`,\n`.mcp.json`, and appended `.gitignore` lines. Remove only what that run created and\nre-run the command from the right directory \u2014 if you already had a `.vscode/`,\n`.cursor/`, or `.gitignore` there, keep the parts you had before.\n\n**"It seems to hang with no output."** If you ran the bare command\n(`npx -y @bridge_gpt/mcp-server`) with no subcommand, you started the MCP *server*,\nnot the installer. It is waiting for an editor to connect over stdio, which is\nexactly what it should do when your editor launches it \u2014 but from a terminal it\nlooks like a hang. It prints a line saying so. Press Ctrl-C and run\n`npx -y @bridge_gpt/mcp-server install` instead. The explicit spelling\n`npx -y @bridge_gpt/mcp-server serve` starts the server on purpose.\n\n**"It can\'t reach Bridge" or "my key was rejected."** The installer checks\nconnectivity before it saves your **credential** anywhere, so a failure here has not\nwritten your key into a config or stored it for later. It has already\nscaffolded the project files by then \u2014 slash commands, agents, pipelines,\n`.bridge/config`, and secret-free per-host MCP placeholders \u2014 so expect those to\nexist; re-running is safe and refreshes them. A\nrejected key means the credential is not valid for that project \u2014 check the project\nname you gave, and generate a fresh key on the Bridge web UI\'s **Security** page if\nneeded. A network failure usually means a proxy or VPN is in the way.\n\n**"Which repository name should I use?"** The one registered with Bridge. If you\nhave an existing key, the installer usually resolves it for you; when it cannot, it\nasks, and `--repo <name>` answers it up front.\n\n**Still stuck? Ask the installer to diagnose itself.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server doctor\n```\n\n`doctor` is strictly read-only. It reports what it found \u2014 configs, registrations,\ncredential availability, prerequisites \u2014 and changes nothing.\n\n**Codex approval profile.** When selecting Codex during `install`, opt in at the\nprompt or pass `--tools=codex --codex-profile=on` in a scripted install. Bridge\ncreates `$CODEX_HOME/bridge.config.toml` (default `~/.codex/bridge.config.toml`)\nwith approval settings for the tools used by its review and implementation\nworkflows. Bridge-spawned Codex workers then select it automatically, including\nthe fresh implementation session after review. Pass `--codex-profile=off` to\ndecline; the installer leaves any existing profile content intact. If a profile\nalready contains user-authored or conflicting content, the installer leaves it\nuntouched and reports that workers cannot use it. Run `doctor --agent codex` to\ninspect the current status. To use the profile in a Codex session you open\nyourself, run `codex --profile bridge`. Bridge `--auto` controls recipe gates;\nCodex MCP tool approvals are a separate setting. The profile does not change\nglobal `sandbox_mode` or `approval_policy`.\n\n</details>\n\n<details>\n<summary id="choosing-how-you-sign-in"><strong>Choosing how you sign in</strong></summary>\n\nThree routes lead to the same place. The interactive question above picks one for\nyou; these flags pick it up front and skip the question entirely.\n\n**No account yet \u2014 sign up with an email.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --email you@example.com\n```\n\nCreates a brand-new Bridge project for that address and your first admin key in one\ncommand. No account, no key, and no invite needed beforehand. The address labels\nyour new workspace and may receive a setup message; delivery is best-effort, so\nnothing waits on it. The email is visible as you type (it is not a secret) and is\nnever written to a log. This is the same route as answering **1** at the prompt.\n\n**You were sent an invite code.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --invite\n```\n\nRedeems the invite, creates your project, and mints your first admin key. Run it\n*without* a value, as shown: the installer then asks for the code at a hidden\nprompt, so the code never lands in your shell history. `--invite <code>` and the\n`BAPI_INVITE` environment variable exist for scripting, but both expose the code to\nyour shell history and to the process list.\n\n**Your team already has a project and gave you an API key.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --api-key <key>\n```\n\nOr omit the flag and paste the key at the hidden prompt. Generate a key on the\nBridge web UI\'s **Security** page (**Create New Key**, role **Admin**) and copy it\nimmediately \u2014 it is shown once. `BAPI_API_KEY` works too.\n\nIf you paste an invite code where a key was expected, or a key where an invite was\nexpected, the installer recognizes the mismatch and tells you before anything is\ncreated or spent.\n\n</details>\n\n<details>\n<summary><strong>Installer flags</strong></summary>\n\n| Flag | What it does |\n|---|---|\n| `--email <addr>` | Sign up for a new Bridge project with just an email address |\n| `--invite [code]` | Redeem an invite code. Omit the value for the hidden prompt (recommended) |\n| `--api-key <key>` | Use an existing Bridge API key |\n| `--repo <name>` | Name the registered repository instead of resolving or asking for it |\n| `--tools <list>` | Configure specific MCP hosts without the picker. Accepted IDs are exactly `claude-code`, `cursor`, `copilot-vscode`, `codex`, and `windsurf` (e.g. `claude-code,cursor`); any other value is a parse error |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step. **No default** \u2014 without this flag the agent is derived from the hosts you selected, and an explicit value always wins, including for a host you did not select |\n| `--dry-run` | Preview every step without writing, contacting Bridge, resolving or prompting for a credential, or opening anything. Genuinely inert: it returns before the project-root prompt, before the repository is resolved, and before any tool-selection prompt, so a value it cannot know locally (an unresolved repository name, an unselected tool) is shown as **not yet known** rather than guessed |\n| `--force` | Overwrite an existing stored key without asking |\n| `-h`, `--help` | Full usage |\n\n`--email`, `--invite`, and `--api-key` are mutually exclusive \u2014 each names a\ndifferent way to arrive, and the installer will not guess between them.\n\n</details>\n\n<details>\n<summary><strong>Setting up an MCP host by hand</strong></summary>\n\nThe installer configures your editors for you. Do this only if you would rather\nwrite the config yourself, or if you use a host it cannot write automatically.\n\nScaffold the project files and write a secret-free MCP registration. Run it from\nthe same project root `install` uses \u2014 your repository root, the one containing\n`.git`. No `package.json` is required:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` always creates `.mcp.json`, and adds `.vscode/mcp.json` or\n`.cursor/mcp.json` when it detects those editors. Each generated entry carries\n`BAPI_BASE_URL`, `BAPI_REPO_NAME`, `BAPI_DOCS_DIR`, and `BAPI_PROJECT_ROOT`, and\n**never** `BAPI_API_KEY` \u2014 the server resolves the credential itself at runtime.\n\nSo the manual work left after `--init` is narrower than writing an entry from\nscratch: correct `BAPI_REPO_NAME` if it was written as the `YOUR_REPO_NAME`\nplaceholder, and supply your credential through a supported source (`BAPI_API_KEY`\nin the entry\'s `env` block, `BAPI_API_KEY` in the server\'s environment, or the\n`~/.config/bridge/credentials.json` store).\n\nWrite the entry yourself instead \u2014 for a host `--init` does not touch, or because\nyou would rather \u2014 using the shapes below. Add `"serve"` as the last launcher\nargument, as shown: it is the explicit way to say "start the MCP server." Pin the\npackage to an exact version and pass `--prefer-offline`, which is what the\ngenerated entries do and what keeps npx from resolving a different build on some\nlater boot.\n\n<!-- BEGIN GENERATED: mcp-host-examples (managed by mcp_server/scripts/sync-readme-host-examples.js \u2014 DO NOT EDIT BY HAND) -->\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>GitHub Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge]\ncommand = "npx"\nargs = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"]\n\n[mcp_servers.bridge.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see [Environment Variables](#environment-variables)).\n</details>\n<!-- END GENERATED: mcp-host-examples -->\n\nAfter saving, reload your editor and ask your assistant to call the `ping` tool to\nconfirm the connection.\n\nAn entry with no trailing `serve` still starts the server \u2014 bare invocation means\n"server" permanently, and nothing rewrites an existing config to add the token.\n\n</details>\n\n<details>\n<summary><strong>Upgrading Bridge</strong></summary>\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest --upgrade\n```\n\n`upgrade` fetches the latest published version, refreshes your scaffolded slash\ncommands, agents, and pipelines, updates the version pin in your MCP config, and\nopens a session so you can reconnect. It is also available as the\n`/upgrade-bridge` slash command.\n\nUse the `@latest` form. It applies to the short-lived *upgrader* process: without\nit, npx may reuse a cached older copy of the package and "upgrade" you with the\nbuild you are trying to replace. The exact `MAJOR.MINOR.PATCH` pin the upgrader\nwrites into your MCP config is deliberately different \u2014 host configs stay pinned\nto an exact release so a project\'s server is reproducible.\n\n`upgrade` reports **per config file**, because a project can have several\n(`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) and they can disagree:\n\n```\nLauncher pins:\n .mcp.json: 0.2.16 -> 0.2.36\n .cursor/mcp.json: already 0.2.36\n```\n\nWhen every applicable launcher pin was already at the target, it prints\n`Already up-to-date.` \u2014 that status comes from comparing your configs, not from\nthe version of the CLI process. A non-zero exit means the upgrade did **not**\nconverge, and nothing is reported as complete in that case. The causes:\n\n- the npm registry lookup failed **and** this process was not started from\n `@latest`, so the target version could not be confirmed \u2014 the likeliest one\n offline, and why the canonical command uses `@latest`;\n- a launcher pin is already **newer** than the target, which an automated repin\n must never downgrade;\n- an unreadable or unparseable config, a launcher carrying a version range or a\n dist-tag rather than an exact release, or two Bridge registrations in one file;\n- a competing local install it could not remove, or a pin that failed post-write\n verification;\n- the upgrade finished but left an **unconfigured** MCP entry \u2014 one that would\n authenticate as nobody.\n\nThe server checks for updates on startup. The check is cached for a day and never\nblocks startup. When a newer version is known, it surfaces in two places you do\nnot have to go looking for: a one-line warning on the server\'s **stderr**, and a\nshort advisory attached to the ordinary `tools/list` response so the agent in the\nsession can see that some tools may be missing or renamed in the older build.\nNeither requires calling `ping` or `doctor`.\n\nRe-running `install` on an already-configured project is safe: it refreshes the\nscaffolded files without overwriting your stored credential unless you pass\n`--force`.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful**, **how to use it**, and its **flags**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships SFCC platform tools \u2014 read-only introspection under the `sfcc` profile, and nine destructive writes under the separate `sfcc-write` opt-in. See [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n<!-- The three tier sections below are GENERATED from TWO catalogs by\n scripts/sync_mcp_server_readme.py: api/library/config/mcp_tool_catalog.json,\n the authoritative MCP tool catalog, and api/library/config/workflow_catalog_lib.py,\n the immutable catalog of slash-command workflows (which have no MCP registration\n and therefore cannot live in the JSON artifact). Edit the curated tool metadata in\n scripts/sync_mcp_tool_catalog.py and the workflow definitions in\n workflow_catalog_lib.py \u2014 never the JSON artifact and never the text between the\n markers. Generation order is: sync_mcp_tool_catalog.py, then\n sync_mcp_server_readme.py, then `cd mcp_server && npm run build` (which bundles this\n file into readme.generated.ts, served as the MCP resource bridge://readme).\n Everything outside the marker pair \u2014 including the sections below it \u2014 is hand-written. -->\n\n<!-- BEGIN GENERATED: mcp-tool-documentation (managed by scripts/sync_mcp_server_readme.py \u2014 DO NOT EDIT BY HAND) -->\n### Regularly useful\n\nThe tools worth knowing for most tickets.\n\n**1. Review Ticket**\n- **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.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-tickets BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review). A single key reviews inline in the current session \u2014 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.\n- **Flags:** `--auto` auto-accept findings and skip the approval gates \xB7 `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the difficulty-adaptive review policy decide.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default \xB7 `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs \xB7 `--rounds=1|2` forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override.\n\n**3. Review and Start**\n- **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 \u2014 review and implementation run in two separate agent contexts, not one shared session.\n- **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).\n- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).\n- **Flags:** `--auto` auto-approves both the review and the implementation phase of every spawned session \xB7 `--rounds=1|2` forwarded to the review phase \xB7 `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.\n\n**4. Explore Ticket**\n- **What it does:** Maps the code paths, dependencies, and project conventions a task would touch, settles its acceptance criteria with you on a decision page, then compares the viable implementation approaches and their trade-offs and writes up a proposed design. Along the way it surfaces the ambiguities that still need deciding and can pull in optional web or deep research where the answer is not in the code.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or a plan, when you\'re unsure how a change would fit the existing code and want the open questions and the realistic options laid out first.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n- **Flags:** None.\n\n**5. Council**\n- **What it does:** Fans your problem out to two different models and returns their approaches, in technical, design, discovery, or general mode.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 technical for how to build it, design for how it should look, discovery for what still needs figuring out before a real ticket exists, general for a quick brief-driven pass before the repository is indexed.\n- **How to use it:** `/council <question>`\n- **Flags:** `--mode` selects one of four modes, passed to the underlying `request_council` tool as e.g. `mode: "discovery"`: `technical` (the default \u2014 implementation/architecture approaches), `design` (UI/UX and visual direction), `discovery` (stakeholder discovery questions, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), and `general` (brief-driven ideation from your task description alone). `technical` and `discovery` are codebase-grounded and need an indexed repository; `general` needs no code index at all, so it works immediately after install. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n\n**6. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge \u2014 libraries, best practices, standards \u2014 that you do not already have.\n- **How to use it:** `/bridge-research <question>`\n- **Flags:** None.\n\n### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. Upload Ticket**\n- **What it does:** Creates a real Jira issue from a drafted ticket, including child tickets under an epic; your agent should confirm with you before creating it.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into your tracker so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket; it should confirm with you before creating the live issue.\n- **Flags:** Name the issue type (Bug / Story / Task / Epic) and, for a child ticket under an epic, the parent key.\n\n**2. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket that references real files in your codebase.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before \u2014 or instead of \u2014 auto-implementing it.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**3. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a technical design document, a functional spec, or a product requirements document.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family, without saving an artifact.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** Ask your agent \u2014 "Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against production."\n- **Flags:** Pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**5. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model, spending provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** Ask your agent \u2014 "Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."\n- **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**6. Create Decision Page**\n- **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.\n- **When it\'s useful:** (Refinement) Use it whenever a conversation has accumulated more open choices than are comfortable to settle in prose \u2014 not for a single simple question, which is faster to just ask directly.\n- **How to use it:** `/create-decision-page <topic or open questions>` \u2014 e.g. *"how we handle rate limiting"* \u2014 or leave it empty to frame the open choices already raised earlier in the conversation.\n- **Flags:** None.\n\n### Now and then\n\nUseful once in a while.\n\n**1. Reimplement Ticket**\n- **What it does:** Gathers the context and attachments added since the last pass so a targeted follow-up change can be made.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n- **Flags:** None.\n\n**2. Update Ticket**\n- **What it does:** Rewrites a ticket\'s description, fully replacing what is there today.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** Ask your agent \u2014 "Replace BAPI-123\'s description with: <new text>." It fully overwrites the live description, which is hard to reverse.\n- **Flags:** None.\n\n**3. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description, and optionally the model tier its difficulty routes to.\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** Ask your agent \u2014 "Pull up BAPI-123 and show me its description, status, and acceptance criteria."\n- **Flags:** `include_model_tier` adds the model tier the ticket\'s difficulty routes to; leave it off for an ordinary read.\n\n**4. Search Tickets**\n- **What it does:** Searches across the tickets in your project.\n- **When it\'s useful:** (Refinement) When you need to find tickets by project, status, or wording rather than by key.\n- **How to use it:** Ask your agent \u2014 "Search our project for open tickets mentioning rate limiting."\n- **Flags:** Narrow the search by project, status, issue type, or free text.\n\n**5. Write Comment**\n- **What it does:** Posts a comment on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** Ask your agent \u2014 "Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it is rotated."\n- **Flags:** A long comment can be attached as a file instead of inlined.\n\n**6. Read Comments**\n- **What it does:** Reads the comment thread on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When the discussion on a ticket matters and you want the agent to read it before acting.\n- **How to use it:** Ask your agent \u2014 "Read the comments on BAPI-123 and summarize what was decided."\n- **Flags:** None.\n\n**7. Ticket Attachments**\n- **What it does:** Downloads files from a ticket to your disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files or logs you need locally, or you want to attach output back to it.\n- **How to use it:** Ask your agent \u2014 "Download the design mockups attached to BAPI-123 into my docs folder," or "Attach build-log.txt to BAPI-123."\n- **Flags:** Choose the direction (download from the ticket, or upload to it) and, for a download, where the files should land.\n\n**8. Estimate Epic**\n- **What it does:** Estimates an epic, or an explicit group of tickets you name.\n- **When it\'s useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.\n- **How to use it:** `/estimate-epic BAPI-123`\n- **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.\n\n**9. Regression Check**\n- **What it does:** Runs the deterministic regression-reviewer (lightweight mode) against a proposed code change and reports its blast radius \u2014 which real call-sites, tests, mocks, or config the change does and doesn\'t account for.\n- **When it\'s useful:** (Correctness) Before merging a change, to see which real callers, tests, or config it leaves unaccounted for.\n- **How to use it:** `/regression-check` reviews the working tree against `HEAD` by default; `/regression-check --diff main...HEAD` reviews an explicit range.\n- **Flags:** `--diff <range>` review a specific diff/PR range instead of the working tree vs. `HEAD` \xB7 `--symbols a,b,c` review a not-yet-diffed planned change by naming its symbols directly.\n\n**10. Scan Test Coverage**\n- **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.\n- **When it\'s useful:** (Testing) Periodically, to find recently shipped features still missing integration-test coverage and how each could be smoke tested instead.\n- **How to use it:** `/scan-test-coverage` runs incrementally from the last recorded marker in `docs/test-coverage/STATE.md`.\n- **Flags:** `--since=YYYY-MM-DD` override the window start date \xB7 `--full` ignore the marker and use a 6-month lookback \xB7 `--limit=N` cap the number of features investigated.\n\n**11. Estimate Ticket**\n- **What it does:** One-ticket shortcut over the existing epic estimation capability: estimates a single Jira ticket via the same `estimate_epic` tool.\n- **When it\'s useful:** (Estimation) When you want one ticket\'s estimate without typing out an epic key or an explicit ticket list.\n- **How to use it:** `/estimate-ticket BAPI-123`\n- **Flags:** `--allow-partial` the sole optional flag.\n\n**12. Draft Ticket**\n- **What it does:** Delegates to the Jira Ticket Writer to create only a local Markdown draft \u2014 it does not create or update a Jira issue.\n- **When it\'s useful:** (Refinement) When you want a reviewable local ticket draft before committing to an upload workflow.\n- **How to use it:** `/draft-ticket <description>`\n- **Flags:** None.\n<!-- END GENERATED: mcp-tool-documentation -->\n\n### Workflow commands\n\nSlash 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) \u2014 the rest live here.\n\n**1. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**2. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** Ask your agent, *"Use the jira ticket writer to turn our conversation into a ticket."* The other ticket commands draft through it automatically.\n- **Flags:** None \u2014 name a specific standards file in your request to have it applied when drafting.\n\n**3. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n- **Flags:** None.\n\n**4. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n- **Flags:** None.\n\n#### Ticket-authoring posture\n\n`/explore-ticket`, `/idea-to-ticket`, and `/plan-epic` all decide ticket shape\nthe same way, as does the `jira-ticket-writer` agent they draft through. A fresh install inherits this with no configuration\nstep and no server call; the full rationale and the closed exception list ship as\n`docs/bridge-ticket-authoring.md`.\n\n- **Drafted by the writer.** Every ticket body \u2014 epic parent, epic child, and\n ordinary sibling alike \u2014 goes through the `jira-ticket-writer` agent. Nothing\n composes a ticket description inline.\n- **Sized toward L, overflowing upward.** `L` (target) \u2192 `XL` (when the work\n does not fit in `L`) \u2192 `M` (third choice) \u2192 `S` (only when unavoidable). A\n slice that outgrows `L` becomes one `XL` ticket rather than two `L` ones \u2014\n splitting a coherent slice to fit a band buys another worktree, another PR, and\n another rebase for nothing. This binds a standalone ticket and an epic child\n alike. Past roughly 40 files or ~3000 LOC it splits anyway, into the largest\n coherent pieces available.\n- **Grouped at three.** Three or more tickets is an epic: an epic parent plus an\n ordered child manifest, shown in full at an approval gate before anything is\n created. One or two are ordinary siblings \u2014 no epic parent, no manifest. The\n threshold is exactly three.\n- **Decomposed once, rendered many.** One pass freezes the split; body drafting\n then fans out one writer invocation per entry against that frozen manifest. A\n rendering invocation never re-splits, merges, reorders, or rescopes.\n- **Handed off once.** An epic handoff names exactly one entry point,\n [`drive-epic`](#drive-epic) \u2014 never a choice between conductors.\n\n**5. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests`\n- **Flags:** `--unit-only` skip the E2E suite \xB7 `--skip-e2e` same, phrased the other way.\n\n**6. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n- **Flags:** None.\n\n**7. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n- **Flags:** None.\n\n### Operational commands\n\nRepository and ticket housekeeping commands, beyond the tiers above. Implementation, commit/push, PR creation, and CI monitoring are steps of the `implement-ticket` recipe rather than standalone commands:\n\n| Command | What it does |\n|---|---|\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> **GitHub Copilot modes.** On VS Code\'s Local agent, Bridge provisions one\n> `.github/prompts/<command>.prompt.md` prompt per packaged command, including\n> `/explore-ticket`. On VS Code Agent Host, prompt files are not loaded, so the\n> installer provisions the same canonical command as a project skill at\n> `.github/skills/<command>/SKILL.md`; invoke that skill by its slash-command\n> name. Bridge does not claim Copilot CLI or cloud-agent workflow support from\n> this scaffold alone. If either artifact is not discovered, use VS Code\'s Chat\n> Customizations diagnostics, confirm the workspace is trusted, and invoke the\n> workflow manually with its full instructions rather than assuming the slash\n> command is available.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks \u2014 the commit-and-push, PR-creation, and CI-monitoring steps of the `implement-ticket` recipe, driven by its bundled instruction assets.\n- **Design documents** for a ticket (TDD / FSD / PRD, generated and retrieved through `create_doc` / `get_doc`).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo and check parse status (`parse_repository`), regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** fetch a resolved pipeline recipe (the step list behind the orchestration commands) and execute its steps agent-side.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity check:** ping Bridge to confirm the API key and repository resolve.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, design docs) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, MRT bundle push, and SCAPI Custom API scaffolding. **As of `@salesforce/b2c-dx-mcp` 1.1.2 (published 2026-05-20)** it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. That comparison is dated on purpose: its basis is this repository\'s hand-maintained [vendor manual](../docs/mcp/b2c-commerce-developer.md), pinned to the same version, so a new Salesforce toolset ages the claim visibly instead of rotting silently. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **OCAPI Data API introspection** of system objects, custom object definitions, and site preferences \u2014 plus, behind a separate opt-in, a set of sandbox-bounded writes.\n\n**Every SFCC tool is restricted to a developer sandbox, and the restriction is checked at invocation time against the hostname your credentials actually resolve to** \u2014 not against anything the caller passes in. If `dw.json` or `SFCC_HOSTNAME` names a host Bridge does not recognize as a developer sandbox, every SFCC tool refuses with a `403` before contacting it. See [Sandbox enforcement](#sandbox-enforcement).\n\n**Credentials stay local** \u2014 in `dw.json` or `SFCC_*` env vars \u2014 and are never sent to Bridge. The `sfcc` profile registers read-only tools; the nine destructive write tools require the separate `sfcc-write` opt-in (see [Read and write profiles](#read-and-write-profiles)).\n\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nEvery SFCC tool must be enabled with a profile (step 3) \u2014 including the two diagnostics, `sfcc_setup_status` and `check_permissions`, which belong to the `sfcc` profile. A session on the default `core` profile registers no SFCC tool at all. Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks the OCAPI tools \u2014 the eight reads, the nine writes, and `check_permissions`. It does **not** block `sfcc_setup_status`, and it does not block `sfcc_log_query`: log query runs on its own gate, which reads neither the `version` field nor `dw.json` and instead probes the backend log capability (log access is WebDAV Basic auth, a different boundary from OCAPI\'s OAuth). Set the field via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. `dw.json` is auto-added to git exclude and must never be committed.\n\nCredentials resolve in **three tiers, highest first** \u2014 the environment wins over `dw.json`, not the other way round:\n\n1. An explicit dotted `instance` argument on the call, **plus** `SFCC_CLIENT_ID` and `SFCC_CLIENT_SECRET` in the environment. Secrets are never read from `dw.json` on this tier, so an explicit instance without those two env values is an error.\n2. `SFCC_HOSTNAME` **and** `SFCC_CLIENT_ID` **and** `SFCC_CLIENT_SECRET`, all three set.\n3. `dw.json`.\n\nBecause tier 2 outranks tier 3, a stale `SFCC_HOSTNAME` left in the environment silently wins over the `dw.json` you are looking at. Check both when a tool reports an unexpected host.\n\n**Use a single-config `dw.json`, or set all three `SFCC_*` variables.** A multi-entry `configs[]` array is **rejected outright** \u2014 it is not a working setup that merely requires an explicit `instance` on every call. Two things make that workaround unavailable: an explicit `instance` takes tier 1, which needs the client id and secret in the environment anyway, and most tools cannot accept a hostname at all \u2014 the value must contain a dot, and the site-preference tools constrain `instance` to `staging | development | sandbox | production`, none of which is a hostname.\n\n**3. Enable the tools you want.** Add the groups to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\n`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.\n\n**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 \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\nRead what it prints before pasting it. The output is **two labelled blocks**, and they are not equivalent:\n\n- **READ/SEARCH TOOL GRANTS** \u2014 what the `sfcc` read tools need: `get` on `/system_object_definitions`, and `get` + `post` on `/system_object_definitions/**`, `/site_preferences/**`, and `/custom_object_definitions/**`. The `post` is OCAPI\'s convention for its `*_search` endpoints, not a mutation \u2014 but it is a grant you are pasting, so it is labelled for what it is rather than as "read-only".\n- **MUTATION GRANTS** \u2014 required by the nine `sfcc-write` tools and by nothing else: `put`/`patch` on `/system_object_definitions/**` and `/custom_object_definitions/**`, and `patch` on `/site_preferences/**`. Paste this block only if you intend to enable `sfcc-write`.\n\nNeither block grants `delete`, and neither pastes the global `resource_id: "/**"` that would cover every Data API resource. Each entry names one resource family \u2014 `/system_object_definitions`, `/custom_object_definitions/**`, `/site_preferences/**` \u2014 so the wildcard is scoped to the family, not to the API. Within a family it is still broad, and `write_attributes` is `(**)`, so a throwaway sandbox is the right place for these.\n\n</details>\n\n### Sandbox enforcement\n\nEvery SFCC tool \u2014 all twenty, reads and writes alike, including the diagnostics \u2014\npasses through one check before its own logic runs: **the hostname your\ncredentials actually resolve to must be a recognized developer sandbox.**\n\n- The check reads `credentials.hostname`, the value that goes into the OCAPI\n URL. It does not read the `instance` tool argument. Omitting `instance`, or\n passing `instance: "sandbox"`, has no effect on the decision \u2014 neither one\n selects or proves anything about the target. A dotted `instance` still\n *selects* a host through the documented credential precedence, but the host it\n selects is then validated like any other, so `check_permissions` cannot be\n aimed at a named production instance.\n- It **fails closed.** An unrecognized, malformed, or unparseable hostname is\n refused with HTTP `403`, `error.code: "TARGET_NOT_SANDBOX"`, and\n `error.details.failure_class: "target-not-sandbox"`, before any request leaves\n your machine.\n\nThe accepted hostname forms are:\n\n| Form | Example |\n|---|---|\n| `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.dx.commercecloud.salesforce.com` | `zyis-001.dx.commercecloud.salesforce.com` |\n\nAnything else is refused. In particular, hosts whose leading label names an\nenvironment (`production-\u2026`, `staging-\u2026`, `development-\u2026`) are rejected even\nwhen they otherwise fit a form above, and the legacy `*.demandware.net` domain\nis not accepted at all \u2014 sandbox, staging, and production instances share that\ndomain with no suffix that separates them.\n\n### Read and write profiles\n\n`sfcc` and `sfcc-write` are **independent** groups. Neither implies the other.\n\n| `BRIDGE_MCP_PROFILE` | SFCC tools registered |\n|---|---|\n| unset / `core` | none |\n| `estimation` | none \u2014 this group adds only `estimate_epic`, which is not an SFCC tool |\n| `sfcc` | `sfcc_setup_status`, `check_permissions`, the 8 OCAPI read tools, and `sfcc_log_query` |\n| `sfcc-write` | the 9 destructive write tools **only** \u2014 it does not imply `sfcc`, so neither diagnostic and no read tool is registered |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable, and it also includes `estimation` |\n\n**`check_permissions` needs `sfcc`, even when you are diagnosing writes.** It is\nthe tool that prints the mutation-grant OCAPI Settings JSON on a 401/403, but it\nis registered by the `sfcc` group, not `sfcc-write`. An `sfcc-write`-only session\ntherefore cannot run it. Use `BRIDGE_MCP_PROFILE=sfcc,sfcc-write` whenever you\nneed to diagnose write grants.\n\n**Diagnostics moved into `sfcc`.** `sfcc_setup_status` and `check_permissions`\nused to register unconditionally, on every session including non-SFCC ones. They\nno longer do. The accepted consequence is that a session without `sfcc` can no\nlonger ask the server why SFCC tools are missing \u2014 run `npx -y @bridge_gpt/mcp-server doctor`\nfor that instead, which reports the resolved profile without needing any tool.\n\n**Migration.** Enabling `sfcc` used to register the nine write tools as well. It\nno longer does. If you were relying on SFCC writes through\n`BRIDGE_MCP_PROFILE=sfcc`, change it to `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`.\nUsers of `BRIDGE_MCP_PROFILE=full` keep write access and need no change.\n\n### Tools\n\nTwenty tools in total, none of them registered unconditionally: the `sfcc` profile\'s **read-only** surface (the two diagnostics, eight OCAPI reads, and `sfcc_log_query`), plus the nine destructive writes that only the separate `sfcc-write` profile registers \u2014 see [Read and write profiles](#read-and-write-profiles). Every one of them is bounded to a developer sandbox by the same invocation-time check. All twenty are enumerated below.\n\nAn oversized response is saved in full to `BAPI_DOCS_DIR/sfcc/` and replaced by a parseable JSON descriptor \u2014 `truncated: true`, the `saved_path` it was written to, and the `page` metadata (`returned`, `total` when OCAPI supplied one, `has_more`) \u2014 so the collection metadata survives even though the data itself is on disk. If that save fails, the complete payload is returned inline instead, still as parseable JSON.\n\nAttribute-definition reads and writes can return an attribute\'s `default_value` at `projection: "full"`, and Bridge withholds it \u2014 every key is preserved except that one, whose value becomes `[REDACTED_BY_BRIDGE]` \u2014 from the inline response, the saved file, and a successful write echo alike. Attribute defaults are intentionally unavailable through this MCP surface; Business Manager is the supported path to read one.\n\n**Diagnostics** (registered by the `sfcc` profile)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (read/search grants for the `sfcc` tools, mutation grants for the `sfcc-write` tools). An explicit `instance` hostname is still subject to the sandbox check below.\n\n**System object model \u2014 reads** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one system object type\'s definition.\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**System object model \u2014 writes** (needs the `sfcc-write` profile; every one is a destructive write, sandbox only)\n- `system_object_attribute_definition_create` \u2014 create an attribute definition via `PUT /system_object_definitions/{type}/attribute_definitions/{id}`.\n- `system_object_attribute_definition_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_group_create` \u2014 create an attribute group via `PUT /system_object_definitions/{type}/attribute_groups/{id}`.\n- `system_object_attribute_group_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_assign_to_group` \u2014 assign an existing attribute definition into a group via `PUT \u2026/attribute_groups/{group}/attribute_definitions/{def}`.\n- `custom_preference_definition_create` \u2014 define a custom site or organization preference via `PUT /system_object_definitions/{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}`.\n\n**Custom object definitions** (reads need `sfcc`; the two writes need `sfcc-write`)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type (`default_value` withheld). OCAPI cannot enumerate custom object type *IDs* directly, so `object_type` must be known \u2014 but it is discoverable: call `system_object_list` at `projection: "full"` for each custom type\'s `display_name` and `attribute_definition_count`, derive a candidate id (e.g. strip spaces from `"Product Quality Result"` \u2192 `ProductQualityResult`), and confirm it by checking that this tool\'s returned attribute count matches that row\'s `attribute_definition_count`.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type (`default_value` withheld). Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability. Same discovery path as above applies to `object_type`.\n- `custom_object_definition_attribute_create` \u2014 **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.\n- `custom_object_definition_attribute_update` \u2014 **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH \u2026/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.\n\n**Site preferences** (reads need `sfcc`; the write needs `sfcc-write`; sandbox only)\n- `site_preference_group_list` \u2014 list the preference groups on a site. This is the discovery tool the other two reads depend on: both take a group, and this is how you find one.\n- `site_preference_get` \u2014 list the preference **identifiers** in a group.\n- `site_preference_search` \u2014 search/filter preference identifiers within a group.\n- `site_preference_values_set` \u2014 **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.\n\n> **Site preference values are write-only through this surface.** `site_preference_get` and `site_preference_search` return **ids only, never values** \u2014 an unset preference and one set to the empty string are indistinguishable. So you can *set* a value with `site_preference_values_set` and have no way to read it back through an MCP tool. Business Manager is the supported path to read a preference value.\n\n**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-cli`**, shelled out to directly: `b2c logs get --since <window> --search <q> --json`. There is no MCP alternative to reach for \u2014 `@salesforce/b2c-dx-mcp` ships **no `logs_*` tool** as of 1.1.2, and every log workflow in the vendor toolkit goes through the CLI anyway (see the [vendor manual](../docs/mcp/b2c-commerce-developer.md)). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted CLI output to an LLM.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`. For existing ticket keys, `/review-and-start` (see [Usage Documentation \u2192 Review and Start](#regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `review-and-implement` reviews the ticket, then hands approval to a fresh implementation session in the same worktree. `--auto` applies to both phases. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; Claude/Cursor use their registry aliases and repository overrides, while Codex uses the authenticated Bridge catalog. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing when its alias is available. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, lifecycle ledger events, and a supervisor peer tab). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent. Claude is the default. Cursor with `--auto` runs headlessly after a bounded authentication/readiness check; manual Cursor runs remain interactive for human review gates. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-6 \u2192 basic, 7-10 \u2192 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 \u2192 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.\n\nFor 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.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses only the per-ticket difficulty lookup and applies that tier to every ticket. Claude/Cursor use the same alias resolution as normal routing; Codex uses the fetched catalog. It is never a raw model alias, and no API key or credential belongs in the spawned command. A malformed tier requests the premium fallback when its alias is available; otherwise the agent default launches. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./docs/CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand \u2014 titled **`bridge doctor \u2014 read-only diagnostics`** \u2014 that diagnoses your whole Bridge install without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nThe report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.\n\nThe report also includes a **Claude login** advisory: whether the host\'s own\n`~/.claude.json` carries a login marker. This is informational only \u2014 it never\nblocks the doctor run and cannot guarantee the next worker spawn will\nauthenticate. See\n[Claude login for conductor workers](#claude-login-for-conductor-workers).\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n**`claude-review` workflow lineage (advisory, read-only).** `doctor` also reports\nwhether the current branch\'s `.github/workflows/claude-review.yml` matches the\nrepository default branch\'s copy. This matters because\n`anthropics/claude-code-action` refuses to run when the two differ \u2014 so a\nlong-lived base branch that has fallen behind silently blinds automated review for\n**every pull request based on it**, even though those pull requests change no\nworkflow file at all. The probe runs `git symbolic-ref`, `git rev-parse`, and two\n`git show` reads against the local object database: it **fetches nothing, spawns\nnothing, and creates no worktree**, and an unresolvable ref simply reports the\ncomparison as unavailable. It is **advisory only** \u2014 it is registered as a\ndoctor-only diagnostic and **never changes `doctor`\'s required-prerequisite exit\ncode**, whether it finds drift or cannot complete the comparison. Spawning a\nworker against a stale base prints the same advisory and **still spawns the\nworker**. Remedy and full diagnosis:\n`docs/claude/runbooks/claude-review-base-branch-drift.md`.\n\n### `drive-epic`\n\nThe one conductor entry point every Bridge surface names. Give it an epic key and\nit reads conductor readiness for your repository and routes to the single path\nyour project can actually run:\n\n```\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC>\n```\n\nYou are never asked to choose. Bridge currently has two conductors and a standing\nrule that they must never operate on the same epic \u2014 two transition authorities on\none epic wedge it permanently \u2014 so the choice is made structurally rather than by\njudgement. Readiness green routes to the v2 bootstrap below (pass `--plan-file`\nand `drive-epic` runs it for you); readiness not green prints the interactive\npilot instruction instead. If readiness is **unknown** \u2014 unreachable,\nunauthorized, or malformed \u2014 it escalates and prints no conductor invocation at\nall, because an unknown owner is not the same as a not-ready one. No branch,\nincluding every error path, ever offers you two paths.\n\n`drive-epic` owns no branch behavior of its own \u2014 it forwards `--feature-branch\n<name>`, `--into-base`, and `--attended` verbatim to `setup-epic`, which decides\nthe strategy and the posture (see below). With none of them, a multi-ticket epic\nruns on `epic/<KEY>`, unattended.\n\n**The composed bring-up is two-phase (BAPI-1102).** With `--plan-file` on a\nrepository whose readiness is missing only the runtime facts, one invocation runs,\nin order: the **control plane** (Bridge API server + reconciler, each gated on its\nown readiness), then `setup-epic` to create and approve the run, then **executor\nlanes scoped to that run** with `--epic-run-id <run>`, then the **dead-man\nobserver** last, then the ready banner. The order is forced: an executor lane must\ncarry an explicit claim scope, and the run it is scoped to does not exist until the\nserver is up and `setup-epic` has run.\n\nA composed plane serves **exactly one** run; start a second plane for a second\nepic. If `setup-epic` fails, the control plane is left running and **no executor\nlane is spawned**. If the lanes fail to become ready, the control plane and the\ncommitted run are both left intact and the observer is not started \u2014 nothing is\nrolled back, because a local process that failed to start says nothing about\nwhether the run is valid.\n\n"Plane ready" reports **processes**, not dispatchability: if the run cuts an epic\nbranch, the reconciler holds ticket dispatch until that branch\'s index scope\nfinishes preparing.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\n\n### `plane up` \u2014 claim scope is required\n\n`plane up` starts the runtime on its own, and every executor lane it starts needs\nan explicit claim scope:\n\n```\nnpx -y @bridge_gpt/mcp-server plane up --epic-run-id <run-id> # repeatable\nnpx -y @bridge_gpt/mcp-server plane up --repo-wide # or repository-wide\n```\n\nThe two are mutually exclusive. Asked to start lanes with neither, `plane up`\n**refuses before spawning anything**, in the executor\'s own words. That is not a\nnew restriction \u2014 an executor started with no claim scope has refused to start\nsince BAPI-1026 \u2014 it is that refusal moved to where it is actionable, instead of\nsurfacing as a member crash and a full plane rollback.\n\n`plane status` reports the lifecycle phase (control plane starting/ready, lanes\nstarting/ready, observer active, plane ready) and the epic run the plane is bound\nto. `plane down` handles every intermediate phase: it stops the bound run first,\nthen signals the recorded process group.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe 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.\n\n**The command above uses an epic branch.** For a plan with **two or more** nodes and\nno branch flag, `setup-epic` derives `epic/<KEY>`: every child-ticket PR targets that\nbranch, and one draft integration PR targets the repository base branch. This holds on\nevery path, CI and `--json` included. A single-node plan keeps the repository base\nbranch, matching plain `start-tickets`.\n\n> **Behavior change (BAPI-1009).** The same no-flag command previously ran children\n> directly into the repository base branch. **Pass `--into-base` to restore that**, if\n> your automation depends on it. Pass `--feature-branch <name>` to override the derived\n> name, or to force a branch for a single-node plan. Passing both is a parse error,\n> refused before any file read, credential resolution, or network call.\n\n**A run that selects an epic branch is created UNATTENDED (BAPI-1102).** Its\n`policy_json` carries `posture: "unattended"` and `v2_auto_merge_enabled: true`, so\nchild PRs merge into the epic branch without parking for a human at every gate. The\n**integration PR into the repository base branch stays human-gated.** The run\'s CI\ngate is stamped **server-side at first approval** \u2014 from your repository\'s declared\ndefault, or, when it declares none, as an explicit recorded `no_ci_gate` mode.\n\n> **Behavior change (BAPI-1102).** Pass `--attended` to restore the previous\n> composition: `base_branch` plus an explicitly requested `--review-policy`, with no\n> posture and no auto-merge authorization. `--into-base`, single-node plans, and\n> branch-silent invocations are **unchanged** \u2014 they send no `policy_json` at all,\n> byte for byte as before. A `--policy-file` still supersedes composition entirely;\n> combining `--attended` with a file that declares `posture: "unattended"` is a named\n> refusal rather than a silent winner.\n\nAn unattended run has two one-time **repository** prerequisites \u2014 consent\n(`unattended_epic_implementer_allowed`) and a verified notify webhook default \u2014 plus\nconfirmed default-branch review/CI workflows. Each is refused by name **before any\nrun row is created** (`target_not_allowed`, `webhook_unverified`,\n`repository_readiness_unconfirmed`), and `drive-epic` readiness and `doctor` both\nlist them with their current state. `notify.local_sink` does not satisfy the webhook\nprerequisite.\n\n> **Rollout note.** The client-side default and the server-side admission,\n> stamping, and readiness changes must be released as **one compatible unit**. The\n> new default\'s shape (`v2_auto_merge_enabled: true` with no `required_checks`) is\n> exactly what the create route refused before BAPI-1102, so shipping the client\n> half alone would 422 every unattended run at `POST /jira/epic-runs/runs`.\n> Deploying only the server half is safe and inert: it narrows two refusals and adds\n> a first-approval stamp, and **rewrites no existing run**. No database migration and\n> no new environment variable is introduced \u2014 `config_projects.unattended_epic_implementer_allowed`\n> and `epic_supervisor_setup.done_gate_config` already exist and are read as-is.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### `implement-epic`\n\nThe deterministic half of the `/conduct-epic` loop: it owns the epic branch, a\nversioned local checkpoint, a per-epic lock, and the read-only probes the loop\ndecides on. It never creates or mutates an `epic_run` \u2014 that is the server-side\nv2 reconciler\'s job, and `init` refuses to start when one is already active.\nThis is EPIC-LEVEL orchestration, distinct from the ticket-level\n`implement-ticket` command.\n\n```\nnpx -y @bridge_gpt/mcp-server implement-epic <verb> [flags]\n```\n\n**Verbs**\n\n| Verb | Flags |\n| --- | --- |\n| `init <EPIC>` | `--tickets K1,K2,\u2026` (required), `--base-branch <b>`, `--checkpoint-path <p>`, `--dry-run`, `--json` |\n| `status <EPIC>` | `--json` (required), `--checkpoint-path <p>` |\n| `checkpoint set <EPIC>` | `--ticket <KEY>` (required), `--field <name> <value>` (repeatable), `--journal "<line>"`, `--checkpoint-path <p>` |\n| `finish <EPIC>` | `--checkpoint-path <p>`, `--json` |\n| `spawn <EPIC>` | `--ticket <KEY>` and `--prompt-file <path>` (required), `--agent claude\\|cursor-agent`, `--checkpoint-path <p>`, `--json` |\n\n**Local state.** Everything lives *outside* the repository, under\n`~/.config/bridge/conduct/<repo>/` (honoring `XDG_CONFIG_HOME`), so it resolves\nidentically from the main checkout and from any worktree and can never be\ncommitted by an agent running `git add`:\n\n| Path | Purpose |\n| --- | --- |\n| `<EPIC>.json` | the version-1 checkpoint (file `0600`, directory `0700`) |\n| `<EPIC>.json.prev` | the previous valid checkpoint, retained on every write |\n| `<EPIC>.lock` | the per-epic lock |\n| `<EPIC>/prompts/<KEY>-<kind>-<n>.md` | prompt files the caller writes for `spawn` |\n\n`status` prints the resolved `checkpoint_path`. To unpark a run a human edits the\ncheckpoint (`needs_human` \u2192 `null`, plus the ticket\'s `status`/counters);\n`last_seen_head`, `ci_last_poll`, and `lock` are observational and are never\nhand-edited.\n\n**`init` runs ONE preflight** that reports *every* failure in a single pass and\nwrites nothing unless all of them pass: `gh auth status`; Worktrunk resolves\n(honoring `BAPI_WORKTRUNK_BIN`); Bridge credentials resolve; `auto_merge_enabled`\nis on \u2014 or is turned on by PUTting the *complete* effective config back with just\nthat flag flipped, which prints a line beginning `announced:`; at least one\nrequired CI check exists (an empty required set would make the done gate pass\nvacuously); no active server-side `epic_run` for the key; the lock is free or its\nowner is provably dead; the base branch exists on `origin` after `git fetch`; and\nthe indexed-branch override is either absent or this epic\'s own \u2014 a re-`init`\nafter a crash is accepted and its `original_base_branch` becomes the default base,\nwhile a *foreign* override is refused by name. `resolve-ci-checks` is called\nexactly once either way, because that call is what warms the `poll-ci-checks`\ncache the first `status` depends on. Only then does `init` push\n`epic/<EPIC>` to `origin` at the fetched base tip (no local checkout), repoint the\nindex, write the checkpoint, and take the lock. `--dry-run` prints the validated\nplan and mutates nothing. A second `init` refuses with `already initialized`.\n\n**Failure posture is split on purpose.** In `status`, each probe fails *open*: a\n`gh`, CI, review, or parse failure leaves that sub-object `null`, adds an entry to\n`probe_errors`, and the command still exits `0` with a complete object \u2014 the loop\nmust be able to read its own checkpoint during a GitHub outage. Everything else\nfails *closed*: a corrupt or wrong-version checkpoint makes every verb but `init`\nexit non-zero **without rewriting it**, and `checkpoint set`, `spawn`, and\n`finish` refuse a lock held by another live process. `status` never takes the lock.\n\n**Exit codes.** `0` on success \u2014 including a missing checkpoint\n(`checkpoint_exists: false`) and an idempotent second `finish`. Non-zero on any\nother failure, with a one-line reason on stderr. With `--json`, stdout is exactly\none JSON object carrying `ok`.\n\n**Credentials** resolve only from `BAPI_API_KEY` or the user-scoped\n`bapi:<repo>` credential target, travel only in the `X-API-Key` header, and never\nappear in a command argument, in stdout/stderr, or in a journal line.\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `epic-implementer install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**. (The event-ledger CLI was renamed from `conductor` to `epic-implementer`; the retained `conductor` bin name is now a fixed migration pointer.)\n\n> The v1 `epic-implementer epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `epic-implementer doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` (server, installer) | No | `https://bridgegpt-api.com` | Bridge API base URL. The MCP server and the `install` CLI both fall back to the production default |\n| `BAPI_BASE_URL` (`executor` subcommand) | **Yes** | _(none)_ | The `executor` deliberately has **no** production fallback \u2014 it refuses to start rather than guess a target |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `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 |\n| `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` \u2192 the connected client\'s MCP `roots/list` \u2192 `CLAUDE_PROJECT_DIR` \u2192 `process.cwd()`. Several paths *write* it into a generated registration (`--init`, host-config provisioning, the worktree `mcp-invoke` shim) \u2014 that is provenance, not a runtime default |\n| `SFCC_HOSTNAME` | No | _(none)_ | SFCC sandbox hostname. Part of the environment credential tier \u2014 `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` |\n| `SFCC_CLIENT_ID` | No | _(none)_ | Account Manager API client id. See `SFCC_HOSTNAME` \u2014 all three are needed together. Also required on its own when a tool is called with an explicit dotted `instance` |\n| `SFCC_CLIENT_SECRET` | No | _(none)_ | Account Manager API client secret. See `SFCC_HOSTNAME` \u2014 all three are needed together. Never sent to Bridge; it goes only to the Account Manager token endpoint |\n| `CLAUDE_CODE_OAUTH_TOKEN` | No | _(none)_ | The supported headless authentication input for conductor workers. Export it into the **executor process\'s own** environment; Bridge forwards it unchanged into the worker and stores it nowhere \u2014 no credential-store entry, no disk, never sent to Bridge. See [Claude login for conductor workers](#claude-login-for-conductor-workers) |\n| `BAPI_INSTALL_DEBUG` | No | _(unset)_ | Set to any non-empty value to unlock raw diagnostics in `install` and the `apply_install_manifest` path \u2014 the underlying error message and stack behind an `unexpected error` summary. The installer\'s own failure text tells you to set it |\n| `BAPI_SIGNUP_EMAIL` | No | _(none)_ | Selects the self-serve signup route without `--email`. Precedence: `--email` first, then this variable, then the visible interactive prompt |\n| `BAPI_INVITE` | No | _(none)_ | Invite code for `install`, for scripting. Like `--invite <code>`, it exposes the code to your shell history and the process list \u2014 prefer bare `--invite` and the hidden prompt |\n| `BAPI_PLANE_PYTHON` | No | `python` | Executable used for the Python members of `plane up`. Point it at a venv interpreter when `python` on `PATH` is not the one you want |\n| `BAPI_PLANE_UVICORN` | No | `uvicorn` | Executable used for the server member of `plane up` |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `BAPI_MCP_UPDATE_CHECK_ENABLED` | No | _(enabled)_ | MCP-local kill switch for the cached update check run at startup. Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip it entirely \u2014 no npm registry request, no update-cache read/write, no stderr advice, and no `tools/list` advisory decoration. Any other value is treated as enabled. Useful behind a firewall/registry-restricted network, and used by the payload-measurement harness so a stale local cache can never perturb a capture |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `estimation` (+ `estimate_epic`, which is **hidden by default** \u2014 `/estimate-epic` needs this group), `sfcc` (+ the 8 heavy SFCC read tools, `sfcc_log_query`, and the `sfcc_setup_status`/`check_permissions` diagnostics \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)), `sfcc-write` (+ the 9 destructive SFCC write tools \u2014 independent of `sfcc`, which does not enable them; see [Read and write profiles](#read-and-write-profiles)), and `full` (shortcut that expands to every group, **including `estimation` and `sfcc-write`**). Example: `sfcc,conductor`; use `sfcc,sfcc-write` for reads plus writes. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup, so **changing this variable requires restarting the MCP server**. That is separate from the learn/onboarding tools, which are gated per session and can appear mid-session without a restart (see [Learn/onboarding tool availability](#learnonboarding-tool-availability)). Conductor/epic sessions launched via `start-tickets --conductor` **merge** `conductor` into the parent process\'s already-resolved groups at the spawn boundary rather than replacing them \u2014 a project running on `sfcc` spawns workers on `core,sfcc,conductor`. A normal `start-tickets` run stays on `core`. |\n\nEnvironment values are **trimmed**, and only a non-empty result wins. A\nwhitespace-only `BAPI_API_KEY` therefore does not override anything: it falls\nthrough to credential-store resolution exactly as an unset variable would.\n\n## Learn/onboarding tool availability\n\nThree tools are **gated per session** rather than by profile:\n\n- `apply_install_manifest`\n- `invite_member`\n- `regenerate_directory_map`\n\nThey are registered in the `core` profile like any other core tool, but the\nserver starts them **disabled**, so a client that honors mid-session tool-list\nchanges does not see them in its first `tools/list`. They are revealed \u2014 all\nthree together, once per session \u2014 by either of two successful requests:\n\n1. a successful **`get_install_manifest`** call, or\n2. a **`get_pipeline_recipe`** request for the **`learn-repository`** pipeline.\n\nOn reveal the server emits `notifications/tools/list_changed`, so a client that\nhonors it picks the three tools up **without a restart**.\n\n`get_install_manifest` and `persist_routing_credential` stay ungated in `core` at\nall times, so the install and `start-tickets` flows always have their entry\npoints. Only the first of those two is a trigger \u2014 `persist_routing_credential`\nnever reveals anything; it is listed here because `/start-tickets` calls it and\nit therefore must never be gated.\n\n**Unrecognized clients get all three at startup.** The hide-then-reveal behavior\napplies only to clients verified to honor mid-session `tools/list_changed`. Any\nother client \u2014 including one that sends no name, a blank name, or a name the\nserver does not recognize \u2014 is enabled at session initialization instead and\ntherefore sees all three tools in its **first** `tools/list`. This is deliberate:\nthe failure mode of guessing wrong should be an extra tool, never a permanently\nmissing one.\n\n**A failed request changes nothing.** Only a genuinely successful manifest\nreveals the tools \u2014 the response must parse as JSON and carry a nonblank\n`snapshot_token`. An error response, a non-JSON body, or a manifest without a\ntoken leaves the three tools hidden, and the response you receive is unchanged by\nthe check either way. Likewise, only the exact `learn-repository` pipeline is a\ntrigger; other recipes and failed lookups reveal nothing.\n\n**Capability filtering still applies independently.** Revealing these tools does\nnot override the backend capability gate described below. The effective surface\nis the *intersection* of the two, so a tool can be enabled for the session and\nstill be absent from `tools/list` because this repository lacks the integration\nit needs. That is expected behavior, not a bug \u2014 check the integration first.\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n### Claude login for conductor workers\n\nConductor workers are **not** isolated into a private Claude configuration\ndirectory \u2014 they run with the executor host\'s own `HOME`, so a worker\nauthenticates the same way any interactive `claude` invocation on that host\ndoes. The prerequisite is simple: run\n\n```bash\nclaude login\n```\n\non the executor host, once, the normal way. Bridge never stores, resolves, mints,\nrotates, validates, or diagnoses this credential \u2014 it is entirely the operator\'s\nown Claude CLI state, exactly as if you were running `claude` at the terminal\nyourself.\n\n**Headless hosts.** If the executor host has no interactive login session\navailable (a service-launched executor, a CI-style runner), export\n`CLAUDE_CODE_OAUTH_TOKEN` into the **executor process\'s own environment**\nyourself before starting it:\n\n```bash\nexport CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)" # run once, wherever you can browser-login\n```\n\nBridge forwards that value **unchanged**, byte-for-byte, into the direct worker\nprocess environment \u2014 nothing else. It is never written to disk, never placed in\na generated launchd/systemd service unit, never placed in project configuration\n(`.mcp.json` / `.cursor/mcp.json`), and never sent to Bridge servers. There is no\ncredential store entry for it and no lifecycle tracking: expiry, rotation, and\nvalidity are entirely the operator\'s own responsibility, the same as any other\nvalue you choose to export into a process environment.\n\n**`ANTHROPIC_API_KEY` is never forwarded to a worker**, under any circumstance \u2014\nthere is no fallback path for it.\n\n`mcp-server doctor` reports a single advisory **Claude login** line \u2014 whether\n`~/.claude.json` on the host it runs on carries a login marker. This is\ninformational only: it cannot confirm the next worker spawn will authenticate,\nand it never blocks the doctor run or changes its exit code.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe authoritative tool catalog covers **75 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).\n\n- **Connectivity & identity** \u2014 `ping` (its JSON also carries `docs_dir`, `role`, and `customer_type`)\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/architecture/fsd/prd, where `architecture` is an alias of `tdd`), `request_ticket_review` (writes both `get_clarifying_questions` and `get_ticket_critique`), `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Bounded waits** \u2014 `wait_for_ticket_review`, `wait_for_ci_checks`. Each call waits at most 240 s and returns `state: "ready" | "pending" | "error"`; `pending` is a normal, resumable result you call again on, never a failure. They exist so an agent can wait without a foreground `sleep` and without binding a long server-side operation to the 900 s MCP client deadline. `wait_for_ci_checks` reports only that checks are **terminal** \u2014 deciding pass or fail stays with the caller.\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`, `get_ticket_state_tree` (live repo-wide lifecycle + dependency tree; read-only, no mutation parameter)\n- **Jira status** \u2014 `update_jira_status` (pass `status: "auto"` to resolve the configured post-PR status server-side)\n- **Repository & CI** \u2014 `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)\n- **Pipelines & automation** \u2014 `get_pipeline_recipe` (returns a fully resolved recipe the agent executes step-by-step)\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `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` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `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>"` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`get_pipeline_recipe` returns a fully resolved recipe: an ordered `steps` array\nplus `total_steps`, `auto_approve`, and `execution_mode`. Each step is either an\n`mcp_call` (a tool name with resolved params) or an `agent_task` (instruction\ntext). `execution_mode` is `inline` \u2014 the calling agent executes every step\nitself and keeps its own durable record through `record_phase_result` /\n`record_checkpoint`. There is no server-side orchestrator and no run to resume.\n';import{readdir,readFile}from"fs/promises";import path from"path";var EXECUTION_MODE_VARIABLE="execution_mode";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),executionMode=options?.executionMode??"inline";variables={...variables,[EXECUTION_MODE_VARIABLE]:executionMode};let resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content,base.instruction_file=step.instruction_file}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Recipes are re-entrant: a recovery run may begin again at step 1, and a step whose tool reports a server-side reuse (e.g. reused: true) has completed successfully \u2014 treat that as the expected fast path, not a failure, and continue. A step can succeed (no on_error handling applies) while its own returned content is a JSON envelope shaped like error: "GATEWAY_TIMEOUT", status: 504, and a recovery_get field \u2014 recognize that shape and read it as "server-side processing may still be running", not as a failure or an invitation to retry. Poll the named retrieval tool (or the recovery_get URL) with the same artifact identifier until a terminal response is reached, and never reissue the original request tool. Only fall back to on_error handling if that retrieval itself terminally fails. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,execution_mode:executionMode,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile(path.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile(path.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
7874
+ `,"phase1/capture-review-decisions.md":'Capture user decisions on review findings for {ticket_key} using the HTML decision page, then interpretively rewrite the clarifying questions and critique docs and upload both to Jira.\n\n<!-- BAPI-977 Phase-1 variant of capture-review-decisions.md. Selected only when\n REVIEW_SYNTHESIS_PHASE1_ENABLED is on. The legacy instruction at\n instructions/capture-review-decisions.md is the disabled path and is unchanged. -->\n\n## Step 1: Read source documents\n\nRead the combined review-and-resolution file:\n- `{docs_dir}/review/{ticket_key}-review-and-resolution.md`\n\nIf the file does not exist or is unreadable, stop and report: "Combined review-and-resolution file not found or unreadable. Run the earlier pipeline steps first."\n\nThe combined file existing but containing no actionable items (empty `Needs Scrutiny` and `Open Questions` sections) is **not** a failure condition \u2014 Step 4 handles the no-decisions-needed flow gracefully when `generate_decision_page` is called with empty `actionable_items`.\n\n## Step 2: Map evaluation items to decision page input\n\nTransform the combined review-and-resolution document into `generate_decision_page` JSON input using these mapping rules:\n\n| Evaluation Section | JSON Field | Mapping Rule |\n|---|---|---|\n| Open Questions (full actionable cards) | `actionable_items` | E-item title \u2192 `question`, `**Source**` \u2192 `source`, `**Original question**` \u2192 `original_question`, `**Why it matters**` \u2192 `why_it_matters`, decision tree branch labels \u2192 `options` (string array, labels only), `**Option consequences**` (parallel to branches) \u2192 `option_consequences`, `**Recommendation explanation**` \u2192 `recommendation_explanation`, `**Assessment**` paragraph plus the item\'s single `**Canonical citation**` \u2192 `codebase_evidence`, `**Recommendation Index**` \u2192 `recommendation_index` |\n| Needs Scrutiny (full actionable cards) | `actionable_items` | Same mapping as Open Questions above |\n| Confirmed Improvements (terse rows) | `clear_improvements` | E-item id \u2192 `id`, E-item title \u2192 `title`, recommended action \u2192 `action`, `Confidence` \u2192 `confidence`, `Source` \u2192 `source` |\n\n### `clear_improvements` carries exactly five fields\n\nA terse Confirmed Improvement row maps to a `clear_improvements` entry containing **only** these five fields, every one of them a non-empty string:\n\n- `id`\n- `title`\n- `action`\n- `confidence`\n- `source`\n\n**Never add anything else to a `clear_improvements` entry.** In particular, do not add `options`, `option_consequences`, `recommendation_index`, `recommendation_explanation`, `question`, `original_question`, `why_it_matters`, or `codebase_evidence` \u2014 not as real values, not as empty strings, not as empty arrays, and not as placeholders. Those fields belong to the actionable-card contract, and a confirmed improvement is not a decision the reviewer is being asked to make. A terse row that seems to need one of them was misclassified upstream; map it as it stands rather than inventing the missing decision data.\n\n### Only full decision cards map to `actionable_items`\n\nAn entry in `actionable_items` must carry the complete contract: `id`, `question`, `why_it_matters`, `recommendation_explanation`, `options` (2\u20134 labels), `option_consequences` (same length as `options`, in the same order), and a `recommendation_index` that is a 0-based integer within range of `options`. `original_question`, `codebase_evidence`, and `source` are optional display fields.\n\nDo not promote a terse Confirmed Improvement row into `actionable_items` by synthesizing branches, consequences, or a recommendation index for it. If the evaluation did not emit a decision tree for an item, that item is not an actionable decision.\n\n### `codebase_evidence` uses the single canonical citation\n\nMap the item\'s `**Assessment**` paragraph together with its one `**Canonical citation**` into `codebase_evidence`. There is exactly one citation per item; do not reconstruct a citation list, do not re-derive citations from the source documents, and do not repeat the canonical citation elsewhere in the payload.\n\n### Ignore the machine-only silent-validation records\n\nThe combined document carries a `**Silent-agreement accounting**` block (`silent_validated`, `silent_accepted`, `silent_demoted`) and a `<!-- silent-validation-ledger ... -->` HTML comment. Both are machine-only records of work already completed. **Ignore them entirely when building both decision-page arrays.** A silently accepted item is not a user decision and never becomes an `actionable_items` entry or a `clear_improvements` entry. A demoted item is already present as its own visible E-item; map that visible item, not the ledger line that references it.\n\n### Item ids\n\nPreserve the E-item id when mapping an actionable card (`E-5` \u2192 `id: "E-5"`). For terse Confirmed Improvement rows, use the E-item id the evaluation assigned; visible E-numbering is sequential and unique across the whole document, so those ids are already stable and distinct.\n\nFor each actionable item, the `options` array is a list of plain label strings extracted from the combined file\'s decision tree branches. The tool auto-generates value keys (`opt-0`, `opt-1`, etc.) and auto-appends a "None of these" option. Do not generate value keys yourself.\n\n## Step 2.5: Auto-approve fast path\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\nIf `auto_approve` is `true` and Step 2 produced at least one actionable item, skip Steps 3\u20136 entirely and synthesize the commit JSON directly:\n\n- `ticket_key`: `{ticket_key}`\n- `general_comment`: `""`\n- `decisions`: an object keyed by each `actionable_items[*].id` from Step 2\'s mapped input. For each item:\n - If `recommendation_index` is a non-negative integer within range of `options`: `choice = "opt-" + recommendation_index`, `chosen_label = options[recommendation_index]`, `comment = ""`, `source` copied from the item.\n - Otherwise (missing, null, or out of range): `choice = "opt-0"`, `chosen_label = options[0]`, `comment = ""`, `source` copied. Never emit `"none"` and never emit `"ask"`.\n\nPost a single chat acknowledgement listing each auto-approved item ID and chosen label, then proceed directly to Step 7 with the synthesized JSON. Step 7\'s "Hard rule" about resolving `ask` items does not apply because no item carries `choice === "ask"`.\n\nThe synthesized settled decisions still receive the implications review described under Step 6\'s "Implications review and proceed gate" before Step 7 runs \u2014 literal `auto_approve = true` only skips the human proceed gate, not the review itself. "Skip Steps 3\u20136" above means skipping their interactive portions (rendering the page, waiting on chat, the Q&A loop); it does not exempt this fast path from the review obligation.\n\nIf Step 2 produced zero actionable items, fall through to Step 3 \u2014 Step 4\'s existing `no_decisions_needed` branch handles the empty case correctly.\n\nOtherwise (any value of `auto_approve` other than the literal `true` \u2014 including empty, `false`, or missing), proceed to Step 3.\n\n## Step 3: Call the MCP tool\n\nCall `generate_decision_page` with `ticket_key` at the root and the review arrays nested under `content`:\n\n**Always pass `content`, even when both arrays are empty.** Send `"content": { "actionable_items": [], "clear_improvements": [] }` rather than omitting the key \u2014 that is what reaches the `no_decisions_needed` branch Step 2 relies on. Omitting `content` entirely is rejected with a `VALIDATION_ERROR`, because root-level arrays are silently dropped by the tool\'s lean input schema and a missing wrapper is far more often a mistake than a deliberate empty call.\n\n```typescript\ninterface ReviewDecisionsContent {\n actionable_items?: Array<{\n id: string;\n question: string;\n why_it_matters: string; // required \u2014 concrete one-sentence impact\n recommendation_explanation: string; // required \u2014 why the recommended branch is best\n options: string[]; // 2-4 option labels\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based index into options\n original_question?: string; // optional display field\n codebase_evidence?: string; // optional display field \u2014 assessment + the one canonical citation\n source?: string; // optional source reference\n }>;\n clear_improvements?: Array<{\n id: string;\n title: string;\n action: string;\n confidence: string;\n source: string; // required for clear_improvements\n }>; // exactly these five fields \u2014 nothing else\n}\n```\n\nExample call:\n```json\n{\n "ticket_key": "{ticket_key}",\n "content": {\n "actionable_items": [\n {\n "id": "E-1",\n "question": "Should we add a configurable timeout?",\n "why_it_matters": "Timeout behavior affects retry paths and user-visible latency.",\n "recommendation_explanation": "Configurable matches existing latency-branching code.",\n "options": ["Keep existing", "Add configurable timeout"],\n "option_consequences": ["No new work.", "Implementers add config + tests."],\n "recommendation_index": 1,\n "original_question": "Does the ticket specify timeout behavior?",\n "codebase_evidence": "The retry path already branches on latency. src/python/llms/ai_client.py:212",\n "source": "Clarifying Q1"\n }\n ],\n "clear_improvements": [\n { "id": "E-2", "title": "Tidy logging", "action": "Use the module logger.", "confidence": "High", "source": "Clarifying Q4 (prior round, concurrence inline)" }\n ]\n }\n}\n```\n\n## Step 4: Check tool response\n\nThe tool returns a JSON response with a `status` field:\n- If `status` is `"no_decisions_needed"`: skip Steps 5, 6, 7, and 8 entirely. Output a success message: "No actionable review decisions needed \u2014 skipping doc rewrite and upload." This covers both the case where every item was confirmed as a Confirmed Improvement and the case where no items were emitted (e.g., `saved_legs` was empty in the preceding wait result).\n- If `status` is `"decision_page_generated"`: continue to Step 5. The response includes `file_path`.\n\n## Step 5: Direct user to the decision page\n\nTell the user to open the generated HTML file in their browser. Provide the `file_path` from the tool response. Then say to the user, verbatim: `Open the page. For any item you\'re unsure about, choose "Ask about this" \u2014 when you submit, I\'ll talk through those before we proceed. You can also ask me questions in chat before submitting if you prefer.`\n\nThis step only directs the user to the page and explains the two allowed next actions (submit selections, or ask questions first). Do not describe Step 7\'s rewrite semantics here; that belongs to the rewrite step.\n\n## Step 6: Q&A loop and commit signal\n\nEnter an open-ended Q&A loop. There is no turn cap \u2014 the user may ask any number of questions in any number of turns. Do not stop and wait silently; engage with each user message as either a commit signal or a discussion turn.\n\n### Proceed signal (commit)\n\nTrim the full user message and attempt to parse the entire trimmed message as JSON. The message is a commit only when the parsed value is an object with all three of these top-level fields:\n\n- `ticket_key` \u2014 must be a string\n- `decisions` \u2014 must be an object\n- `general_comment` \u2014 must be a string\n\nThe first valid commit-shaped JSON paste commits immediately. Proceed to Step 7 without prompting for additional confirmation. Any combination of `decisions` keys is accepted (the page may submit a partial set if the user only resolved some items conversationally). Do not over-validate the per-card fields beyond the top-level commit-shape check \u2014 the page guarantees the per-card schema, and over-validating risks rejecting valid pastes if the page schema evolves.\n\n### Discussion signal (Q&A turn)\n\nAnything that is not commit-shaped JSON is a discussion turn. This includes:\n\n- Freeform questions (with or without other text).\n- Questions pasted alongside other text or alongside JSON.\n- Malformed JSON (parse failure).\n- Well-formed JSON missing one or more of the required top-level keys (`ticket_key`, `decisions`, `general_comment`).\n\nFor JSON-shaped input that is missing required top-level fields, call this out in the reply \u2014 explain which fields are missing and ask whether the user intended to submit or share partial state \u2014 rather than silently treating it as a freeform question.\n\nAnswer discussion turns using these sources, in priority order:\n\n1. The combined `{ticket_key}-review-and-resolution.md` file already read in Step 1.\n2. The original source document(s) for whichever leg(s) are in the preceding wait\'s `saved_legs` (BAPI-1121) \u2014 `{ticket_key}-clarifying-questions.md` and/or `{ticket_key}-ticket-quality-critique.md`. Never read a leg in `withheld_legs` or `absent_legs`, even if a file happens to exist at its canonical path.\n3. Codebase lookups when the question requires verifying current code state.\n\nIf a discussion turn asks about an item the reviewer cannot find in the visible sections, the silent-validation ledger is a legitimate place to *look it up* \u2014 it records which prior items were silently accepted and what grounded each one. Answer from it in chat when asked. It still never becomes a decision-page entry or a rewrite target.\n\nFallback: if running on a pre-PR1 branch where the combined review-and-resolution document does not exist, use the pre-PR1 `{ticket_key}-review-evaluation.md` and `{ticket_key}-resolution-guide.md` pair in its place.\n\nFor plain freeform questions, infer the item from chat context when possible.\n\n### In-flight decision state\n\nDuring the Q&A loop, maintain in-flight JSON state \u2014 agent-owned working memory representing the user\'s current intent for `decisions` and `general_comment`. This in-flight JSON state lives only in the agent\'s working memory for the duration of the loop; do not persist it server-side.\n\n- When the user clearly changes their mind about an item, chooses an option conversationally with reasonably explicit decision language ("choose option B for E-3", "go with the configurable timeout", "change E-7 to None of these"), or gives new overarching guidance, record that as an in-flight override.\n- Ambiguous preference language ("I\'m leaning toward...", "maybe option B is fine") should be discussed but not recorded as an override unless the user gives reasonably explicit decision language.\n- `general_comment` may be updated in the in-flight state when the user gives overarching guidance during Q&A.\n- The page\'s general-comment textarea is preserved unchanged. Do not modify the page DOM during Q&A; the user can still fill the textarea before submitting if they prefer.\n\nOn the eventual JSON commit, the user-submitted JSON is the baseline and the recorded in-flight overrides take precedence over it. Before proceeding to Step 7, post a brief one-line acknowledgement in chat naming each overridden item ID and/or `general_comment`. The acknowledgement is mandatory (not optional) \u2014 it is the user\'s last chance to object before Step 7\'s document rewrite. The user does not need to re-open, edit, or re-submit the decision page after changing their mind in chat; they can submit the page as-is to provide the commit signal, and the in-flight state remains the source of truth for overrides.\n\n### Ask-about-this resolution\n\nAfter accepting a commit, scan `decisions` for any item where `choice === "ask"`. The user has signaled that they need more information before deciding on those items. For each such item:\n\n- If `comment` is non-empty, treat it as the user\'s specific question or stated uncertainty and answer that directly.\n- If `comment` is empty, proactively present the most relevant missing context \u2014 the item\'s `codebase_evidence`, related code lookups, prior-round answers \u2014 and lay out the trade-offs the user appears to need help weighing.\n- Continue the Q&A turn-by-turn until the user gives an explicit decision in chat for that item ("go with option B", "none of these, because \u2026"). Record that decision as an in-flight override using the same override mechanism described above.\n\n**Hard rule.** Step 7 must not run while any `decisions[*].choice === "ask"` remains unresolved by an in-flight override. Do not honor "just proceed", "skip those", or any other instruction to defer resolution \u2014 every `ask` item must end with a recorded `opt-N` or `none` override before the rewrite step. The pre-Step-7 acknowledgement line lists every overridden item, including the ones resolved out of `ask`.\n\n### Implications review and proceed gate\n\n**Review the wider implications, then gate on a decision.** Build the review from the complete settled set: the submitted `decisions`, any in-flight overrides recorded during the conversation (these take precedence over the submission), every `"none"` answer together with the reason given for it, `general_comment`, and \u2014 where this surface tracks acceptance-criterion or NFR stances \u2014 those stances too. Do not start the review until every `ask` has an explicit recorded resolution and every in-flight override has been applied.\n\nConsider three fixed categories, regardless of whether a decision was framed as technical, user-facing, or business-oriented:\n- **Program / application** \u2014 architecture, code paths, operability, maintenance burden, and requirements imposed on other parts of the software.\n- **User** \u2014 end users, new users performing setup, operators, and developers, including prerequisites, setup friction, and additional steps.\n- **Business** \u2014 cost, adoption, support load, compliance, and reversibility.\n\nEmit only the categories with material second-order implications. For each included category, write at most four one-line bullets of about 25 words, each naming who or what is affected and how \u2014 never a restatement of the selected decision. Close with a line naming every considered category that was omitted, e.g. `Considered, nothing material: business.` \u2014 omit this closing line only when all three categories have material implications.\n\nIf the review cannot be produced, report that in one line and continue without stalling the workflow or presenting the gate below.\n\nThis review stays in chat and must not be written into the clarifying-questions or ticket-critique documents rewritten in Step 7.\n\nThen present the gate, verbatim: `Implications reviewed. Proceed, or name a decision to revisit.` Accept only a normalized `proceed`, `yes`, `y`, or `go` as a continuation token. Any other response names a decision to reopen: re-settle it in chat, record the new override, rerun the entire implications review against the changed settled set, and present the gate again.\n\nLiteral `auto_approve = true` emits the review but skips this gate entirely; a missing or non-true `auto_approve` value follows the human-in-the-loop path above.\n\nA decision named at this gate is re-settled in chat and recorded as an in-flight override using the same override mechanism as the rest of this step; the rerun review picks it up, and the pre-Step-7 acknowledgement line above also names it. Step 7 must not begin until every submitted `ask` is resolved (per the hard rule above) **and** this gate has accepted a proceed token \u2014 except when the review fails open or `auto_approve` is literal `true`.\n\n## Step 7: Interpretively rewrite source documents\n\nThe pasted JSON contains a `decisions` object keyed by item ID. Each decision includes `source`, `choice`, `chosen_label`, and `comment`. **Manifest authority (BAPI-1121)**: read the source document for a leg ONLY when that leg is in the preceding `wait_for_ticket_review` result\'s `saved_legs` \u2014 a canonical file existing on disk is a content LOCATION, never independent evidence that this review delivered it, and a leg in `withheld_legs` or `absent_legs` must not be read even if a file is present at its path. Define **generation-proven delivery** as the intersection of `saved_legs` and `advanced_legs` when `freshness_basis` is `"generation"` \u2014 the normal case; define **terminality-only fallback delivery** as `saved_legs` alone when `freshness_basis` is `"terminality_only"`, and treat that case as freshness-unproven even though it was saved (the identity-footer agreement check below is what backstops it). Canonical files are content locations and never independently establish delivery or current-review provenance on their own \u2014 the manifest is what does:\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` \u2014 when `clarify` is in `saved_legs`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` \u2014 when `critique` is in `saved_legs`\n\n**Write the rewrite to a DIFFERENT file; never overwrite the ones above** (BAPI-1097):\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions-reviewed.md`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique-reviewed.md`\n\nThose two source files are the **canonical** artifacts: their bytes are what the\ngeneration identity attests, and they are what the carry-through monitor and the\nhash verification read. The `-reviewed.md` files are the **human-facing** copies \u2014\nthe same findings, cleaned and read as if the decisions had always been made. Both\nsurvive; a consumer chooses between them deliberately rather than by reading\nwhichever one it finds.\n\nRewrite only the leg(s) in `saved_legs` and skip any leg not in it. A cost-saved review resolves zero critique rounds for an Easy ticket and ships clarifying questions alone, so critique appears in `absent_legs` rather than `saved_legs` \u2014 that is not an error. A leg in `withheld_legs` (delivered but not proven current \u2014 see the wait result\'s `freshness_basis` and per-leg `reason`) is likewise never rewritten; its canonical file was already removed by the wait, so there is nothing to read even if you tried.\n\n**Only submitted actionable decisions produce rewrites.** A silently accepted item generates no rewrite operation. Its presence in the machine-only ledger records that it was validated, not that a reviewer decided anything about it, and rewriting a document because of a ledger line would put a change into the source docs that no human ever approved.\n\nAfter a second-opinion run, each document has this shape:\n\n- A top-level H1 (`# Ticket Analysis` or `# Ticket Quality Critique`) followed by an italic provider-attribution line `_This analysis was generated by GPT|Claude|Gemini._` naming the first-round LLM family. **Preserve this attribution line verbatim** \u2014 do not move, edit, or remove it during the rewrite step.\n- The first-round questions / critique items, exactly as written by the first-round model.\n- **Inline second-opinion blockquotes** (`> **Second opinion (<provider>) - concurrence|refinement|disagreement.** ...`) nested directly under each prior item the second round addressed. The `(<provider>)` parenthetical is the second-round LLM family (`GPT|Claude|Gemini`). A `concurrence` blockquote may legitimately carry no body and no citations \u2014 that is a valid pure concurrence, not a malformed blockquote. Items the second round did not comment on have no blockquote at all \u2014 that is the silent-agreement signal.\n- A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 there is a second italic attribution line `_These additional points were raised by GPT|Claude|Gemini._` naming the second-round family \u2014 **also preserve this verbatim**. Then agent-specific sub-headings:\n - Clarifier docs: `### New Requirements Questions` / `### New Technical Questions` (numbering continues from the prior section).\n - Critique docs: `### New Requested Changes` / `### New Points to Consider` (numbering continues from the prior section).\n- A final **`## Second Opinion Summary`** footer. **This footer must be preserved verbatim** \u2014 under Phase 1 it is generated by the server from execution metadata and is the canonical record that the second round ran. Do not edit it, do not regenerate it, and do not remove it, even when individual items it accounts for have been resolved.\n\nThe `source` field on each decision tells you where the item lives:\n\n- `Clarifying Q3 (prior round, silent agreement \u2014 demoted)` \u2192 the prior section, no inline blockquote. Rewrite the prior item\'s answer.\n- `Clarifying Q9 (prior round, concurrence inline)` \u2192 the prior section, prior item carries an explicit `concurrence` blockquote (possibly an empty one). Rewrite the prior answer; the blockquote can be removed once the answer absorbs the resolution.\n- `Clarifying Q3 (prior round, refinement inline)` / `(prior round, disagreement inline)` \u2192 the prior section, prior item carries an explicit `refinement` or `disagreement` blockquote. Rewrite the prior answer to reconcile the dispute, then handle the blockquote per the rule below.\n- `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` \u2192 the `## New in Second Opinion > ### New Requirements Questions` sub-section. Rewrite the item in place inside that sub-section, not at the top of the prior analysis.\n- Equivalent forms for critique items: `Critique: Requested Change 2 (prior round, refinement inline)`, `Critique: Points to Consider N+1 (new in second opinion \u2192 New Points to Consider)`, etc.\n\n**Legacy fallback shape**: if the document instead ends with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section (because the JSON pipeline fell back), apply decisions to the equivalent location: `### Response to Prior Items` for inline-style responses, `### Additional Points > New X` for tail-style new items. Preserve the `\\n\\n---\\n\\n` separator and the `## Second Opinion` heading verbatim.\n\nApply the decision to the item in its home location. Then apply the decision:\n\n### Actionable item decisions\n\n- **Selected option** (`choice` is `opt-N`): Add `**Review Decision**: Accepted. <chosen_label>.` to the corresponding section. Integrate the selected direction into the section text so it reads as a final recommendation or resolved answer.\n- **None of these** (`choice` is `none`): Add `**Review Decision**: Rejected \u2014 none of the proposed options accepted.` Include the user\'s `comment` explaining why. Rewrite the section to reflect this decision.\n\nFor actionable items sourced from clarifying questions, rewrite the question\'s best-guess answer so it reads as the final resolved direction chosen by the reviewer. Do not leave the item framed as an unresolved accept/reject/modify prompt.\n\nFor items sourced from `(prior round, refinement inline)` or `(prior round, disagreement inline)` \u2014 disputes of a prior-round item carried in an inline blockquote \u2014 the prior-round item is the canonical home: rewrite its answer to absorb the resolution. Then handle the blockquote in one of two ways: (a) remove the blockquote outright if the rewritten answer fully absorbs the second-opinion content, or (b) shorten the blockquote to a single sentence noting the resolution while preserving the `(<provider>)` attribution (e.g. `> **Second opinion (Claude) - refinement.** Resolved by reviewer decision E-N.`). The strongest grounding reference from the original blockquote may be promoted into the rewritten prior-item answer if useful \u2014 keep one.\n\nFor items sourced from `(new in second opinion \u2192 ...)` \u2014 gap-captured items that received a decision \u2014 rewrite the item in place inside its tail-block sub-section (`## New in Second Opinion > ### New X`), not at the top of the prior analysis. Preserve the sub-section heading and continued numbering.\n\n### General comment handling\n\nTreat `general_comment` as overarching guidance that informs the tone and direction of both document rewrites. If it contains specific actionable feedback, weave it into the relevant sections. If it is broad or general, use it as context for how the rewrites should read. Do not create a separate "General Comment" or "Reviewer Notes" section \u2014 the goal is "final draft" form.\n\n### Rewrite principles\n\nThe goal is a **final draft** \u2014 the documents should read as if they were written with the decisions already made. Do not mechanically append decisions. Instead, lightly rewrite affected sections so they reflect the decisions naturally. Preserve all non-affected sections unchanged. The prior-round content should still read as coherent standalone analysis after integration. Preserve the `## New in Second Opinion` tail block intact for any items that weren\'t decided. **Always preserve the `## Second Opinion Summary` footer verbatim** \u2014 it is the server-generated record of the second round\'s execution and should not be edited even when individual items are resolved. **Do NOT carry the `bridge-review-decision-identity` footer into the rewritten copy.** That footer attests a SHA-256 taken over the delivered bytes, so leaving it on a rewritten document produces an attestation of bytes that no longer exist (BAPI-1097). Read the identity out of the untouched canonical document \u2014 Step 7.5 still needs it \u2014 and write the rewrite to its own file with no footer at all. The upload route strips a footer that arrives anyway and refuses a document carrying more than one. **Preserve every `<!-- bapi-item:... -->` marker exactly**: the route requires the rewritten leg\'s marker set to equal that leg\'s manifest ids, and refuses the upload otherwise.\n\n## Step 7.5: Record the decisions durably (AUTHORITATIVE)\n\n**This step, not the document rewrite, is what makes the decisions durable.** The\n`**Review Decision**:` markers Step 7 stamps into the two documents are a\nhuman-readable record only. Those documents are stored with `url=\'ai-generated\'`,\nand BAPI-738 deliberately excludes such rows from the description plan generation\nreads \u2014 so a decision that exists only as prose reaches nothing downstream. The\nstructured write below is the authoritative one; the prose is secondary.\n\n### Build one resolution per manifest id\n\nEvery critique finding and every clarifying question carries a stable id as an\nHTML comment on its item line: `<!-- bapi-item:crit-001 -->`,\n`<!-- bapi-item:clar-004 -->`. Preserve these markers through the Step 7 rewrite \u2014\nnever delete, renumber, or invent one.\n\nProduce **exactly one** resolution for **every** id present in the saved-leg\ndocuments, not just the ones the reviewer chose an option for. A cost-saved review\nsaves only the legs it ran \u2014 an Easy ticket resolves zero critique rounds and\nships clarifying questions alone \u2014 so build resolutions for the ids in whichever\ndocuments are in `saved_legs`. An id you omit is indistinguishable from\na decision that was lost, which is the exact failure this record exists to prevent.\nThe server refuses a payload with a missing, duplicate, or unknown id.\n\nMap each settled decision onto the id of the item it resolves:\n\n| Settled decision | `adjudication` | `effect` | `binding_instruction` |\n| --- | --- | --- | --- |\n| An option was chosen (`choice` is `opt-N`) and it obliges a change | `accepted` | `apply` | the ratified direction, non-empty |\n| Resolved, but nothing is to be done about it | `accepted` | `dismiss` | `null` |\n| None of these (`choice` is `none`) | `rejected` | `dismiss` | `null` |\n\nAn accepted dismissal is exactly `{adjudication: "accepted", effect: "dismiss",\nbinding_instruction: null}`. It means "this item is settled and requires no\nwork" \u2014 which is a different fact from "this item was declined", and the two must\nnot be collapsed onto one another.\n\n**Binding corrections go in `binding_instruction`, never in prose alone.** When\nthe reviewer\'s answer obliges an implementation to do something specific, that\nobligation belongs in this field on that item\'s id, even when the conversation\nproduced no `**Review Decision**:` marker for it. An unmarked prose correction is\nnot an independent requirement source and will not survive.\n\n### Read the generation identity from the saved-leg documents\n\nEvery saved review document carries one metadata footer naming the decision\ngeneration it belongs to:\n\n```\n<!-- bridge-review-decision-identity: {"generation":7,"source_bundle_sha256":"<64 lowercase hex>"} -->\n```\n\nRead it from whichever of these documents is in the preceding wait result\'s\n`saved_legs` (BAPI-1121) \u2014 **never** from a leg in `withheld_legs` or `absent_legs`,\neven if a file happens to exist at its canonical path:\n\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` \u2014 when `critique` is in `saved_legs`\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` \u2014 when `clarify` is in `saved_legs`\n\nA cost-saved review legitimately saves only one leg \u2014 an Easy ticket resolves zero\ncritique rounds and ships clarifying questions alone, so `critique` is in\n`absent_legs` rather than `saved_legs`. Read the footer from each document in\n`saved_legs`, and do not treat a leg absent from `saved_legs` as a failure.\n\nEvery saved-leg document\'s footer must be present and must agree \u2014 the same\npositive `generation` and the same 64-character lowercase-hex\n`source_bundle_sha256`. If a footer is missing or malformed on a document IN\n`saved_legs`, or two saved-leg documents disagree, **stop and report it**. Do not\nguess a generation, do not send one document\'s identity on behalf of a document\nthat carries none, and do not ask the server which generation is current. The\nvalue\'s entire worth is that it came from the documents you actually adjudicated\nrather than from whatever happens to be pending by the time you call \u2014 a review\nre-run between your read and this call is exactly what it exists to catch.\n\nThe footer-agreement check above is the downstream integrity backstop this ticket\nrelies on, and it matters MOST under `freshness_basis: "terminality_only"` (BAPI-1121):\nwhen no submission baseline was available to prove freshness, this is the one\nremaining check standing between an adjudication and a document that is not\nactually this review\'s output. Treat a missing/malformed/disagreeing footer with\nextra weight in that mode rather than assuming the wait\'s terminal-delivery\nacceptance already vouched for it.\n\nRead every saved-leg document\'s footer BEFORE the Step 7 rewrite, and read it from the canonical document, which Step 7 leaves untouched on disk. The rewritten copy deliberately carries no footer (BAPI-1097), so a footer read from it would either be absent or attest bytes that changed.\n\n**Degraded-run remediation.** If `freshness_basis` is `"terminality_only"` and freshness genuinely must be proven before adjudicating (rather than accepted on the terminality-only/footer-agreement basis above), submit the review again with `request_ticket_review` and wait again with `wait_for_ticket_review` within the SAME MCP session \u2014 a live submission establishes the baseline this backstop is standing in for.\n\n### Call the tool\n\nCall `capture_review_decisions` with:\n\n- `ticket_number`: `{ticket_key}`\n- `generation`: **required** \u2014 the integer read from the identity footer above.\n- `source_bundle_sha256`: **required** \u2014 the 64-character lowercase-hex digest\n read from that same footer. It is what proves these resolutions were authored\n against the exact bytes the generation was minted from.\n- `schema_version`: optional; omit it unless you actually know it.\n- `resolutions`: the array built above\n- `critique_document` / `clarify_document`: the **rewritten** markdown from\n Step 7, in memory, for each leg in `saved_legs` \u2014 send only those, omitting a\n leg absent from `saved_legs` (an ordinary policy omission) or withheld from it\n (unproven-freshness content that was never rewritten). These are transient \u2014\n the server counts `Review Decision` markers in them as a floor check and then\n discards them.\n- `request_id`: reuse the same value across retries of one logical capture\n\n### Do not continue past a refusal\n\nA refusal names a closed `reason`. Stop and report it; do not proceed to Step 8\nand do not describe the capture as successful.\n\n| `reason` | What it means |\n| --- | --- |\n| `manifest_coverage_mismatch` | An id is missing, duplicated, or unknown. Rebuild the resolution set against the ids actually in the documents. |\n| `marker_count_gap` | The prose records more decisions than the payload carries. Some decision was not captured \u2014 find it. |\n| `decision_set_too_large` | The set exceeds the record\'s bounds; nothing was truncated. Read `terminalized`: `true` means the generation is now `invalid` and closed, `false` means the invalidating write did not commit and the generation is still open \u2014 report it either way, and never assume the generation closed. |\n| `missing_identity` | The call carried no `generation` or no `source_bundle_sha256`. Stop and re-read the identity footer from each saved-leg document; never substitute a value you did not read. |\n| `stale_generation` | The review was re-run, so the documents you adjudicated are no longer the current generation. Wait for the review again to get a fresh `saved_legs` manifest, re-read the latest saved-leg documents, and re-establish the adjudication against them. Do not resubmit the same resolutions under a newly discovered generation number. |\n| `source_bundle_mismatch` | The submitted digest does not identify the current pending bundle. It is never adopted and never overwrites what is stored. Re-read the saved-leg documents and adjudicate the bundle that is actually pending. |\n| `invalid_decision_entry` | An `apply` has an empty instruction, or a `dismiss` has a non-null one. |\n| `conflicting_decision_set` | This generation was already captured with different decisions. Do not overwrite it. |\n| `generation_already_terminal` | The generation is already `complete` or `invalid`. A new review bundle is what produces a new generation; this one cannot be reopened. |\n| `no_pending_generation` | No decision generation exists for this ticket. The review was never delivered, or it was delivered before this record existed. |\n| `unsupported_schema` / `record_unreadable` | A record exists that this deployment cannot interpret. Report it; do not attempt to replace it. |\n\nA `409` or `422` here is a real failure of adjudication, not a formality to route\naround.\n\n## Step 8: Upload to Jira\n\nOnly after Step 7.5 succeeded. This upload and the prose markers it carries are\nthe human-readable record; the durable one is already written. A failure here is\nreported on its own and never invalidates the completed capture.\n\nUpload the **rewritten** copy of each document in `saved_legs`, using\n`attachment` (operation: `"upload"`) \u2014 a cost-saved Easy ticket has only `clarify`\nin `saved_legs`, so upload only that leg:\n\n1. Upload the rewritten clarifying questions **only when `clarify` is in `saved_legs`**:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions-reviewed.md`\n - `link_type`: `clarifying-questions.md`\n\n2. Upload the rewritten ticket quality critique **only when `critique` is in `saved_legs`** (skip this when critique is in `withheld_legs` or `absent_legs`):\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique-reviewed.md`\n - `link_type`: `ticket-quality-critique.md`\n\nThe `link_type` still names the CANONICAL leg. That is how the route identifies which\nreview leg the rewrite belongs to; it does not mean the canonical row is replaced. The\nroute stores the rewrite under its own human-facing type and its own Jira filename\n(`{ticket_key}-clarifying-questions-reviewed.md` /\n`{ticket_key}-ticket-quality-critique-reviewed.md`), and never touches the stamped\ncanonical row or its attachment.\n\n**Retry rules.** The route writes the rewrite to the database as pending, uploads to\nJira, then promotes. Only after promotion does it report success.\n- `503` \u2014 recorded as pending, but Jira or the promotion did not complete. **Retry**;\n no generation is minted either way. Byte-identical content resumes the same pending\n record, and different content supersedes it \u2014 a re-run of this workflow never\n reproduces the previous bytes exactly, so a differing retry is expected, not an error.\n- `409` \u2014 either the stored canonical bundle failed its own integrity check, or the\n rewritten markers do not match the manifest. Do not retry blindly; report it. On a\n ticket reviewed before BAPI-1097, a canonical-bundle `409` can mean the canonical row\n was already overwritten by the old workflow and no longer hashes to its footer.\n- `400` \u2014 the rewrite carries duplicate/malformed `bapi-item` markers, or more than one\n identity footer. Fix the document, then retry.\n\nReport the upload as successful only once the route returns success, which is the point\nat which both the Jira human-facing attachment and the current human-facing database row\ncarry the accepted bytes.\n\n## Step 9: Complete\n\nConfirm: "Review decisions captured and uploaded to {ticket_key}." State the\ngeneration that was recorded. Say this only when Step 7.5 returned a success\noutcome \u2014 an upload without a durable capture is not a completed adjudication.\n\n## Return\n\nConfirm "Review decisions captured and uploaded to {ticket_key}." and list the human-facing attachment(s) uploaded (`{ticket_key}-clarifying-questions-reviewed.md`, and `{ticket_key}-ticket-quality-critique-reviewed.md` when `critique` is in `saved_legs`). Say this only after each upload returned success \u2014 that is the point at which both the Jira attachment and the current human-facing database row carry the accepted bytes. Note any decisions that could not be applied, and any leg left pending after a `503`.\n\n### Summarize how the review improved {ticket_key}\n\nAfter the capture is confirmed, close with a short chat-only digest titled **How this review improved {ticket_key}** \u2014 three to six brief bullets, each naming one concrete way the review changed the ticket. Build it only from what was durably settled in Step 7.5, not from a fresh reading of the ticket:\n\n- One bullet per `accepted` + `apply` resolution, phrased as the ticket change it obliges. Compress its `binding_instruction` into the improvement \u2014 e.g. "Added a key-rotation prerequisite: the restricted key cannot read Subscriptions" or "Corrected the period-end read to `SubscriptionItem`."\n- One bullet for a Confirmed Improvement that closed a real gap the ticket was missing (an absent migration, an unhandled state, an assumption the codebase disproved), framed as the gap now closed.\n- Do not list `dismiss` or `rejected` items, or an item that only reworded existing text \u2014 those did not improve the ticket.\n\nLead each bullet with the improvement itself \u2014 a new requirement, a corrected assumption, a closed gap \u2014 not with where in the codebase it was found; the grounding is the evidence, not the point. Keep each bullet to one line. When every item settled as a dismissal or the review produced no actionable decisions, write one line instead of an empty list: "No ticket changes \u2014 the review confirmed the existing draft." This digest is chat-only; never write it into the clarifying-questions or ticket-critique documents.\n',"phase1/evaluate-and-recommend.md":'Evaluate the clarifying questions and ticket critiques generated for {ticket_key} against the actual codebase, then decorate every genuinely actionable item with the resolution guidance the reviewer will need on the decision page. The result is a single combined review-and-resolution document.\n\n<!-- BAPI-977 Phase-1 variant of evaluate-and-recommend.md. Selected only when\n REVIEW_SYNTHESIS_PHASE1_ENABLED is on. The legacy instruction at\n instructions/evaluate-and-recommend.md is the disabled path and is unchanged. -->\n\n## Phase 0 \u2014 Grounding & Audit Setup\n\nBefore gathering any source documents, extract the codebase-grounding context produced by the preceding `fresh_base` (`action: "materialize"`) pipeline step:\n\n- Read the `fresh_base` materialize tool result from earlier in this session. It returns JSON `{ base_sha, base_branch, fresh_base_root }` \u2014 or, when `no_refresh_base` was set, `{ base_sha: "local-stale", fresh_base_root: <original repo root> }`.\n- Retain `fresh_base_root` and `base_sha` for the rest of this procedure. Every codebase read in Phase 1 / Phase 2 below is grounded against `fresh_base_root`. `fresh_base_root` is also the exact value you must pass to the pipeline\'s later `fresh_base` (`action: "cleanup"`) step \u2014 it is a *runtime* value returned by the tool call, not a static recipe variable, so pass the real path string you captured here, not any placeholder text shown in the step\'s params.\n- If the materialize step\'s result contains an `error` field and `no_refresh_base` was NOT set, this is the fail-loud condition the recipe\'s `on_error: "halt"` exists for: stop here, do not fall back to grounding against your own working directory, and report the failure (name the attempted base branch and the remediation \u2014 retry, or rerun with `--no-refresh-base`).\n\n**Metadata Audit Header** \u2014 the very first content of the generated review-and-resolution document, before any other section, must be:\n\n```\n**Base SHA**: <base_sha>\n**Base Branch**: <base_branch, or "(local, in-place)" when base_sha is "local-stale">\n**Grounding Status**: <Freshly Materialized | Stale/In-Place Fallback>\n```\n\n- `Grounding Status` is **Freshly Materialized** whenever `base_sha` is a real commit SHA (the normal path).\n- `Grounding Status` is **Stale/In-Place Fallback** whenever `base_sha` is exactly `local-stale` (the `--no-refresh-base` opt-out path). In this case, immediately follow the header with a prominent, bold, high-contrast warning block, for example:\n\n > **\u26A0 STALE GROUNDING \u2014 `--no-refresh-base` was used.** This review evaluated the codebase as checked out locally, NOT a freshly-fetched `origin/<base>`. `file:line` citations may reflect uncommitted or unmerged local state.\n\n**Codebase grounding rule**: Ground ALL file reads and codebase searches exclusively against the `fresh_base_root` directory extracted above. Do NOT read codebase files from your default working directory or session cwd \u2014 `fresh_base_root` is the only trustworthy source of truth for `file:line` citations in this procedure.\n\n**Original-repo rule**: Ticket docs, `{docs_dir}` inputs, and ALL output paths stay in the ORIGINAL repository, never the `fresh_base_root` temp dir. This includes the ticket-fetch call below, the clarifying-questions / critique source documents, and the saved review-and-resolution output file (see the Save rule at the bottom). Do NOT redirect any of these into `fresh_base_root`.\n\n**Path hygiene rule**: Every `file:line` citation and every `Canonical citation` in the output document MUST be repo-relative \u2014 strip the `fresh_base_root` absolute-path prefix before writing it down. A citation must never contain a temp-dir / `/tmp/...`-style absolute path (write `src/foo.ts:10`, never `/tmp/bridge-review-.../src/foo.ts:10`).\n\n1. Fetch the current ticket description using the `get_ticket` tool with ticket_number `{ticket_key}` exactly once at the top of this procedure.\n\n2. **Manifest authority (BAPI-1121).** The preceding `wait_for_ticket_review` step\'s ready result carries the authoritative `saved_legs` (plus `advanced_legs`, `withheld_legs`, `absent_legs`, and `freshness_basis`) \u2014 that wait persisted exactly those documents and already removed the canonical file for any withheld leg. Determine which source documents to gather from `saved_legs`, NOT from checking which files exist on disk: a leg\'s canonical file existing is a content LOCATION, never independent evidence that this review delivered it.\n\n - Read the source document only for a leg present in `saved_legs`, from its canonical path: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` for `clarify`, `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` for `critique`.\n - A leg in `withheld_legs` or `absent_legs` must NOT be read or evaluated, even if a file happens to exist at its canonical path \u2014 a withheld leg\'s file was deliberately removed by the wait, and an absent leg\'s is either not present or, if one is, is not this review\'s output.\n - `advanced_legs` means strictly generation-proven current-review output. Under `freshness_basis: "terminality_only"` (no submission baseline was available to the wait), `advanced_legs` is intentionally empty even though `saved_legs` holds every terminally-delivered leg \u2014 surface this in the document\'s Round Agreement Summary as a degraded-freshness note (the artifacts may be indistinguishable from a prior attachment; the identity-footer agreement check downstream is the remaining backstop), rather than treating a `terminality_only` result as an ordinary fresh review.\n - **Stop and report** if a leg listed in `saved_legs` is missing or unreadable at its canonical content location \u2014 the wait\'s persisted manifest and the actual filesystem state have diverged, which is not a condition to paper over.\n - **Stop and report** if `saved_legs` is empty \u2014 no current-review source can be vouched for, so producing an evaluation from whatever happens to be on disk would silently source a review nobody delivered.\n\n After a second-opinion run, each document you DO gather has this shape:\n\n - A top-level H1 (`# Ticket Analysis` for clarifier docs, `# Ticket Quality Critique` for critique docs) followed by an italic provider-attribution line of the form `_This analysis was generated by GPT|Claude|Gemini._` The attribution names the LLM family that produced the **first round**.\n - The first-round questions / critique items, exactly as written by the first-round model.\n - **Inline second-opinion blockquotes** nested directly under each prior item the second round addressed. Each blockquote starts with `> **Second opinion (<provider>) - <stance>.**` where `<provider>` is `GPT|Claude|Gemini` and `<stance>` is `concurrence|refinement|disagreement`. The blockquote may be followed by `> *Citations: <comma-separated grounding refs>*`. Items the second round did **not** comment on have no blockquote \u2014 that is the "silent agreement" signal.\n\n **A `concurrence` blockquote may legitimately carry no body text and no citations.** Under Phase 1 the second round is instructed not to pad a pure agreement, so an empty concurrence blockquote is a deliberate, valid signal \u2014 it is an *explicit* concurrence, not a malformed one, and not the same thing as silence. Treat it as such; do not discard it and do not demand a citation from it.\n - A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 you will find a second italic attribution line of the form `_These additional points were raised by GPT|Claude|Gemini._` \u2014 this names the second-round LLM family. Sub-headings are agent-specific:\n - Clarifier docs: `### New Requirements Questions` and `### New Technical Questions` \u2014 numbering continues from the prior section.\n - Critique docs: `### New Requested Changes` and `### New Points to Consider` \u2014 numbering continues from the prior section.\n Each new item has its own `*Citations: ...*` line.\n - A final **`## Second Opinion Summary`** footer capturing the second round\'s execution outcome. Under Phase 1 this footer is generated by the server from execution metadata rather than written by the model, so it is a run record and NOT a source of analytical content. Do not mine it for findings and do not cite it.\n\n **Legacy fallback shape**: in rare cases (model lacks JSON-schema support, the JSON call failed, or the response could not be parsed), the document may instead end with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section containing `### Response to Prior Items` and `### Additional Points` subsections. If you detect this fallback shape, treat it equivalently: subsection responses tagged `concurrence` map to explicit concurrence, `refinement`/`disagreement` map to the disagree buckets, and items under `### Additional Points` map to the gap-captured bucket below.\n\n **Partial-source-doc tolerance (manifest-driven, BAPI-1121)**: a leg absent from `saved_legs` because it is in `absent_legs` (an ordinary policy omission \u2014 e.g. an Easy-ticket clarify-only review) is skipped silently; produce items only for the leg(s) actually in `saved_legs`. Do not fail. If `saved_legs` contains neither leg, still write the combined output file at `{docs_dir}/review/{ticket_key}-review-and-resolution.md` with the standard top-level sections (`Confirmed Improvements`, `Needs Scrutiny`, `Open Questions`, `Round Agreement Summary`) present, no emitted E-items in any section, and the Silent-Agreement Accounting block present with all totals at zero. This preserves downstream file-existence expectations for the capture-review-decisions step. This is different from the stop-and-report cases just above: an *absent* leg is a normal shape to omit, a `saved_legs`-listed leg that cannot be read is not.\n\n3. Determine **Round Agreement** for every clarifying question and critique point using these rules:\n\n - **Silent agreement** \u2014 the prior item has NO inline blockquote AND is not in `## New in Second Opinion`. The second round did not object to the point and did not consider it important enough to comment on. Handle it under **Phase 1S \u2014 Silent-agreement validation and accounting** below; it does not become a visible E-item unless it is demoted.\n - **Explicit concurrence** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - concurrence.** ...` blockquote, with or without body text and citations. The second round took the trouble to mark this item specifically. It stays a **retained visible item**. Establish its canonical citation yourself from the codebase \u2014 do not rely on the blockquote having supplied one.\n - **Rounds disagree (refinement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - refinement.** ...` blockquote. The second round modified or added detail. Apply full disagreement-depth analysis; the blockquote\'s citations are a starting point you must verify.\n - **Rounds disagree (disagreement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - disagreement.** ...` blockquote. The second round contradicts the prior. Apply full disagreement-depth analysis; categorize the outcome based on which position the codebase supports.\n - **Gap captured** \u2014 the item lives under `## New in Second Opinion > ### New <category>` (one of: New Requirements Questions, New Technical Questions, New Requested Changes, New Points to Consider). Apply the two-axis check below. The new item\'s citations are a starting point you must verify.\n - **Single round only** \u2014 the document has none of the above markers (no inline blockquotes, no `## New in Second Opinion` block, no `## Second Opinion Summary` footer). The pipeline ran only one round. Treat every item as a disagreement: cite the strongest codebase location and give full analytical depth.\n\n Apply these depth and categorization rules:\n\n - **Explicit concurrence**: 1-2 sentence assessment. Categorize as Confirmed Improvement unless your own validation contradicts it, in which case it becomes Needs Scrutiny at disagreement depth.\n - **Rounds disagree (refinement or disagreement)**: 3-4 sentence assessment that explicitly weighs the prior-round position against the second-opinion position. Categorize based on which position the evidence supports. Always include both positions in the Assessment.\n - **Gap captured \u2014 two-axis check** (for items in `## New in Second Opinion`):\n - If both the question is grounded in the codebase/standards AND the best-guess answer is sensible \u2192 **Confirmed Improvement** with a 1-2 sentence assessment.\n - If the question is genuine but the best-guess answer is flawed \u2192 **Needs Scrutiny**. Disagreement depth.\n - If the question itself does not hold up \u2192 **Needs Scrutiny** with evidence of what the code actually does. Disagreement depth.\n - If neither codebase nor standards can settle the question \u2192 **Open Questions**. Disagreement depth.\n - **Single round only**: Treat as a disagreement and give full analytical depth.\n\n For critique points (Requested Changes and Points to Consider), apply the same Round Agreement rules. The signal locations are inline `> **Second opinion (<provider>) - ...**` blockquotes nested under items in `### Requested Changes` / `### Points to Consider`, and gap-captured items under `## New in Second Opinion > ### New Requested Changes` / `### New Points to Consider`.\n\n **Source field conventions** \u2014 the `**Source**` string disambiguates where in the source doc the item lives so the downstream `capture-review-decisions` step can route the rewrite correctly. Use these forms:\n\n - **Silent agreement, demoted to visible**: `Clarifying Q3 (prior round, silent agreement \u2014 demoted)` or `Critique: Requested Change 2 (prior round, silent agreement \u2014 demoted)`.\n - **Explicit concurrence**: `Clarifying Q9 (prior round, concurrence inline)` or `Critique: Points to Consider 1 (prior round, concurrence inline)`.\n - **Refinement (inline blockquote)**: `Clarifying Q3 (prior round, refinement inline)`.\n - **Disagreement (inline blockquote)**: `Clarifying Q5 (prior round, disagreement inline)`.\n - **Gap captured (tail-block item)**: `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` or `Critique: Requested Change N+1 (new in second opinion \u2192 New Requested Changes)`. Always spell out the sub-section name after the arrow \u2014 capture-review-decisions uses it to find the rewrite target.\n - **Single round only**: `Clarifying Q3 (single round)`.\n\n## Phase 1S \u2014 Silent-agreement validation and accounting\n\nThis phase covers every prior-round item classified as **silent agreement** above. The second round\'s silence is not itself evidence, so each such item is still checked \u2014 but a check that passes produces a *count and a ledger entry*, not a reviewer-facing paragraph. Re-printing an item that two rounds and a codebase check all agreed on is the padding this phase exists to remove.\n\n**Validate every silent item individually.** Do not batch, sample, or spot-check. For each silent prior item, look at the actual code the item claims something about, in `fresh_base_root`, and decide whether the item\'s claim and its best-guess answer hold up. An item you did not individually validate may not be counted as accepted.\n\nThree counts govern this phase. Define them exactly as follows and emit all three:\n\n- `silent_validated` \u2014 the number of silent prior items you individually validated. This is every silent item, with no exceptions.\n- `silent_accepted` \u2014 the number of those whose claim and answer the codebase supports. These are **not** emitted as visible items.\n- `silent_demoted` \u2014 the number of those the codebase does **not** support. These **are** emitted as visible items.\n\nThe accounting must satisfy this invariant exactly, with no rounding, no "approximately", and no unaccounted remainder:\n\n```\nsilent_validated = silent_accepted + silent_demoted\n```\n\n### Accepted silent items \u2014 counted, not printed\n\nA silently accepted item is **omitted entirely from the reviewer-facing output**. It gets no E-item row, no bullet in any agreement list, and no entry in any visible section. It is recorded only in the machine-only ledger below.\n\n### Demoted silent items \u2014 the safety catch\n\nA silent item whose validation finds the claim or the answer to be wrong is **demoted**: emit it as a full **Needs Scrutiny** E-item at disagreement depth, with `**Source**` in the `(prior round, silent agreement \u2014 demoted)` form, and count it in `silent_demoted`. Silence must never be able to bury an incorrect item \u2014 the whole point of validating each one is that this catch fires.\n\n### The machine-only silent-validation ledger\n\nEmit exactly one ledger, as a single structured HTML comment, immediately after the Silent-Agreement Accounting block. Markdown renderers do not display HTML comments, so this record is available to tooling without adding a word to what the reviewer reads. Use this exact shape:\n\n```\n<!-- silent-validation-ledger\naccepted:\n- anchor: <source anchor, e.g. Clarifying Q4> | citation: <one repo-relative file:line>\n- anchor: <source anchor> | citation: <one repo-relative file:line>\ndemoted:\n- anchor: <source anchor> | item: <emitted E-item id, e.g. E-7>\n-->\n```\n\nRules for the ledger:\n\n- Every silently **accepted** item appears in `accepted:` with its source anchor and **one** canonical repo-relative `file:line` reference \u2014 the single grounding that made you accept it.\n- Every **demoted** item appears in `demoted:` with its source anchor and the id of the visible E-item it became. Its canonical citation lives on that visible item and is **not** repeated here; a demoted item\'s grounding is something the reviewer reads, so duplicating it into the ledger would be the same content twice.\n- Source anchors are unique across the whole ledger. The same anchor must never appear twice, and never in both lists.\n- The ledger is emitted even when both lists are empty (emit the two keys with no entries beneath them).\n- Explicit concurrences never appear here. They are retained visible items, not silent agreements.\n\n## Phase 1 \u2014 Evaluate and classify every visible item\n\nNumber every **visible** item sequentially across all sections (E-1, E-2, E-3, \u2026) with no gaps. Silently accepted items consume no number \u2014 they are not visible items. When the same underlying issue is raised in BOTH the clarifying-questions doc and the critique doc, consolidate it into a SINGLE E-item rather than emitting one per source, and cite both origins in its `**Source**` field (e.g. `Clarifying Q3 + Critique: Requested Change 2`); keep the numbering sequential with no gaps.\n\nClassify every visible item into exactly one of three buckets using the Round Agreement rules, codebase groundedness checks, and the `gap captured` two-axis check before producing any recommendation decoration:\n\n- **Confirmed Improvements**: Grounded suggestions that would genuinely improve the ticket. Includes explicit concurrences and `gap captured` items that passed both axes.\n- **Needs Scrutiny**: Suggestions based on inaccurate codebase assumptions, with evidence of the actual code behavior. Includes `gap captured` items that failed either axis, demoted silent items, and the loser of any rounds-disagree pair.\n- **Open Questions**: Legitimate ambiguities that require human input to resolve.\n\nPhase 1 must complete before Phase 2 begins \u2014 do not start decorating an item with a decision tree, recommendation index, or clarity fields until classification is final.\n\n## Canonical citation \u2014 exactly one grounding reference per retained item\n\nEvery retained visible item carries **one** `Canonical citation` field: a single repo-relative `file:line` reference that is the strongest grounding for the item. This replaces the previous practice of repeating grounding in three places \u2014 the decision-tree branches, the Assessment prose, and a Codebase Evidence list.\n\n- **One field, one reference.** Do not emit a list. If two locations genuinely matter, name the stronger one in `Canonical citation` and describe the second in the Assessment *without* a `file:line` reference.\n- **Do not repeat the canonical citation anywhere else in the item.** Not in the Assessment, not in a branch, not in an evidence block. It appears exactly once.\n- **Decision-tree branches carry no citations.** Do not append `See file:line` to a branch. A branch states a condition and an action; its grounding is the item\'s canonical citation.\n- If the item genuinely has no codebase grounding, write `Canonical citation: none \u2014 <one clause saying why>` and set `Confidence` to `Low`.\n\n## Phase 2 \u2014 Decorate genuinely actionable items\n\nPhase 2 applies **only** to items in the `Needs Scrutiny` and `Open Questions` buckets that represent a **genuine unresolved choice for the reviewer** \u2014 a decision a human still has to make. Confirmed Improvements stay terse (see "Confirmed Improvements output" below), and so does any Needs Scrutiny item whose resolution is already settled by the evidence.\n\n**A full decision card is only for a real choice.** If the evidence settles the matter and there is nothing for the reviewer to decide, the item does not get a card: render it as a terse Confirmed Improvement instead. Do not manufacture a second branch to justify a card.\n\nFor every genuinely actionable item, produce the following template using these stable labels:\n\n```\n### E-<sequential number>: <concise title>\n\n**Source**: <where this item lives in the source doc \u2014 see Source field conventions above>\n\n**Round Agreement**: <one of the values above> \u2014 <1 sentence on what the second round contributed>\n\n**Confidence**: <High|Medium|Low>\n\n**Canonical citation**: `path/to/file.ts:42`\n\n**Resolution path**: <"resolve at your desk" or "needs a conversation">\n\n**Decision tree**:\n- If <condition 1>, then <action 1>.\n- If <condition 2>, then <action 2>.\n\n**Recommendation Index**: <0-based index of the recommended branch in the decision tree above>\n\n**Recommendation**: <which branch the evidence best supports and why, 1-2 sentences>\n\n**Original question**: <the clarifying-question or critique point as it was originally raised, sourced verbatim or near-verbatim from the original clarifying-questions / critique docs. Light rephrasing is allowed; do NOT introduce new technical content. Soft cap ~30 words.>\n\n**Option consequences**:\n- <consequence for branch 1 \u2014 the behavioral consequence of choosing this option, not its rationale. ~25 words.>\n- <consequence for branch 2 \u2014 same shape. ~25 words.>\n\n**Why it matters**: <one concrete sentence on the impact this decision has on the ticket, the users, or the affected code paths. Soft cap ~40 words.>\n\n**Recommendation explanation**: <explain why the recommended branch is the best choice, tied to the evidence and the consequences of each option. Soft cap ~60 words.>\n\n**Assessment**: <what the code shows and what it implies, 3-4 sentences for a disagreement, 1-2 for a concurrence. State the evidence and the implication. Do NOT restate the original question \u2014 it is already in the Original question field. Do NOT restate the branch rationale \u2014 it is already in Recommendation explanation. Do NOT repeat the canonical citation.>\n```\n\n**Decision tree rules**:\n- Each decision tree must have **2\u20134 branches**, each a genuine, non-contrived alternative. Do not exceed 4 and do not produce only 1.\n- **Strict lower bound \u2014 reclassify on single-branch items**: If you can think of only one branch \u2014 that is, the resolution is effectively forced \u2014 reclassify the item as a **Confirmed Improvement** instead of emitting a single-branch decision tree. Do not stretch to a contrived second branch. If a single answer is genuinely the only path, the item belongs in Confirmed Improvements.\n- Each branch must end with a concrete, actionable step (not "investigate further").\n- Branches carry no `file:line` references. The item\'s `Canonical citation` is its grounding.\n- Cap each branch at one sentence.\n- `**Recommendation Index**` must be the 0-based index of the recommended branch in the decision tree above. The first branch is index 0, the second is index 1, etc. It must be within range of the branches actually emitted.\n- **Option consequences** must be a list parallel to the decision-tree branches: one entry per branch, in the same order, same length. Describe the behavioral consequence of choosing that option, not its rationale.\n- **"resolve at your desk"**: The item can be resolved through technical investigation \u2014 reading code, running tests, or checking configuration. No stakeholder input needed.\n- **"needs a conversation"**: The item involves a product decision, scope question, or cross-team dependency that cannot be resolved from the codebase alone.\n\n**Confidence Tags** \u2014 assign confidence based on codebase evidence strength:\n- **High**: The canonical citation directly supports the assessment.\n- **Medium**: The canonical citation is a related pattern or convention, not the exact code in question.\n- **Low**: No direct codebase grounding. Assessment rests on general reasoning or domain knowledge.\n\n### Confirmed Improvements output\n\nRender each Confirmed Improvement as a single terse row. No per-item headings, no decision trees, no clarity-field decoration:\n\n- **E-<number>: <title>** \u2014 Source: <source string>; Round Agreement: <value>; Confidence: <High|Medium|Low>; Canonical citation: `path/to/file.ts:42`. <recommended action, 1 sentence.>\n\nA Confirmed Improvement row never carries `options`, `option_consequences`, a decision tree, a recommendation index, or an empty placeholder for any of them. If an item seems to need one of those fields, it is not a Confirmed Improvement \u2014 reclassify it.\n\n## Silent-Agreement Accounting\n\nEmit this block in every run, including runs where one or both source documents were missing (in which case every total is `0`):\n\n```\n**Silent-agreement accounting**\n- silent_validated: <N>\n- silent_accepted: <N>\n- silent_demoted: <N>\n```\n\nThe three numbers must satisfy `silent_validated = silent_accepted + silent_demoted` exactly. Immediately after this block, emit the machine-only silent-validation ledger described in Phase 1S.\n\n## Round Agreement Summary\n\nAfter all items are processed, produce a summary section that groups items by round agreement status:\n\n### Points of Disagreement\nFor items where the evaluation marked `rounds disagree (refinement)`, `rounds disagree (disagreement)`, or `single round only` \u2014 including `gap captured` items that failed the two-axis check and landed in Needs Scrutiny \u2014 list as bullets with the E-number, the nature of the disagreement, and a 1-sentence explanation of why this disagreement matters for the ticket (e.g. it indicates an architectural ambiguity, a scope question, or a standards gap).\n\nIf no items were marked as disagreements, write: "All reviewed points had round consensus. No disagreement-driven risks identified."\n\n### Points of Agreement\n\n**Strong agreement** \u2014 items where the evaluation marked `explicit concurrence`. The second round took the trouble to mark this item specifically; that is a soft signal it is important enough to underline. List as bullets with the E-number and a half-sentence noting the shared conclusion.\n\n**Weak agreement** \u2014 represented by the accounting totals, not by an item list. Write a single line of the form: `<silent_accepted> prior item(s) were silently accepted after individual validation; <silent_demoted> were demoted to Needs Scrutiny (<E-numbers, or "none">).` Do not list the accepted items individually \u2014 they are recorded in the machine-only ledger, and re-printing them here is exactly the padding this variant removes.\n\nIf a sub-bullet has no items, omit it (rather than writing a "no items" note for each \u2014 keep the section tidy). The weak-agreement accounting line is always emitted, even when every total is zero.\n\n### Gaps Captured by Second Round\nFor items where the evaluation marked `gap captured` (sound second-opinion additions confirmed as Confirmed Improvements): list as bullets with the E-number and a half-sentence noting the gap the second round surfaced. These items did not require a decision \u2014 they are already in Confirmed Improvements \u2014 but are surfaced here so the reviewer sees what the second-round analysis added on top of the first round.\n\nIf no gaps were captured, write: "The second round did not surface any net-new confirmed improvements."\n\n## Edge Cases\n\n- If the evaluation contains zero items in Needs Scrutiny, write: "No items flagged for scrutiny. All reviewed suggestions were either confirmed or remain open questions."\n- If the evaluation contains zero items in Open Questions, write: "No open questions identified. All ambiguities were resolved through codebase analysis."\n- If both Needs Scrutiny and Open Questions are empty, include only the Confirmed Improvements section and add a summary: "All suggestions from the review were confirmed as grounded improvements. No decision trees are needed."\n- If `saved_legs` contains neither leg, still write the combined file with the standard top-level sections present, no emitted E-items, the Silent-Agreement Accounting block with all totals at zero, and the ledger with both lists empty \u2014 rather than failing.\n\n## Example of a Well-Written Terse Row (Explicit Concurrence \u2014 Confirmed Improvement)\n\n- **E-4: Sequential per-type review_repository fan-out** \u2014 Source: Clarifying Technical Q2 (prior round, concurrence inline); Round Agreement: explicit concurrence; Confidence: High; Canonical citation: `src/python/learn_repository/review_repository.py:369`. Keep the per-type fan-out sequential; the internal chunk-level gather already provides the concurrency.\n\n## Example of a Well-Written Actionable Card (Rounds Disagree \u2014 Needs Scrutiny)\n\n### E-5: Authentication middleware placement for new endpoint\n\n**Source**: Clarifying Q2 (prior round, disagreement inline)\n\n**Round Agreement**: rounds disagree (disagreement) \u2014 the prior round recommended adding auth at the router level; the second-opinion blockquote argued the existing middleware stack already covers it.\n\n**Confidence**: High\n\n**Canonical citation**: `api/routes/__init__.py:18`\n\n**Resolution path**: resolve at your desk\n\n**Decision tree**:\n- If routers each opt in to auth via dependencies, then add `Depends(require_api_key)` to the new endpoint.\n- If the global middleware stack already enforces auth on `/api/*`, then drop the explicit dependency.\n\n**Recommendation Index**: 0\n\n**Recommendation**: Routers opt in individually here, so the new endpoint must declare the dependency itself.\n\n**Original question**: Should the new `/api/exports` endpoint declare an explicit auth dependency, or is it covered by the global middleware?\n\n**Option consequences**:\n- Endpoint requires a valid API key, matching every other `/api/*` route.\n- Endpoint becomes publicly reachable; protected data leaks via the new path.\n\n**Why it matters**: Authentication on `/api/exports` decides whether protected data leaks; the wrong default is a security regression, not a stylistic choice.\n\n**Recommendation explanation**: Each router declares its own dependency and there is no catch-all auth middleware, so following the established pattern adds two lines and keeps auth uniform across endpoints.\n\n**Assessment**: The global middleware applies rate limiting and CORS but not authentication, and every existing router opts in explicitly. That contradicts the second opinion\'s claim of blanket coverage and supports the prior round\'s position. Omitting the dependency would leave the endpoint unauthenticated.\n\n## Example of the Accounting Block and Ledger\n\n```\n**Silent-agreement accounting**\n- silent_validated: 5\n- silent_accepted: 4\n- silent_demoted: 1\n```\n\n<!-- silent-validation-ledger\naccepted:\n- anchor: Clarifying Q4 | citation: src/python/learn_repository/__init__.py:14\n- anchor: Clarifying Q6 | citation: api/library/db/postgres_helpers.py:88\n- anchor: Critique: Requested Change 1 | citation: db/alembic/versions/0165_add_review_claims_and_result_cache.py:12\n- anchor: Critique: Points to Consider 3 | citation: main.py:45\ndemoted:\n- anchor: Clarifying Q8 | item: E-7\n-->\n\n## Save rule\n\nSave the combined review-and-resolution document to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`. Output only the combined review-and-resolution document \u2014 no meta-commentary.\n\n## Return\n\nConfirm "Review-and-resolution document written to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`." and report the total count of visible E-items captured together with the three accounting totals (`silent_validated`, `silent_accepted`, `silent_demoted`).\n'};init_version_generated();var README='# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Install\n\nFrom your **project root**, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\nThat is the whole setup command. It works whether or not you already have a Bridge\naccount \u2014 it will ask.\n\n> We recommend **the command above** instead of the `npm i @bridge_gpt/mcp-server`\n> one in npm\'s sidebar, because it **will make set up much easier**.\n\n**What it will do**\n\n- **Bootstraps the Bridge MCP for you** \u2014 one command and your editor\'s agent can\n use Bridge\'s tools and slash commands on this project.\n- Registers a `bridge` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates and updates the files it needs inside your project root: slash commands\n and agent definitions for your editor (`.claude/commands/`, `.cursor/commands/`,\n and the equivalents your editor uses), your editor\'s MCP config, and `.bridge/`\n for your project manifest and pipeline definitions.\n- Stores your Bridge credential outside the project, so the MCP server and the\n tooling that spawns its own shells can find it without you configuring anything.\n Re-running `install` still asks for the credential unless you supply it through\n `--api-key` or `BAPI_API_KEY` \u2014 the installer writes that store, it does not read\n it back.\n- Writes outside your project root only when you pick a host whose configuration is\n global: OpenAI Codex (`~/.codex/config.toml`).\n\n**Prerequisites**\n\n- **Node.js 18 or newer** (`node --version`), which is what provides `npx`.\n- **A project directory** \u2014 run the command from the folder your editor opens: your\n repository root, the one containing `.git`. No `package.json` is required \u2014 SFCC\n cartridge repos, Python, Go, Rust, and other non-Node projects work the same way.\n- **An MCP-capable editor or CLI**: Claude Code, GitHub Copilot in VS Code, Cursor,\n Windsurf, or OpenAI Codex.\n- **No Bridge account needed.** The installer can create one for you from just an\n email address.\n\n## Contents\n\n- [Install](#install)\n- [Installation details](#installation-details)\n - [Installing, step by step](#installing-step-by-step)\n - [What to expect](#what-to-expect)\n - [Troubleshooting](#troubleshooting)\n- [Usage Documentation](#usage-documentation)\n - [Regularly useful](#regularly-useful)\n - [Occasionally useful](#occasionally-useful)\n - [Now and then](#now-and-then)\n - [Workflow commands](#workflow-commands)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./docs/CONDUCTOR.md).\n\n## Installation details\n\n### Installing, step by step\n\n**1. Open a terminal in your project root.** This matters: the installer writes\nyour slash commands and MCP config relative to the directory you run it from. If\nyou run it in your home directory, your editor will not find any of it.\n\n**2. Run the command.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\n**3. Answer the sign-in question.** On a first run it asks whether you already have\na token:\n\n```\n1. I need a token\n2. I already have a token\n```\n\n- Choose **1** if you have nothing yet. It asks for your email address and a name\n for your new Bridge project, then creates both for you.\n- Choose **2** if someone gave you a token \u2014 either a Bridge API key or an invite\n code. Paste it at the hidden prompt; you do not have to say which kind it is,\n because the installer recognizes it. Nothing is echoed as you type.\n\nThere is no default answer, so pressing Enter alone selects nothing. If you would\nrather not be asked, pass the answer up front instead \u2014 see\n[Choosing how you sign in](#choosing-how-you-sign-in).\n\n**4. Pick which editors to configure.** The installer detects the MCP hosts on your\nmachine and asks which ones to set up. Pick every editor you actually use for this\nproject; you can re-run the command later to add another.\n\n**5. Reload your MCP host.** Editors read their MCP configuration at startup, so a\nfreshly written config is not live until you reload. Restart the editor, or use its\n"reload MCP servers" action. In Claude Code you will also be asked to trust the\nproject\'s `.mcp.json` the first time.\n\n**6. Finish in the agent session the installer opens \u2014 when it opens one.** The\nlast thing the installer does is offer to open a fresh agent session running\n`/install-bridge`, which reads your codebase, fills in the remaining project\nsettings, and prints a short report of what Bridge can help with. Let it finish.\n\nThree things all have to hold for that session to open: your selection has to\ninclude a host the installer can launch, the run has to be on an interactive\nterminal, and you have to accept the consent prompt (*"Bridge can configure and set\nup this project for you automatically. Open a `<tool>` session to do that now?\n(Y/n)"*). Claude Code is the only selection that launches on its own. A\nCursor-only, Copilot, Codex, or Windsurf selection, a non-interactive run, or a\ndeclined prompt all print the command to continue by hand instead. Pass\n`--agent claude` or `--agent cursor-agent` to override the decision outright.\n\n**7. Follow the next step the session shows you, if it shows one.** The installer\nasks the server what should happen next and shows that command only when there is\none to show \u2014 most often `/learn-repository`, which it recommends when the project\nstill needs its architecture, testing, review, and correctness standards documented\nand your key can run it. The installer deliberately does not run it for you. Those\nstandards are what make every later plan, critique, and review match how your\nproject actually works, and they only need to be gathered once per project \u2014 the\nresult is shared with everyone on the team. If the session shows no next step,\nthere is nothing for you to run.\n\nWant to see what would happen without changing anything? Add `--dry-run`.\n\n<details>\n<summary id="what-to-expect"><strong>What to expect</strong></summary>\n\n**Files that appear in your project**\n\n| Path | What it is | Commit it? |\n|---|---|---|\n| `.claude/commands/`, `.cursor/commands/`, `.github/prompts/`, `.github/skills/` | The generated workflow commands for supported agent hosts | Yes |\n| `.claude/agents/` and editor equivalents | Agent definitions used by those commands | Yes |\n| `.bridge/config` | Your project manifest \u2014 the repository name and which MCP targets to provision. Deliberately secret-free | Yes |\n| `.bridge/pipelines/`, `.bridge/instructions/` | Editable pipeline definitions | Yes |\n| `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` | MCP registrations for your editor. These can carry your key, so the installer git-ignores them | No |\n\nThe installer tells you which of these are safe to commit and never recommends\ncommitting anything that can hold a credential.\n\n**Prompts you will see.** More than the sign-in question, in three groups:\n\n- *Always on a first bare interactive run:* the sign-in question, a hidden prompt\n for a token (or a visible one for an email), a project name for a brand-new\n project, a picker for which editors to configure, and an offer to connect GitHub\n (`Connect GitHub? [y/N]:`).\n- *Conditional on your situation:* a confirmation when the directory has no `.git`\n (default **No**, and declining aborts); a *"Which tool should open? [1-N]"*\n chooser when your selection contains more than one launchable tool; and the\n consent prompt before the final agent session.\n- *Overwrite confirmations, each default **No** and each skippable with `--force`:*\n a saved key for this project already exists; a host config already contains a\n `BAPI_API_KEY`; a **git-tracked** config would receive your real key; a saved but\n expired self-serve signup would be discarded.\n\n**A fresh agent session opens at the end \u2014 if your selection can launch one.** See\nstep 6 above for the three conditions. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Selecting Windsurf prints instructions instead of writing config.** Windsurf\'s\nglobal `mcp_config.json` is never modified automatically; the installer reports the\nentry for you to paste yourself. Codex *is* written automatically, even though its\nfile is global too.\n\n**Your key is stored for the tools that read the store.** The MCP server and the\nshell-spawned tooling (`start-tickets` and its model routing) resolve it from\n`~/.config/bridge/credentials.json` on their own. The **installer** does not: a\nrepeat `install` prompts for the credential again unless you pass `--api-key` or\nset `BAPI_API_KEY` in the environment.\n\n**A next step, when the project needs one.** The session closes with whatever\ncommand the server says comes next, and stays quiet when there is nothing to\nrecommend. `/learn-repository` is the usual one: it is recommended when the project\nstill needs its conventions documented and your key can run it. It is never\nautomatic \u2014 until someone runs it, Bridge\'s agents work from your code alone rather\nthan from your project\'s documented conventions.\n\n**Indexing happens on its own.** There is no "index my repository?" question. Once\nyour project has the settings it needs, indexing starts server-side. You never have\nto ask for it.\n\n</details>\n\n<details>\n<summary id="troubleshooting"><strong>Troubleshooting</strong></summary>\n\n**"My editor doesn\'t see any Bridge tools."** Two usual causes. First, the config\nwas written somewhere your editor is not looking \u2014 re-run the installer from the\ndirectory your editor actually opens, and check that a `bridge` entry exists in\nthat project\'s MCP config. Second, the editor has not been reloaded since the file\nwas written; restart it. In Claude Code, also confirm you accepted the trust prompt\nfor the project\'s `.mcp.json`.\n\n**"I ran it in the wrong folder."** Nothing is broken. Depending on which editors\nwere detected, a run can leave `.bridge/`, `.bridge/install-state.json`, `.claude/`,\n`.cursor/commands/`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.github/agents/`,\n`.mcp.json`, and appended `.gitignore` lines. Remove only what that run created and\nre-run the command from the right directory \u2014 if you already had a `.vscode/`,\n`.cursor/`, or `.gitignore` there, keep the parts you had before.\n\n**"It seems to hang with no output."** If you ran the bare command\n(`npx -y @bridge_gpt/mcp-server`) with no subcommand, you started the MCP *server*,\nnot the installer. It is waiting for an editor to connect over stdio, which is\nexactly what it should do when your editor launches it \u2014 but from a terminal it\nlooks like a hang. It prints a line saying so. Press Ctrl-C and run\n`npx -y @bridge_gpt/mcp-server install` instead. The explicit spelling\n`npx -y @bridge_gpt/mcp-server serve` starts the server on purpose.\n\n**"It can\'t reach Bridge" or "my key was rejected."** The installer checks\nconnectivity before it saves your **credential** anywhere, so a failure here has not\nwritten your key into a config or stored it for later. It has already\nscaffolded the project files by then \u2014 slash commands, agents, pipelines,\n`.bridge/config`, and secret-free per-host MCP placeholders \u2014 so expect those to\nexist; re-running is safe and refreshes them. A\nrejected key means the credential is not valid for that project \u2014 check the project\nname you gave, and generate a fresh key on the Bridge web UI\'s **Security** page if\nneeded. A network failure usually means a proxy or VPN is in the way.\n\n**"Which repository name should I use?"** The one registered with Bridge. If you\nhave an existing key, the installer usually resolves it for you; when it cannot, it\nasks, and `--repo <name>` answers it up front.\n\n**Still stuck? Ask the installer to diagnose itself.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server doctor\n```\n\n`doctor` is strictly read-only. It reports what it found \u2014 configs, registrations,\ncredential availability, prerequisites \u2014 and changes nothing.\n\n**Codex approval profile.** When selecting Codex during interactive `install`,\npress Enter to accept the default Yes at the profile prompt, or answer `n`/`no`\nto decline. Pass `--tools=codex --codex-profile=on` in a scripted install. Bridge\ncreates `$CODEX_HOME/bridge.config.toml` (default `~/.codex/bridge.config.toml`)\nwith approval settings for the tools used by its review and implementation\nworkflows. Bridge-spawned Codex workers then select it automatically, including\nthe fresh implementation session after review. Pass `--codex-profile=off` to\ndecline; the installer leaves any existing profile content intact. On package\nupgrade, the next Bridge-spawned worker refreshes an older, unmodified managed\nprofile before launch. If a profile\nalready contains user-authored or conflicting content, the installer leaves it\nuntouched and reports that workers cannot use it. Run `doctor --agent codex` to\ninspect the current status. To use the profile in a Codex session you open\nyourself, run `codex --profile bridge`. Bridge `--auto` controls recipe gates;\nCodex MCP tool approvals are a separate setting. The profile does not change\nglobal `sandbox_mode` or `approval_policy`.\n\n</details>\n\n<details>\n<summary id="choosing-how-you-sign-in"><strong>Choosing how you sign in</strong></summary>\n\nThree routes lead to the same place. The interactive question above picks one for\nyou; these flags pick it up front and skip the question entirely.\n\n**No account yet \u2014 sign up with an email.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --email you@example.com\n```\n\nCreates a brand-new Bridge project for that address and your first admin key in one\ncommand. No account, no key, and no invite needed beforehand. The address labels\nyour new workspace and may receive a setup message; delivery is best-effort, so\nnothing waits on it. The email is visible as you type (it is not a secret) and is\nnever written to a log. This is the same route as answering **1** at the prompt.\n\n**You were sent an invite code.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --invite\n```\n\nRedeems the invite, creates your project, and mints your first admin key. Run it\n*without* a value, as shown: the installer then asks for the code at a hidden\nprompt, so the code never lands in your shell history. `--invite <code>` and the\n`BAPI_INVITE` environment variable exist for scripting, but both expose the code to\nyour shell history and to the process list.\n\n**Your team already has a project and gave you an API key.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --api-key <key>\n```\n\nOr omit the flag and paste the key at the hidden prompt. Generate a key on the\nBridge web UI\'s **Security** page (**Create New Key**, role **Admin**) and copy it\nimmediately \u2014 it is shown once. `BAPI_API_KEY` works too.\n\nIf you paste an invite code where a key was expected, or a key where an invite was\nexpected, the installer recognizes the mismatch and tells you before anything is\ncreated or spent.\n\n</details>\n\n<details>\n<summary><strong>Installer flags</strong></summary>\n\n| Flag | What it does |\n|---|---|\n| `--email <addr>` | Sign up for a new Bridge project with just an email address |\n| `--invite [code]` | Redeem an invite code. Omit the value for the hidden prompt (recommended) |\n| `--api-key <key>` | Use an existing Bridge API key |\n| `--repo <name>` | Name the registered repository instead of resolving or asking for it |\n| `--tools <list>` | Configure specific MCP hosts without the picker. Accepted IDs are exactly `claude-code`, `cursor`, `copilot-vscode`, `codex`, and `windsurf` (e.g. `claude-code,cursor`); any other value is a parse error |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step. **No default** \u2014 without this flag the agent is derived from the hosts you selected, and an explicit value always wins, including for a host you did not select |\n| `--dry-run` | Preview every step without writing, contacting Bridge, resolving or prompting for a credential, or opening anything. Genuinely inert: it returns before the project-root prompt, before the repository is resolved, and before any tool-selection prompt, so a value it cannot know locally (an unresolved repository name, an unselected tool) is shown as **not yet known** rather than guessed |\n| `--force` | Overwrite an existing stored key without asking |\n| `-h`, `--help` | Full usage |\n\n`--email`, `--invite`, and `--api-key` are mutually exclusive \u2014 each names a\ndifferent way to arrive, and the installer will not guess between them.\n\n</details>\n\n<details>\n<summary><strong>Setting up an MCP host by hand</strong></summary>\n\nThe installer configures your editors for you. Do this only if you would rather\nwrite the config yourself, or if you use a host it cannot write automatically.\n\nScaffold the project files and write a secret-free MCP registration. Run it from\nthe same project root `install` uses \u2014 your repository root, the one containing\n`.git`. No `package.json` is required:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` always creates `.mcp.json`, and adds `.vscode/mcp.json` or\n`.cursor/mcp.json` when it detects those editors. Each generated entry carries\n`BAPI_BASE_URL`, `BAPI_REPO_NAME`, `BAPI_DOCS_DIR`, and `BAPI_PROJECT_ROOT`, and\n**never** `BAPI_API_KEY` \u2014 the server resolves the credential itself at runtime.\n\nSo the manual work left after `--init` is narrower than writing an entry from\nscratch: correct `BAPI_REPO_NAME` if it was written as the `YOUR_REPO_NAME`\nplaceholder, and supply your credential through a supported source (`BAPI_API_KEY`\nin the entry\'s `env` block, `BAPI_API_KEY` in the server\'s environment, or the\n`~/.config/bridge/credentials.json` store).\n\nWrite the entry yourself instead \u2014 for a host `--init` does not touch, or because\nyou would rather \u2014 using the shapes below. Add `"serve"` as the last launcher\nargument, as shown: it is the explicit way to say "start the MCP server." Pin the\npackage to an exact version and pass `--prefer-offline`, which is what the\ngenerated entries do and what keeps npx from resolving a different build on some\nlater boot.\n\n<!-- BEGIN GENERATED: mcp-host-examples (managed by mcp_server/scripts/sync-readme-host-examples.js \u2014 DO NOT EDIT BY HAND) -->\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>GitHub Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge]\ncommand = "npx"\nargs = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"]\n\n[mcp_servers.bridge.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see [Environment Variables](#environment-variables)).\n</details>\n<!-- END GENERATED: mcp-host-examples -->\n\nAfter saving, reload your editor and ask your assistant to call the `ping` tool to\nconfirm the connection.\n\nAn entry with no trailing `serve` still starts the server \u2014 bare invocation means\n"server" permanently, and nothing rewrites an existing config to add the token.\n\n</details>\n\n<details>\n<summary><strong>Upgrading Bridge</strong></summary>\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest --upgrade\n```\n\n`upgrade` fetches the latest published version, refreshes your scaffolded slash\ncommands, agents, and pipelines, updates the version pin in your MCP config, and\nopens a session so you can reconnect. It is also available as the\n`/upgrade-bridge` slash command.\n\nUse the `@latest` form. It applies to the short-lived *upgrader* process: without\nit, npx may reuse a cached older copy of the package and "upgrade" you with the\nbuild you are trying to replace. The exact `MAJOR.MINOR.PATCH` pin the upgrader\nwrites into your MCP config is deliberately different \u2014 host configs stay pinned\nto an exact release so a project\'s server is reproducible.\n\n`upgrade` reports **per config file**, because a project can have several\n(`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) and they can disagree:\n\n```\nLauncher pins:\n .mcp.json: 0.2.16 -> 0.2.36\n .cursor/mcp.json: already 0.2.36\n```\n\nWhen every applicable launcher pin was already at the target, it prints\n`Already up-to-date.` \u2014 that status comes from comparing your configs, not from\nthe version of the CLI process. A non-zero exit means the upgrade did **not**\nconverge, and nothing is reported as complete in that case. The causes:\n\n- the npm registry lookup failed **and** this process was not started from\n `@latest`, so the target version could not be confirmed \u2014 the likeliest one\n offline, and why the canonical command uses `@latest`;\n- a launcher pin is already **newer** than the target, which an automated repin\n must never downgrade;\n- an unreadable or unparseable config, a launcher carrying a version range or a\n dist-tag rather than an exact release, or two Bridge registrations in one file;\n- a competing local install it could not remove, or a pin that failed post-write\n verification;\n- the upgrade finished but left an **unconfigured** MCP entry \u2014 one that would\n authenticate as nobody.\n\nThe server checks for updates on startup. The check is cached for a day and never\nblocks startup. When a newer version is known, it surfaces in two places you do\nnot have to go looking for: a one-line warning on the server\'s **stderr**, and a\nshort advisory attached to the ordinary `tools/list` response so the agent in the\nsession can see that some tools may be missing or renamed in the older build.\nNeither requires calling `ping` or `doctor`.\n\nRe-running `install` on an already-configured project is safe: it refreshes the\nscaffolded files without overwriting your stored credential unless you pass\n`--force`.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful**, **how to use it**, and its **flags**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships SFCC platform tools \u2014 read-only introspection under the `sfcc` profile, and nine destructive writes under the separate `sfcc-write` opt-in. See [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n<!-- The three tier sections below are GENERATED from TWO catalogs by\n scripts/sync_mcp_server_readme.py: api/library/config/mcp_tool_catalog.json,\n the authoritative MCP tool catalog, and api/library/config/workflow_catalog_lib.py,\n the immutable catalog of slash-command workflows (which have no MCP registration\n and therefore cannot live in the JSON artifact). Edit the curated tool metadata in\n scripts/sync_mcp_tool_catalog.py and the workflow definitions in\n workflow_catalog_lib.py \u2014 never the JSON artifact and never the text between the\n markers. Generation order is: sync_mcp_tool_catalog.py, then\n sync_mcp_server_readme.py, then `cd mcp_server && npm run build` (which bundles this\n file into readme.generated.ts, served as the MCP resource bridge://readme).\n Everything outside the marker pair \u2014 including the sections below it \u2014 is hand-written. -->\n\n<!-- BEGIN GENERATED: mcp-tool-documentation (managed by scripts/sync_mcp_server_readme.py \u2014 DO NOT EDIT BY HAND) -->\n### Regularly useful\n\nThe tools worth knowing for most tickets.\n\n**1. Review Ticket**\n- **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.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-tickets BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review). A single key reviews inline in the current session \u2014 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.\n- **Flags:** `--auto` auto-accept findings and skip the approval gates \xB7 `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the difficulty-adaptive review policy decide.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default \xB7 `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs \xB7 `--rounds=1|2` forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override.\n\n**3. Review and Start**\n- **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 \u2014 review and implementation run in two separate agent contexts, not one shared session.\n- **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).\n- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).\n- **Flags:** `--auto` auto-approves both the review and the implementation phase of every spawned session \xB7 `--rounds=1|2` forwarded to the review phase \xB7 `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.\n\n**4. Explore Ticket**\n- **What it does:** Maps the code paths, dependencies, and project conventions a task would touch, settles its acceptance criteria with you on a decision page, then compares the viable implementation approaches and their trade-offs and writes up a proposed design. Along the way it surfaces the ambiguities that still need deciding and can pull in optional web or deep research where the answer is not in the code.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or a plan, when you\'re unsure how a change would fit the existing code and want the open questions and the realistic options laid out first.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n- **Flags:** None.\n\n**5. Council**\n- **What it does:** Fans your problem out to two different models and returns their approaches, in technical, design, discovery, or general mode.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 technical for how to build it, design for how it should look, discovery for what still needs figuring out before a real ticket exists, general for a quick brief-driven pass before the repository is indexed.\n- **How to use it:** `/council <question>`\n- **Flags:** `--mode` selects one of four modes, passed to the underlying `request_council` tool as e.g. `mode: "discovery"`: `technical` (the default \u2014 implementation/architecture approaches), `design` (UI/UX and visual direction), `discovery` (stakeholder discovery questions, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), and `general` (brief-driven ideation from your task description alone). `technical` and `discovery` are codebase-grounded and need an indexed repository; `general` needs no code index at all, so it works immediately after install. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n\n**6. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge \u2014 libraries, best practices, standards \u2014 that you do not already have.\n- **How to use it:** `/bridge-research <question>`\n- **Flags:** None.\n\n### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. Upload Ticket**\n- **What it does:** Creates a real Jira issue from a drafted ticket, including child tickets under an epic; your agent should confirm with you before creating it.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into your tracker so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket; it should confirm with you before creating the live issue.\n- **Flags:** Name the issue type (Bug / Story / Task / Epic) and, for a child ticket under an epic, the parent key.\n\n**2. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket that references real files in your codebase.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before \u2014 or instead of \u2014 auto-implementing it.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**3. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a technical design document, a functional spec, or a product requirements document.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family, without saving an artifact.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** Ask your agent \u2014 "Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against production."\n- **Flags:** Pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**5. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model, spending provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** Ask your agent \u2014 "Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."\n- **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**6. Create Decision Page**\n- **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.\n- **When it\'s useful:** (Refinement) Use it whenever a conversation has accumulated more open choices than are comfortable to settle in prose \u2014 not for a single simple question, which is faster to just ask directly.\n- **How to use it:** `/create-decision-page <topic or open questions>` \u2014 e.g. *"how we handle rate limiting"* \u2014 or leave it empty to frame the open choices already raised earlier in the conversation.\n- **Flags:** None.\n\n### Now and then\n\nUseful once in a while.\n\n**1. Reimplement Ticket**\n- **What it does:** Gathers the context and attachments added since the last pass so a targeted follow-up change can be made.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n- **Flags:** None.\n\n**2. Update Ticket**\n- **What it does:** Rewrites a ticket\'s description, fully replacing what is there today.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** Ask your agent \u2014 "Replace BAPI-123\'s description with: <new text>." It fully overwrites the live description, which is hard to reverse.\n- **Flags:** None.\n\n**3. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description, and optionally the model tier its difficulty routes to.\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** Ask your agent \u2014 "Pull up BAPI-123 and show me its description, status, and acceptance criteria."\n- **Flags:** `include_model_tier` adds the model tier the ticket\'s difficulty routes to; leave it off for an ordinary read.\n\n**4. Search Tickets**\n- **What it does:** Searches across the tickets in your project.\n- **When it\'s useful:** (Refinement) When you need to find tickets by project, status, or wording rather than by key.\n- **How to use it:** Ask your agent \u2014 "Search our project for open tickets mentioning rate limiting."\n- **Flags:** Narrow the search by project, status, issue type, or free text.\n\n**5. Write Comment**\n- **What it does:** Posts a comment on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** Ask your agent \u2014 "Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it is rotated."\n- **Flags:** A long comment can be attached as a file instead of inlined.\n\n**6. Read Comments**\n- **What it does:** Reads the comment thread on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When the discussion on a ticket matters and you want the agent to read it before acting.\n- **How to use it:** Ask your agent \u2014 "Read the comments on BAPI-123 and summarize what was decided."\n- **Flags:** None.\n\n**7. Ticket Attachments**\n- **What it does:** Downloads files from a ticket to your disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files or logs you need locally, or you want to attach output back to it.\n- **How to use it:** Ask your agent \u2014 "Download the design mockups attached to BAPI-123 into my docs folder," or "Attach build-log.txt to BAPI-123."\n- **Flags:** Choose the direction (download from the ticket, or upload to it) and, for a download, where the files should land.\n\n**8. Estimate Epic**\n- **What it does:** Estimates an epic, or an explicit group of tickets you name.\n- **When it\'s useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.\n- **How to use it:** `/estimate-epic BAPI-123`\n- **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.\n\n**9. Regression Check**\n- **What it does:** Runs the deterministic regression-reviewer (lightweight mode) against a proposed code change and reports its blast radius \u2014 which real call-sites, tests, mocks, or config the change does and doesn\'t account for.\n- **When it\'s useful:** (Correctness) Before merging a change, to see which real callers, tests, or config it leaves unaccounted for.\n- **How to use it:** `/regression-check` reviews the working tree against `HEAD` by default; `/regression-check --diff main...HEAD` reviews an explicit range.\n- **Flags:** `--diff <range>` review a specific diff/PR range instead of the working tree vs. `HEAD` \xB7 `--symbols a,b,c` review a not-yet-diffed planned change by naming its symbols directly.\n\n**10. Scan Test Coverage**\n- **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.\n- **When it\'s useful:** (Testing) Periodically, to find recently shipped features still missing integration-test coverage and how each could be smoke tested instead.\n- **How to use it:** `/scan-test-coverage` runs incrementally from the last recorded marker in `docs/test-coverage/STATE.md`.\n- **Flags:** `--since=YYYY-MM-DD` override the window start date \xB7 `--full` ignore the marker and use a 6-month lookback \xB7 `--limit=N` cap the number of features investigated.\n\n**11. Estimate Ticket**\n- **What it does:** One-ticket shortcut over the existing epic estimation capability: estimates a single Jira ticket via the same `estimate_epic` tool.\n- **When it\'s useful:** (Estimation) When you want one ticket\'s estimate without typing out an epic key or an explicit ticket list.\n- **How to use it:** `/estimate-ticket BAPI-123`\n- **Flags:** `--allow-partial` the sole optional flag.\n\n**12. Draft Ticket**\n- **What it does:** Delegates to the Jira Ticket Writer to create only a local Markdown draft \u2014 it does not create or update a Jira issue.\n- **When it\'s useful:** (Refinement) When you want a reviewable local ticket draft before committing to an upload workflow.\n- **How to use it:** `/draft-ticket <description>`\n- **Flags:** None.\n<!-- END GENERATED: mcp-tool-documentation -->\n\n### Workflow commands\n\nSlash 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) \u2014 the rest live here.\n\n**1. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**2. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** Ask your agent, *"Use the jira ticket writer to turn our conversation into a ticket."* The other ticket commands draft through it automatically.\n- **Flags:** None \u2014 name a specific standards file in your request to have it applied when drafting.\n\n**3. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n- **Flags:** None.\n\n**4. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n- **Flags:** None.\n\n#### Ticket-authoring posture\n\n`/explore-ticket`, `/idea-to-ticket`, and `/plan-epic` all decide ticket shape\nthe same way, as does the `jira-ticket-writer` agent they draft through. A fresh install inherits this with no configuration\nstep and no server call; the full rationale and the closed exception list ship as\n`docs/bridge-ticket-authoring.md`.\n\n- **Drafted by the writer.** Every ticket body \u2014 epic parent, epic child, and\n ordinary sibling alike \u2014 goes through the `jira-ticket-writer` agent. Nothing\n composes a ticket description inline.\n- **Sized toward L, overflowing upward.** `L` (target) \u2192 `XL` (when the work\n does not fit in `L`) \u2192 `M` (third choice) \u2192 `S` (only when unavoidable). A\n slice that outgrows `L` becomes one `XL` ticket rather than two `L` ones \u2014\n splitting a coherent slice to fit a band buys another worktree, another PR, and\n another rebase for nothing. This binds a standalone ticket and an epic child\n alike. Past roughly 40 files or ~3000 LOC it splits anyway, into the largest\n coherent pieces available.\n- **Grouped at three.** Three or more tickets is an epic: an epic parent plus an\n ordered child manifest, shown in full at an approval gate before anything is\n created. One or two are ordinary siblings \u2014 no epic parent, no manifest. The\n threshold is exactly three.\n- **Decomposed once, rendered many.** One pass freezes the split; body drafting\n then fans out one writer invocation per entry against that frozen manifest. A\n rendering invocation never re-splits, merges, reorders, or rescopes.\n- **Handed off once.** An epic handoff names exactly one entry point,\n [`drive-epic`](#drive-epic) \u2014 never a choice between conductors.\n\n**5. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests`\n- **Flags:** `--unit-only` skip the E2E suite \xB7 `--skip-e2e` same, phrased the other way.\n\n**6. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n- **Flags:** None.\n\n**7. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n- **Flags:** None.\n\n### Operational commands\n\nRepository and ticket housekeeping commands, beyond the tiers above. Implementation, commit/push, PR creation, and CI monitoring are steps of the `implement-ticket` recipe rather than standalone commands:\n\n| Command | What it does |\n|---|---|\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> **GitHub Copilot modes.** On VS Code\'s Local agent, Bridge provisions one\n> `.github/prompts/<command>.prompt.md` prompt per packaged command, including\n> `/explore-ticket`. On VS Code Agent Host, prompt files are not loaded, so the\n> installer provisions the same canonical command as a project skill at\n> `.github/skills/<command>/SKILL.md`; invoke that skill by its slash-command\n> name. Bridge does not claim Copilot CLI or cloud-agent workflow support from\n> this scaffold alone. If either artifact is not discovered, use VS Code\'s Chat\n> Customizations diagnostics, confirm the workspace is trusted, and invoke the\n> workflow manually with its full instructions rather than assuming the slash\n> command is available.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks \u2014 the commit-and-push, PR-creation, and CI-monitoring steps of the `implement-ticket` recipe, driven by its bundled instruction assets.\n- **Design documents** for a ticket (TDD / FSD / PRD, generated and retrieved through `create_doc` / `get_doc`).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo and check parse status (`parse_repository`), regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** fetch a resolved pipeline recipe (the step list behind the orchestration commands) and execute its steps agent-side.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity check:** ping Bridge to confirm the API key and repository resolve.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, design docs) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, MRT bundle push, and SCAPI Custom API scaffolding. **As of `@salesforce/b2c-dx-mcp` 1.1.2 (published 2026-05-20)** it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. That comparison is dated on purpose: its basis is this repository\'s hand-maintained [vendor manual](../docs/mcp/b2c-commerce-developer.md), pinned to the same version, so a new Salesforce toolset ages the claim visibly instead of rotting silently. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **OCAPI Data API introspection** of system objects, custom object definitions, and site preferences \u2014 plus, behind a separate opt-in, a set of sandbox-bounded writes.\n\n**Every SFCC tool is restricted to a developer sandbox, and the restriction is checked at invocation time against the hostname your credentials actually resolve to** \u2014 not against anything the caller passes in. If `dw.json` or `SFCC_HOSTNAME` names a host Bridge does not recognize as a developer sandbox, every SFCC tool refuses with a `403` before contacting it. See [Sandbox enforcement](#sandbox-enforcement).\n\n**Credentials stay local** \u2014 in `dw.json` or `SFCC_*` env vars \u2014 and are never sent to Bridge. The `sfcc` profile registers read-only tools; the nine destructive write tools require the separate `sfcc-write` opt-in (see [Read and write profiles](#read-and-write-profiles)).\n\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nEvery SFCC tool must be enabled with a profile (step 3) \u2014 including the two diagnostics, `sfcc_setup_status` and `check_permissions`, which belong to the `sfcc` profile. A session on the default `core` profile registers no SFCC tool at all. Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks the OCAPI tools \u2014 the eight reads, the nine writes, and `check_permissions`. It does **not** block `sfcc_setup_status`, and it does not block `sfcc_log_query`: log query runs on its own gate, which reads neither the `version` field nor `dw.json` and instead probes the backend log capability (log access is WebDAV Basic auth, a different boundary from OCAPI\'s OAuth). Set the field via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. `dw.json` is auto-added to git exclude and must never be committed.\n\nCredentials resolve in **three tiers, highest first** \u2014 the environment wins over `dw.json`, not the other way round:\n\n1. An explicit dotted `instance` argument on the call, **plus** `SFCC_CLIENT_ID` and `SFCC_CLIENT_SECRET` in the environment. Secrets are never read from `dw.json` on this tier, so an explicit instance without those two env values is an error.\n2. `SFCC_HOSTNAME` **and** `SFCC_CLIENT_ID` **and** `SFCC_CLIENT_SECRET`, all three set.\n3. `dw.json`.\n\nBecause tier 2 outranks tier 3, a stale `SFCC_HOSTNAME` left in the environment silently wins over the `dw.json` you are looking at. Check both when a tool reports an unexpected host.\n\n**Use a single-config `dw.json`, or set all three `SFCC_*` variables.** A multi-entry `configs[]` array is **rejected outright** \u2014 it is not a working setup that merely requires an explicit `instance` on every call. Two things make that workaround unavailable: an explicit `instance` takes tier 1, which needs the client id and secret in the environment anyway, and most tools cannot accept a hostname at all \u2014 the value must contain a dot, and the site-preference tools constrain `instance` to `staging | development | sandbox | production`, none of which is a hostname.\n\n**3. Enable the tools you want.** Add the groups to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\n`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.\n\n**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 \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\nRead what it prints before pasting it. The output is **two labelled blocks**, and they are not equivalent:\n\n- **READ/SEARCH TOOL GRANTS** \u2014 what the `sfcc` read tools need: `get` on `/system_object_definitions`, and `get` + `post` on `/system_object_definitions/**`, `/site_preferences/**`, and `/custom_object_definitions/**`. The `post` is OCAPI\'s convention for its `*_search` endpoints, not a mutation \u2014 but it is a grant you are pasting, so it is labelled for what it is rather than as "read-only".\n- **MUTATION GRANTS** \u2014 required by the nine `sfcc-write` tools and by nothing else: `put`/`patch` on `/system_object_definitions/**` and `/custom_object_definitions/**`, and `patch` on `/site_preferences/**`. Paste this block only if you intend to enable `sfcc-write`.\n\nNeither block grants `delete`, and neither pastes the global `resource_id: "/**"` that would cover every Data API resource. Each entry names one resource family \u2014 `/system_object_definitions`, `/custom_object_definitions/**`, `/site_preferences/**` \u2014 so the wildcard is scoped to the family, not to the API. Within a family it is still broad, and `write_attributes` is `(**)`, so a throwaway sandbox is the right place for these.\n\n</details>\n\n### Sandbox enforcement\n\nEvery SFCC tool \u2014 all twenty, reads and writes alike, including the diagnostics \u2014\npasses through one check before its own logic runs: **the hostname your\ncredentials actually resolve to must be a recognized developer sandbox.**\n\n- The check reads `credentials.hostname`, the value that goes into the OCAPI\n URL. It does not read the `instance` tool argument. Omitting `instance`, or\n passing `instance: "sandbox"`, has no effect on the decision \u2014 neither one\n selects or proves anything about the target. A dotted `instance` still\n *selects* a host through the documented credential precedence, but the host it\n selects is then validated like any other, so `check_permissions` cannot be\n aimed at a named production instance.\n- It **fails closed.** An unrecognized, malformed, or unparseable hostname is\n refused with HTTP `403`, `error.code: "TARGET_NOT_SANDBOX"`, and\n `error.details.failure_class: "target-not-sandbox"`, before any request leaves\n your machine.\n\nThe accepted hostname forms are:\n\n| Form | Example |\n|---|---|\n| `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.dx.commercecloud.salesforce.com` | `zyis-001.dx.commercecloud.salesforce.com` |\n\nAnything else is refused. In particular, hosts whose leading label names an\nenvironment (`production-\u2026`, `staging-\u2026`, `development-\u2026`) are rejected even\nwhen they otherwise fit a form above, and the legacy `*.demandware.net` domain\nis not accepted at all \u2014 sandbox, staging, and production instances share that\ndomain with no suffix that separates them.\n\n### Read and write profiles\n\n`sfcc` and `sfcc-write` are **independent** groups. Neither implies the other.\n\n| `BRIDGE_MCP_PROFILE` | SFCC tools registered |\n|---|---|\n| unset / `core` | none |\n| `estimation` | none \u2014 this group adds only `estimate_epic`, which is not an SFCC tool |\n| `sfcc` | `sfcc_setup_status`, `check_permissions`, the 8 OCAPI read tools, and `sfcc_log_query` |\n| `sfcc-write` | the 9 destructive write tools **only** \u2014 it does not imply `sfcc`, so neither diagnostic and no read tool is registered |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable, and it also includes `estimation` |\n\n**`check_permissions` needs `sfcc`, even when you are diagnosing writes.** It is\nthe tool that prints the mutation-grant OCAPI Settings JSON on a 401/403, but it\nis registered by the `sfcc` group, not `sfcc-write`. An `sfcc-write`-only session\ntherefore cannot run it. Use `BRIDGE_MCP_PROFILE=sfcc,sfcc-write` whenever you\nneed to diagnose write grants.\n\n**Diagnostics moved into `sfcc`.** `sfcc_setup_status` and `check_permissions`\nused to register unconditionally, on every session including non-SFCC ones. They\nno longer do. The accepted consequence is that a session without `sfcc` can no\nlonger ask the server why SFCC tools are missing \u2014 run `npx -y @bridge_gpt/mcp-server doctor`\nfor that instead, which reports the resolved profile without needing any tool.\n\n**Migration.** Enabling `sfcc` used to register the nine write tools as well. It\nno longer does. If you were relying on SFCC writes through\n`BRIDGE_MCP_PROFILE=sfcc`, change it to `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`.\nUsers of `BRIDGE_MCP_PROFILE=full` keep write access and need no change.\n\n### Tools\n\nTwenty tools in total, none of them registered unconditionally: the `sfcc` profile\'s **read-only** surface (the two diagnostics, eight OCAPI reads, and `sfcc_log_query`), plus the nine destructive writes that only the separate `sfcc-write` profile registers \u2014 see [Read and write profiles](#read-and-write-profiles). Every one of them is bounded to a developer sandbox by the same invocation-time check. All twenty are enumerated below.\n\nAn oversized response is saved in full to `BAPI_DOCS_DIR/sfcc/` and replaced by a parseable JSON descriptor \u2014 `truncated: true`, the `saved_path` it was written to, and the `page` metadata (`returned`, `total` when OCAPI supplied one, `has_more`) \u2014 so the collection metadata survives even though the data itself is on disk. If that save fails, the complete payload is returned inline instead, still as parseable JSON.\n\nAttribute-definition reads and writes can return an attribute\'s `default_value` at `projection: "full"`, and Bridge withholds it \u2014 every key is preserved except that one, whose value becomes `[REDACTED_BY_BRIDGE]` \u2014 from the inline response, the saved file, and a successful write echo alike. Attribute defaults are intentionally unavailable through this MCP surface; Business Manager is the supported path to read one.\n\n**Diagnostics** (registered by the `sfcc` profile)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (read/search grants for the `sfcc` tools, mutation grants for the `sfcc-write` tools). An explicit `instance` hostname is still subject to the sandbox check below.\n\n**System object model \u2014 reads** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one system object type\'s definition.\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**System object model \u2014 writes** (needs the `sfcc-write` profile; every one is a destructive write, sandbox only)\n- `system_object_attribute_definition_create` \u2014 create an attribute definition via `PUT /system_object_definitions/{type}/attribute_definitions/{id}`.\n- `system_object_attribute_definition_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_group_create` \u2014 create an attribute group via `PUT /system_object_definitions/{type}/attribute_groups/{id}`.\n- `system_object_attribute_group_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_assign_to_group` \u2014 assign an existing attribute definition into a group via `PUT \u2026/attribute_groups/{group}/attribute_definitions/{def}`.\n- `custom_preference_definition_create` \u2014 define a custom site or organization preference via `PUT /system_object_definitions/{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}`.\n\n**Custom object definitions** (reads need `sfcc`; the two writes need `sfcc-write`)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type (`default_value` withheld). OCAPI cannot enumerate custom object type *IDs* directly, so `object_type` must be known \u2014 but it is discoverable: call `system_object_list` at `projection: "full"` for each custom type\'s `display_name` and `attribute_definition_count`, derive a candidate id (e.g. strip spaces from `"Product Quality Result"` \u2192 `ProductQualityResult`), and confirm it by checking that this tool\'s returned attribute count matches that row\'s `attribute_definition_count`.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type (`default_value` withheld). Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability. Same discovery path as above applies to `object_type`.\n- `custom_object_definition_attribute_create` \u2014 **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.\n- `custom_object_definition_attribute_update` \u2014 **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH \u2026/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.\n\n**Site preferences** (reads need `sfcc`; the write needs `sfcc-write`; sandbox only)\n- `site_preference_group_list` \u2014 list the preference groups on a site. This is the discovery tool the other two reads depend on: both take a group, and this is how you find one.\n- `site_preference_get` \u2014 list the preference **identifiers** in a group.\n- `site_preference_search` \u2014 search/filter preference identifiers within a group.\n- `site_preference_values_set` \u2014 **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.\n\n> **Site preference values are write-only through this surface.** `site_preference_get` and `site_preference_search` return **ids only, never values** \u2014 an unset preference and one set to the empty string are indistinguishable. So you can *set* a value with `site_preference_values_set` and have no way to read it back through an MCP tool. Business Manager is the supported path to read a preference value.\n\n**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-cli`**, shelled out to directly: `b2c logs get --since <window> --search <q> --json`. There is no MCP alternative to reach for \u2014 `@salesforce/b2c-dx-mcp` ships **no `logs_*` tool** as of 1.1.2, and every log workflow in the vendor toolkit goes through the CLI anyway (see the [vendor manual](../docs/mcp/b2c-commerce-developer.md)). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted CLI output to an LLM.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`. For existing ticket keys, `/review-and-start` (see [Usage Documentation \u2192 Review and Start](#regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `review-and-implement` reviews the ticket, then hands approval to a fresh implementation session in the same worktree. `--auto` applies to both phases. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; Claude/Cursor use their registry aliases and repository overrides, while Codex uses the authenticated Bridge catalog. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing when its alias is available. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, lifecycle ledger events, and a supervisor peer tab). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent. Claude is the default. Cursor with `--auto` runs headlessly after a bounded authentication/readiness check; manual Cursor runs remain interactive for human review gates. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-6 \u2192 basic, 7-10 \u2192 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 \u2192 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.\n\nFor 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.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses only the per-ticket difficulty lookup and applies that tier to every ticket. Claude/Cursor use the same alias resolution as normal routing; Codex uses the fetched catalog. It is never a raw model alias, and no API key or credential belongs in the spawned command. A malformed tier requests the premium fallback when its alias is available; otherwise the agent default launches. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./docs/CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand \u2014 titled **`bridge doctor \u2014 read-only diagnostics`** \u2014 that diagnoses your whole Bridge install without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nThe report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.\n\nThe report also includes a **Claude login** advisory: whether the host\'s own\n`~/.claude.json` carries a login marker. This is informational only \u2014 it never\nblocks the doctor run and cannot guarantee the next worker spawn will\nauthenticate. See\n[Claude login for conductor workers](#claude-login-for-conductor-workers).\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n**`claude-review` workflow lineage (advisory, read-only).** `doctor` also reports\nwhether the current branch\'s `.github/workflows/claude-review.yml` matches the\nrepository default branch\'s copy. This matters because\n`anthropics/claude-code-action` refuses to run when the two differ \u2014 so a\nlong-lived base branch that has fallen behind silently blinds automated review for\n**every pull request based on it**, even though those pull requests change no\nworkflow file at all. The probe runs `git symbolic-ref`, `git rev-parse`, and two\n`git show` reads against the local object database: it **fetches nothing, spawns\nnothing, and creates no worktree**, and an unresolvable ref simply reports the\ncomparison as unavailable. It is **advisory only** \u2014 it is registered as a\ndoctor-only diagnostic and **never changes `doctor`\'s required-prerequisite exit\ncode**, whether it finds drift or cannot complete the comparison. Spawning a\nworker against a stale base prints the same advisory and **still spawns the\nworker**. Remedy and full diagnosis:\n`docs/claude/runbooks/claude-review-base-branch-drift.md`.\n\n### `drive-epic`\n\nThe one conductor entry point every Bridge surface names. Give it an epic key and\nit reads conductor readiness for your repository and routes to the single path\nyour project can actually run:\n\n```\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC>\n```\n\nYou are never asked to choose. Bridge currently has two conductors and a standing\nrule that they must never operate on the same epic \u2014 two transition authorities on\none epic wedge it permanently \u2014 so the choice is made structurally rather than by\njudgement. Readiness green routes to the v2 bootstrap below (pass `--plan-file`\nand `drive-epic` runs it for you); readiness not green prints the interactive\npilot instruction instead. If readiness is **unknown** \u2014 unreachable,\nunauthorized, or malformed \u2014 it escalates and prints no conductor invocation at\nall, because an unknown owner is not the same as a not-ready one. No branch,\nincluding every error path, ever offers you two paths.\n\n`drive-epic` owns no branch behavior of its own \u2014 it forwards `--feature-branch\n<name>`, `--into-base`, and `--attended` verbatim to `setup-epic`, which decides\nthe strategy and the posture (see below). With none of them, a multi-ticket epic\nruns on `epic/<KEY>`, unattended.\n\n**The composed bring-up is two-phase (BAPI-1102).** With `--plan-file` on a\nrepository whose readiness is missing only the runtime facts, one invocation runs,\nin order: the **control plane** (Bridge API server + reconciler, each gated on its\nown readiness), then `setup-epic` to create and approve the run, then **executor\nlanes scoped to that run** with `--epic-run-id <run>`, then the **dead-man\nobserver** last, then the ready banner. The order is forced: an executor lane must\ncarry an explicit claim scope, and the run it is scoped to does not exist until the\nserver is up and `setup-epic` has run.\n\nA composed plane serves **exactly one** run; start a second plane for a second\nepic. If `setup-epic` fails, the control plane is left running and **no executor\nlane is spawned**. If the lanes fail to become ready, the control plane and the\ncommitted run are both left intact and the observer is not started \u2014 nothing is\nrolled back, because a local process that failed to start says nothing about\nwhether the run is valid.\n\n"Plane ready" reports **processes**, not dispatchability: if the run cuts an epic\nbranch, the reconciler holds ticket dispatch until that branch\'s index scope\nfinishes preparing.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\n\n### `plane up` \u2014 claim scope is required\n\n`plane up` starts the runtime on its own, and every executor lane it starts needs\nan explicit claim scope:\n\n```\nnpx -y @bridge_gpt/mcp-server plane up --epic-run-id <run-id> # repeatable\nnpx -y @bridge_gpt/mcp-server plane up --repo-wide # or repository-wide\n```\n\nThe two are mutually exclusive. Asked to start lanes with neither, `plane up`\n**refuses before spawning anything**, in the executor\'s own words. That is not a\nnew restriction \u2014 an executor started with no claim scope has refused to start\nsince BAPI-1026 \u2014 it is that refusal moved to where it is actionable, instead of\nsurfacing as a member crash and a full plane rollback.\n\n`plane status` reports the lifecycle phase (control plane starting/ready, lanes\nstarting/ready, observer active, plane ready) and the epic run the plane is bound\nto. `plane down` handles every intermediate phase: it stops the bound run first,\nthen signals the recorded process group.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe 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.\n\n**The command above uses an epic branch.** For a plan with **two or more** nodes and\nno branch flag, `setup-epic` derives `epic/<KEY>`: every child-ticket PR targets that\nbranch, and one draft integration PR targets the repository base branch. This holds on\nevery path, CI and `--json` included. A single-node plan keeps the repository base\nbranch, matching plain `start-tickets`.\n\n> **Behavior change (BAPI-1009).** The same no-flag command previously ran children\n> directly into the repository base branch. **Pass `--into-base` to restore that**, if\n> your automation depends on it. Pass `--feature-branch <name>` to override the derived\n> name, or to force a branch for a single-node plan. Passing both is a parse error,\n> refused before any file read, credential resolution, or network call.\n\n**A run that selects an epic branch is created UNATTENDED (BAPI-1102).** Its\n`policy_json` carries `posture: "unattended"` and `v2_auto_merge_enabled: true`, so\nchild PRs merge into the epic branch without parking for a human at every gate. The\n**integration PR into the repository base branch stays human-gated.** The run\'s CI\ngate is stamped **server-side at first approval** \u2014 from your repository\'s declared\ndefault, or, when it declares none, as an explicit recorded `no_ci_gate` mode.\n\n> **Behavior change (BAPI-1102).** Pass `--attended` to restore the previous\n> composition: `base_branch` plus an explicitly requested `--review-policy`, with no\n> posture and no auto-merge authorization. `--into-base`, single-node plans, and\n> branch-silent invocations are **unchanged** \u2014 they send no `policy_json` at all,\n> byte for byte as before. A `--policy-file` still supersedes composition entirely;\n> combining `--attended` with a file that declares `posture: "unattended"` is a named\n> refusal rather than a silent winner.\n\nAn unattended run has two one-time **repository** prerequisites \u2014 consent\n(`unattended_epic_implementer_allowed`) and a verified notify webhook default \u2014 plus\nconfirmed default-branch review/CI workflows. Each is refused by name **before any\nrun row is created** (`target_not_allowed`, `webhook_unverified`,\n`repository_readiness_unconfirmed`), and `drive-epic` readiness and `doctor` both\nlist them with their current state. `notify.local_sink` does not satisfy the webhook\nprerequisite.\n\n> **Rollout note.** The client-side default and the server-side admission,\n> stamping, and readiness changes must be released as **one compatible unit**. The\n> new default\'s shape (`v2_auto_merge_enabled: true` with no `required_checks`) is\n> exactly what the create route refused before BAPI-1102, so shipping the client\n> half alone would 422 every unattended run at `POST /jira/epic-runs/runs`.\n> Deploying only the server half is safe and inert: it narrows two refusals and adds\n> a first-approval stamp, and **rewrites no existing run**. No database migration and\n> no new environment variable is introduced \u2014 `config_projects.unattended_epic_implementer_allowed`\n> and `epic_supervisor_setup.done_gate_config` already exist and are read as-is.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### `implement-epic`\n\nThe deterministic half of the `/conduct-epic` loop: it owns the epic branch, a\nversioned local checkpoint, a per-epic lock, and the read-only probes the loop\ndecides on. It never creates or mutates an `epic_run` \u2014 that is the server-side\nv2 reconciler\'s job, and `init` refuses to start when one is already active.\nThis is EPIC-LEVEL orchestration, distinct from the ticket-level\n`implement-ticket` command.\n\n```\nnpx -y @bridge_gpt/mcp-server implement-epic <verb> [flags]\n```\n\n**Verbs**\n\n| Verb | Flags |\n| --- | --- |\n| `init <EPIC>` | `--tickets K1,K2,\u2026` (required), `--base-branch <b>`, `--checkpoint-path <p>`, `--dry-run`, `--json` |\n| `status <EPIC>` | `--json` (required), `--checkpoint-path <p>` |\n| `checkpoint set <EPIC>` | `--ticket <KEY>` (required), `--field <name> <value>` (repeatable), `--journal "<line>"`, `--checkpoint-path <p>` |\n| `finish <EPIC>` | `--checkpoint-path <p>`, `--json` |\n| `spawn <EPIC>` | `--ticket <KEY>` and `--prompt-file <path>` (required), `--agent claude\\|cursor-agent`, `--checkpoint-path <p>`, `--json` |\n\n**Local state.** Everything lives *outside* the repository, under\n`~/.config/bridge/conduct/<repo>/` (honoring `XDG_CONFIG_HOME`), so it resolves\nidentically from the main checkout and from any worktree and can never be\ncommitted by an agent running `git add`:\n\n| Path | Purpose |\n| --- | --- |\n| `<EPIC>.json` | the version-1 checkpoint (file `0600`, directory `0700`) |\n| `<EPIC>.json.prev` | the previous valid checkpoint, retained on every write |\n| `<EPIC>.lock` | the per-epic lock |\n| `<EPIC>/prompts/<KEY>-<kind>-<n>.md` | prompt files the caller writes for `spawn` |\n\n`status` prints the resolved `checkpoint_path`. To unpark a run a human edits the\ncheckpoint (`needs_human` \u2192 `null`, plus the ticket\'s `status`/counters);\n`last_seen_head`, `ci_last_poll`, and `lock` are observational and are never\nhand-edited.\n\n**`init` runs ONE preflight** that reports *every* failure in a single pass and\nwrites nothing unless all of them pass: `gh auth status`; Worktrunk resolves\n(honoring `BAPI_WORKTRUNK_BIN`); Bridge credentials resolve; `auto_merge_enabled`\nis on \u2014 or is turned on by PUTting the *complete* effective config back with just\nthat flag flipped, which prints a line beginning `announced:`; at least one\nrequired CI check exists (an empty required set would make the done gate pass\nvacuously); no active server-side `epic_run` for the key; the lock is free or its\nowner is provably dead; the base branch exists on `origin` after `git fetch`; and\nthe indexed-branch override is either absent or this epic\'s own \u2014 a re-`init`\nafter a crash is accepted and its `original_base_branch` becomes the default base,\nwhile a *foreign* override is refused by name. `resolve-ci-checks` is called\nexactly once either way, because that call is what warms the `poll-ci-checks`\ncache the first `status` depends on. Only then does `init` push\n`epic/<EPIC>` to `origin` at the fetched base tip (no local checkout), repoint the\nindex, write the checkpoint, and take the lock. `--dry-run` prints the validated\nplan and mutates nothing. A second `init` refuses with `already initialized`.\n\n**Failure posture is split on purpose.** In `status`, each probe fails *open*: a\n`gh`, CI, review, or parse failure leaves that sub-object `null`, adds an entry to\n`probe_errors`, and the command still exits `0` with a complete object \u2014 the loop\nmust be able to read its own checkpoint during a GitHub outage. Everything else\nfails *closed*: a corrupt or wrong-version checkpoint makes every verb but `init`\nexit non-zero **without rewriting it**, and `checkpoint set`, `spawn`, and\n`finish` refuse a lock held by another live process. `status` never takes the lock.\n\n**Exit codes.** `0` on success \u2014 including a missing checkpoint\n(`checkpoint_exists: false`) and an idempotent second `finish`. Non-zero on any\nother failure, with a one-line reason on stderr. With `--json`, stdout is exactly\none JSON object carrying `ok`.\n\n**Credentials** resolve only from `BAPI_API_KEY` or the user-scoped\n`bapi:<repo>` credential target, travel only in the `X-API-Key` header, and never\nappear in a command argument, in stdout/stderr, or in a journal line.\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `epic-implementer install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**. (The event-ledger CLI was renamed from `conductor` to `epic-implementer`; the retained `conductor` bin name is now a fixed migration pointer.)\n\n> The v1 `epic-implementer epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `epic-implementer doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` (server, installer) | No | `https://bridgegpt-api.com` | Bridge API base URL. The MCP server and the `install` CLI both fall back to the production default |\n| `BAPI_BASE_URL` (`executor` subcommand) | **Yes** | _(none)_ | The `executor` deliberately has **no** production fallback \u2014 it refuses to start rather than guess a target |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `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 |\n| `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` \u2192 the connected client\'s MCP `roots/list` \u2192 `CLAUDE_PROJECT_DIR` \u2192 `process.cwd()`. Several paths *write* it into a generated registration (`--init`, host-config provisioning, the worktree `mcp-invoke` shim) \u2014 that is provenance, not a runtime default |\n| `SFCC_HOSTNAME` | No | _(none)_ | SFCC sandbox hostname. Part of the environment credential tier \u2014 `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` |\n| `SFCC_CLIENT_ID` | No | _(none)_ | Account Manager API client id. See `SFCC_HOSTNAME` \u2014 all three are needed together. Also required on its own when a tool is called with an explicit dotted `instance` |\n| `SFCC_CLIENT_SECRET` | No | _(none)_ | Account Manager API client secret. See `SFCC_HOSTNAME` \u2014 all three are needed together. Never sent to Bridge; it goes only to the Account Manager token endpoint |\n| `CLAUDE_CODE_OAUTH_TOKEN` | No | _(none)_ | The supported headless authentication input for conductor workers. Export it into the **executor process\'s own** environment; Bridge forwards it unchanged into the worker and stores it nowhere \u2014 no credential-store entry, no disk, never sent to Bridge. See [Claude login for conductor workers](#claude-login-for-conductor-workers) |\n| `BAPI_INSTALL_DEBUG` | No | _(unset)_ | Set to any non-empty value to unlock raw diagnostics in `install` and the `apply_install_manifest` path \u2014 the underlying error message and stack behind an `unexpected error` summary. The installer\'s own failure text tells you to set it |\n| `BAPI_SIGNUP_EMAIL` | No | _(none)_ | Selects the self-serve signup route without `--email`. Precedence: `--email` first, then this variable, then the visible interactive prompt |\n| `BAPI_INVITE` | No | _(none)_ | Invite code for `install`, for scripting. Like `--invite <code>`, it exposes the code to your shell history and the process list \u2014 prefer bare `--invite` and the hidden prompt |\n| `BAPI_PLANE_PYTHON` | No | `python` | Executable used for the Python members of `plane up`. Point it at a venv interpreter when `python` on `PATH` is not the one you want |\n| `BAPI_PLANE_UVICORN` | No | `uvicorn` | Executable used for the server member of `plane up` |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `BAPI_MCP_UPDATE_CHECK_ENABLED` | No | _(enabled)_ | MCP-local kill switch for the cached update check run at startup. Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip it entirely \u2014 no npm registry request, no update-cache read/write, no stderr advice, and no `tools/list` advisory decoration. Any other value is treated as enabled. Useful behind a firewall/registry-restricted network, and used by the payload-measurement harness so a stale local cache can never perturb a capture |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `estimation` (+ `estimate_epic`, which is **hidden by default** \u2014 `/estimate-epic` needs this group), `sfcc` (+ the 8 heavy SFCC read tools, `sfcc_log_query`, and the `sfcc_setup_status`/`check_permissions` diagnostics \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)), `sfcc-write` (+ the 9 destructive SFCC write tools \u2014 independent of `sfcc`, which does not enable them; see [Read and write profiles](#read-and-write-profiles)), and `full` (shortcut that expands to every group, **including `estimation` and `sfcc-write`**). Example: `sfcc,conductor`; use `sfcc,sfcc-write` for reads plus writes. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup, so **changing this variable requires restarting the MCP server**. That is separate from the learn/onboarding tools, which are gated per session and can appear mid-session without a restart (see [Learn/onboarding tool availability](#learnonboarding-tool-availability)). Conductor/epic sessions launched via `start-tickets --conductor` **merge** `conductor` into the parent process\'s already-resolved groups at the spawn boundary rather than replacing them \u2014 a project running on `sfcc` spawns workers on `core,sfcc,conductor`. A normal `start-tickets` run stays on `core`. |\n\nEnvironment values are **trimmed**, and only a non-empty result wins. A\nwhitespace-only `BAPI_API_KEY` therefore does not override anything: it falls\nthrough to credential-store resolution exactly as an unset variable would.\n\n## Learn/onboarding tool availability\n\nThree tools are **gated per session** rather than by profile:\n\n- `apply_install_manifest`\n- `invite_member`\n- `regenerate_directory_map`\n\nThey are registered in the `core` profile like any other core tool, but the\nserver starts them **disabled**, so a client that honors mid-session tool-list\nchanges does not see them in its first `tools/list`. They are revealed \u2014 all\nthree together, once per session \u2014 by either of two successful requests:\n\n1. a successful **`get_install_manifest`** call, or\n2. a **`get_pipeline_recipe`** request for the **`learn-repository`** pipeline.\n\nOn reveal the server emits `notifications/tools/list_changed`, so a client that\nhonors it picks the three tools up **without a restart**.\n\n`get_install_manifest` and `persist_routing_credential` stay ungated in `core` at\nall times, so the install and `start-tickets` flows always have their entry\npoints. Only the first of those two is a trigger \u2014 `persist_routing_credential`\nnever reveals anything; it is listed here because `/start-tickets` calls it and\nit therefore must never be gated.\n\n**Unrecognized clients get all three at startup.** The hide-then-reveal behavior\napplies only to clients verified to honor mid-session `tools/list_changed`. Any\nother client \u2014 including one that sends no name, a blank name, or a name the\nserver does not recognize \u2014 is enabled at session initialization instead and\ntherefore sees all three tools in its **first** `tools/list`. This is deliberate:\nthe failure mode of guessing wrong should be an extra tool, never a permanently\nmissing one.\n\n**A failed request changes nothing.** Only a genuinely successful manifest\nreveals the tools \u2014 the response must parse as JSON and carry a nonblank\n`snapshot_token`. An error response, a non-JSON body, or a manifest without a\ntoken leaves the three tools hidden, and the response you receive is unchanged by\nthe check either way. Likewise, only the exact `learn-repository` pipeline is a\ntrigger; other recipes and failed lookups reveal nothing.\n\n**Capability filtering still applies independently.** Revealing these tools does\nnot override the backend capability gate described below. The effective surface\nis the *intersection* of the two, so a tool can be enabled for the session and\nstill be absent from `tools/list` because this repository lacks the integration\nit needs. That is expected behavior, not a bug \u2014 check the integration first.\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n### Claude login for conductor workers\n\nConductor workers are **not** isolated into a private Claude configuration\ndirectory \u2014 they run with the executor host\'s own `HOME`, so a worker\nauthenticates the same way any interactive `claude` invocation on that host\ndoes. The prerequisite is simple: run\n\n```bash\nclaude login\n```\n\non the executor host, once, the normal way. Bridge never stores, resolves, mints,\nrotates, validates, or diagnoses this credential \u2014 it is entirely the operator\'s\nown Claude CLI state, exactly as if you were running `claude` at the terminal\nyourself.\n\n**Headless hosts.** If the executor host has no interactive login session\navailable (a service-launched executor, a CI-style runner), export\n`CLAUDE_CODE_OAUTH_TOKEN` into the **executor process\'s own environment**\nyourself before starting it:\n\n```bash\nexport CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)" # run once, wherever you can browser-login\n```\n\nBridge forwards that value **unchanged**, byte-for-byte, into the direct worker\nprocess environment \u2014 nothing else. It is never written to disk, never placed in\na generated launchd/systemd service unit, never placed in project configuration\n(`.mcp.json` / `.cursor/mcp.json`), and never sent to Bridge servers. There is no\ncredential store entry for it and no lifecycle tracking: expiry, rotation, and\nvalidity are entirely the operator\'s own responsibility, the same as any other\nvalue you choose to export into a process environment.\n\n**`ANTHROPIC_API_KEY` is never forwarded to a worker**, under any circumstance \u2014\nthere is no fallback path for it.\n\n`mcp-server doctor` reports a single advisory **Claude login** line \u2014 whether\n`~/.claude.json` on the host it runs on carries a login marker. This is\ninformational only: it cannot confirm the next worker spawn will authenticate,\nand it never blocks the doctor run or changes its exit code.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe authoritative tool catalog covers **75 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).\n\n- **Connectivity & identity** \u2014 `ping` (its JSON also carries `docs_dir`, `role`, and `customer_type`)\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/architecture/fsd/prd, where `architecture` is an alias of `tdd`), `request_ticket_review` (writes both `get_clarifying_questions` and `get_ticket_critique`), `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Bounded waits** \u2014 `wait_for_ticket_review`, `wait_for_ci_checks`. Each returns `state: "ready" | "pending" | "error"`; review waits default to 20 s and CI waits default to 240 s (both accept up to 240 s explicitly). `pending` is a normal, resumable result you call again on, never a failure. They exist so an agent can wait without a foreground `sleep` and without binding a long server-side operation to the 900 s MCP client deadline. `wait_for_ci_checks` reports only that checks are **terminal** \u2014 deciding pass or fail stays with the caller.\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`, `get_ticket_state_tree` (live repo-wide lifecycle + dependency tree; read-only, no mutation parameter)\n- **Jira status** \u2014 `update_jira_status` (pass `status: "auto"` to resolve the configured post-PR status server-side)\n- **Repository & CI** \u2014 `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)\n- **Pipelines & automation** \u2014 `get_pipeline_recipe` (returns a fully resolved recipe the agent executes step-by-step)\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `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` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `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>"` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`get_pipeline_recipe` returns a fully resolved recipe: an ordered `steps` array\nplus `total_steps`, `auto_approve`, and `execution_mode`. Each step is either an\n`mcp_call` (a tool name with resolved params) or an `agent_task` (instruction\ntext). `execution_mode` is `inline` \u2014 the calling agent executes every step\nitself and keeps its own durable record through `record_phase_result` /\n`record_checkpoint`. There is no server-side orchestrator and no run to resume.\n';import{readdir,readFile}from"fs/promises";import path from"path";var EXECUTION_MODE_VARIABLE="execution_mode";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),executionMode=options?.executionMode??"inline";variables={...variables,[EXECUTION_MODE_VARIABLE]:executionMode};let resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content,base.instruction_file=step.instruction_file}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Recipes are re-entrant: a recovery run may begin again at step 1, and a step whose tool reports a server-side reuse (e.g. reused: true) has completed successfully \u2014 treat that as the expected fast path, not a failure, and continue. A step can succeed (no on_error handling applies) while its own returned content is a JSON envelope shaped like error: "GATEWAY_TIMEOUT", status: 504, and a recovery_get field \u2014 recognize that shape and read it as "server-side processing may still be running", not as a failure or an invitation to retry. Poll the named retrieval tool (or the recovery_get URL) with the same artifact identifier until a terminal response is reached, and never reissue the original request tool. Only fall back to on_error handling if that retrieval itself terminally fails. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,execution_mode:executionMode,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile(path.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile(path.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
7875
7875
  ${errors.join(`
7876
7876
  `)}`);continue}let pipeline=parsed,key=file.replace(/\.json$/,""),hasInvalidRef=!1;for(let step of pipeline.steps)if(step.type==="agent_task"&&step.instruction_file){let content=mergedInstructions[step.instruction_file];if(content===void 0){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" not found.`),hasInvalidRef=!0;break}if(!hasTerminalReturnSection(content)){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" is missing a terminal "## Return" section (required by BAPI-275 agent_result contract).`),hasInvalidRef=!0;break}}hasInvalidRef||(userPipelines[key]=pipeline,userPipelineKeys2.add(key))}}catch(err){return err.code!=="ENOENT"&&console.error(`Warning: could not read pipelines directory "${pipelinesDir}": ${err.message}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}return userPipelineKeys2.size>0&&console.error(`Loaded ${userPipelineKeys2.size} user pipeline(s) from ${pipelinesDir}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}init_env_flags();var REVIEW_SYNTHESIS_PHASE1_ENV_VAR="REVIEW_SYNTHESIS_PHASE1_ENABLED",PHASE1_INSTRUCTION_PAIRS=[["evaluate-and-recommend.md","phase1/evaluate-and-recommend.md"],["capture-review-decisions.md","phase1/capture-review-decisions.md"]];function reviewSynthesisPhase1Enabled(env=process.env){return parseDefaultOffEnvFlag(env[REVIEW_SYNTHESIS_PHASE1_ENV_VAR])}function selectPhase1Instructions(instructions,env=process.env){if(!reviewSynthesisPhase1Enabled(env))return instructions;let selected={...instructions};for(let[legacyKey,phase1Key]of PHASE1_INSTRUCTION_PAIRS){let phase1Content=instructions[phase1Key];typeof phase1Content=="string"&&(selected[legacyKey]=phase1Content)}return selected}var PLAN_PROVENANCE_CLASSES=["implementation","documentation","unit_tests","e2e_tests","rendered_ui_review","test_gap_review","final_plan_review"],PLAN_PHASES=["produce","pre_pr_verification","post_pr_gap_close"],PLAN_STEP_DISPOSITIONS=["executed","adapted","escalated","unrun-advisory"],MECHANICAL_ADAPTATION_KINDS=["locator-correction","repository-command-correction","equivalent-implementation-recognized"],ESCALATION_ONLY_CATEGORIES=["design","schema","public-api","dependencies","security"],PLAN_CLASS_OWNERSHIP=Object.freeze({implementation:"produce",documentation:"produce",unit_tests:"pre_pr_verification",e2e_tests:"pre_pr_verification",rendered_ui_review:"pre_pr_verification",test_gap_review:"pre_pr_verification",final_plan_review:"pre_pr_verification"}),PRE_PR_VERIFICATION_LIMITS=Object.freeze({maxCorrectionTurns:3,maxChangedFiles:40,maxDiffLines:2e3}),RENDERED_UI_MAX_CYCLES=3,PlanLedgerError=class extends Error{constructor(message){super(message),this.name="PlanLedgerError"}};function isPlainObject(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isPositiveInteger(value){return typeof value=="number"&&Number.isInteger(value)&&value>=1}function validatePlanMetadata(value){if(value==null)throw new PlanLedgerError("plan metadata is absent; routing requires provenance and must not be inferred from plan prose");if(!isPlainObject(value))throw new PlanLedgerError(`plan metadata must be an object, received ${Array.isArray(value)?"array":typeof value}`);if(value.version!==1)throw new PlanLedgerError(`unsupported plan metadata version ${String(value.version)}; expected 1`);let rawParts=value.parts;if(!Array.isArray(rawParts)||rawParts.length===0)throw new PlanLedgerError("plan metadata must carry a non-empty parts array");let parts=[],seenIds=new Set,previousEnd=0,previousId="";for(let[index,raw]of rawParts.entries()){if(!isPlainObject(raw))throw new PlanLedgerError(`plan metadata part at index ${index} is not an object`);let partId=raw.part_id;if(typeof partId!="string"||partId.trim()==="")throw new PlanLedgerError(`plan metadata part at index ${index} has an empty part_id`);if(seenIds.has(partId))throw new PlanLedgerError(`duplicate plan metadata part_id '${partId}'`);seenIds.add(partId);let producer=raw.producer;if(typeof producer!="string"||producer.trim()==="")throw new PlanLedgerError(`plan metadata part '${partId}' has an empty producer`);let provenanceClass=raw.provenance_class;if(typeof provenanceClass!="string"||!PLAN_PROVENANCE_CLASSES.includes(provenanceClass))throw new PlanLedgerError(`plan metadata part '${partId}' has unknown provenance class '${String(provenanceClass)}'`);let startStep=raw.start_step,endStep=raw.end_step;if(!isPositiveInteger(startStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive start_step ${String(startStep)}`);if(!isPositiveInteger(endStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive end_step ${String(endStep)}`);if(endStep<startStep)throw new PlanLedgerError(`plan metadata part '${partId}' has a reversed range ${startStep}-${endStep}`);if(startStep<=previousEnd)throw new PlanLedgerError(`plan metadata part '${partId}' range ${startStep}-${endStep} overlaps or precedes '${previousId}' ending at ${previousEnd}; ranges must be disjoint and ascending`);if(startStep>previousEnd+1)throw new PlanLedgerError(`plan metadata part '${partId}' starts at step ${startStep} but '${previousId}' ended at ${previousEnd}; steps ${previousEnd+1}-${startStep-1} are claimed by no part and would be executed by no phase`);previousEnd=endStep,previousId=partId,parts.push({part_id:partId,producer,provenance_class:provenanceClass,start_step:startStep,end_step:endStep,declared_advisory:raw.declared_advisory===!0})}let totalSteps=value.total_steps;if(typeof totalSteps!="number"||!Number.isInteger(totalSteps))throw new PlanLedgerError("plan metadata total_steps must be an integer");if(totalSteps<previousEnd)throw new PlanLedgerError(`plan metadata total_steps ${totalSteps} is below the highest declared step ${previousEnd}`);let declaredClasses=value.provenance_classes,derivedClasses=[];for(let part of parts)derivedClasses.includes(part.provenance_class)||derivedClasses.push(part.provenance_class);if(Array.isArray(declaredClasses)){for(let declared of declaredClasses)if(!derivedClasses.includes(declared))throw new PlanLedgerError(`plan metadata declares provenance class '${String(declared)}' that no part produces`)}return{version:1,parts,provenance_classes:derivedClasses,total_steps:totalSteps}}function resolveOwnedSteps(metadata,phase){if(!PLAN_PHASES.includes(phase))throw new PlanLedgerError(`unknown phase '${phase}'`);let owned=[];for(let part of metadata.parts)if(PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)for(let step=part.start_step;step<=part.end_step;step+=1)owned.push(step);return owned}function resolveOwnedParts(metadata,phase){return metadata.parts.filter(part=>PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)}function assertStepClassCoverage(metadata,ownership=PLAN_CLASS_OWNERSHIP){let unowned=[];for(let part of metadata.parts){let owner=ownership[part.provenance_class];if(owner===void 0){if(part.declared_advisory)continue;unowned.push(`class '${part.provenance_class}' (part '${part.part_id}', steps ${part.start_step}-${part.end_step}) has no executing phase and is not declared advisory`);continue}PLAN_PHASES.includes(owner)||unowned.push(`class '${part.provenance_class}' is mapped to unknown phase '${String(owner)}'`)}if(unowned.length>0)throw new PlanLedgerError(`plan step-class coverage failed \u2014 every class a planner can emit must be executed by some phase or declared advisory in the plan:
7877
7877
  ${unowned.join(`
@@ -8018,7 +8018,7 @@ Options:
8018
8018
  when omitted; you will be asked to confirm).
8019
8019
  --help Show this message.`;function parseConnectBitbucketArgs(argv){let out={help:!1};for(let i=0;i<argv.length;i+=1){let arg=argv[i];if(arg==="--help"||arg==="-h"){out.help=!0;continue}if(arg==="--repo"){let value=argv[i+1];if(!value||value.startsWith("-"))return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value,i+=1;continue}if(arg.startsWith("--repo=")){let value=arg.slice(7);if(!value)return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value;continue}return arg==="--yes"||arg==="-y"?{ok:!1,error:"connect-bitbucket does not support --yes: connecting a repository always requires an explicit confirmation."}:arg==="--installation-id"||arg.startsWith("--installation-id=")?{ok:!1,error:"connect-bitbucket does not accept --installation-id: the installation is resolved by Bridge from the connection code, not supplied by the caller."}:arg==="--api-key"||arg.startsWith("--api-key=")?{ok:!1,error:"connect-bitbucket does not accept --api-key: credentials resolve only through the Bridge credential store."}:arg.startsWith("-")?{ok:!1,error:`Unknown option: ${arg}`}:{ok:!1,error:`Unexpected argument: ${arg}`}}return{ok:!0,value:out}}function defaultPromptLine2(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function defaultOpenBrowser2(platform,url){let[command,args]=platform==="darwin"?["open",[url]]:platform==="win32"?["cmd",["/c","start","",url]]:["xdg-open",[url]];return new Promise(resolve2=>{try{let child=spawn7(command,args,{stdio:"ignore",detached:!1,shell:!1});child.on("error",()=>resolve2(!1)),child.on("spawn",()=>resolve2(!0))}catch{resolve2(!1)}})}function createDefaultConnectBitbucketDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os21.homedir,isTTY:!!process.stdin.isTTY,readFile:filePath=>readFile14(filePath,"utf-8"),stat:async filePath=>({mode:(await stat9(filePath)).mode}),fetch:globalThis.fetch,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),jitter:()=>Math.random(),promptLine:defaultPromptLine2,openBrowser:url=>defaultOpenBrowser2(process.platform,url),stdout:message=>process.stdout.write(`${message}
8020
8020
  `),stderr:message=>process.stderr.write(`${message}
8021
- `)}}async function resolveConnectBitbucketRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path44.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openBitbucketInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS2=["Connect Bitbucket","Complete Bitbucket Forge install in browser","Verify connection","Choose repository","Confirm connection"];function renderStep2(deps,index){deps.stderr(`[${index+1}/${STEPS2.length}] ${STEPS2[index]}`)}function candidateLabel2(c){let slugForm=`${c.workspace_slug}/${c.repository_slug}`;return c.display_name&&c.display_name!==slugForm&&c.display_name!==c.repository_slug?`${slugForm} (${c.display_name})`:slugForm}var OUTCOME_MESSAGES2={expired:"The connection request expired before the Forge app reported back. Run connect-bitbucket again.",invalid:"This connection request is no longer valid. Run connect-bitbucket again.","no-repositories":"The Bitbucket Forge app installation did not include any repositories Bridge can access. Re-run connect-bitbucket and grant access to at least one repository."},FAILURE_MESSAGES2={network:"Could not reach Bridge API. Check your network, then run connect-bitbucket again.",timeout:"Bridge API did not respond in time. Run connect-bitbucket again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-bitbucket again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-bitbucket again shortly.",deadline:"Timed out waiting for the Bitbucket Forge app. If you completed the install and pasted the code, run connect-bitbucket again to pick up the connection."};function reportNoConnection2(deps,detail){return deps.stderr(""),deps.stderr("No Bitbucket connection was made."),deps.stderr(detail),1}async function chooseCandidate2(deps,candidates){if(candidates.length===1){let only=candidates[0],answer2=(await deps.promptLine(`Connect ${candidateLabel2(only)}? [y/N]: `)).trim().toLowerCase();return answer2!=="y"&&answer2!=="yes"?(deps.stderr(""),deps.stderr("No Bitbucket connection was made. Nothing was changed."),null):only}deps.stderr(""),deps.stderr("Your Bitbucket Forge installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel2(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(answer.toLowerCase()==="q"||!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No Bitbucket connection was made. No repository was selected."),null;let selected=candidates[index-1];return deps.stderr(`Selected ${candidateLabel2(selected)}.`),selected}async function runBitbucketConnectionFlow(deps,api,repoName){renderStep2(deps,0);let minted=await mintBitbucketConnection(api,repoName);if(!minted.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[minted.kind]);renderStep2(deps,1),deps.stderr("Opening Bitbucket\u2026");let opened=await openBitbucketInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection2(deps,opened.error);deps.stderr(`Paste this code into the installed app's admin panel: ${minted.value.state}`),renderStep2(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS2/6e4);deps.stderr(`Waiting for the Bitbucket Forge app\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollBitbucketConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="connected")return deps.stdout(`Connected ${result.repoDisplayName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection2(deps,OUTCOME_MESSAGES2[result.status]??OUTCOME_MESSAGES2.invalid);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection2(deps,OUTCOME_MESSAGES2["no-repositories"]);renderStep2(deps,3);let selected=await chooseCandidate2(deps,candidates);if(!selected)return 1;renderStep2(deps,4);let confirmed=await confirmBitbucketConnection(api,repoName,minted.value.state,selected.workspace_uuid,selected.repository_uuid);if(!confirmed.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[confirmed.kind]);let displayName=confirmed.value.workspaceSlug&&confirmed.value.repositorySlug?`${confirmed.value.workspaceSlug}/${confirmed.value.repositorySlug}`:confirmed.value.repoName;return deps.stdout(`Connected ${displayName}.`),0}async function runConnectBitbucketCli(argv,injected){let deps=injected??createDefaultConnectBitbucketDeps();try{let parsed=parseConnectBitbucketArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE3),1;if(parsed.value.help)return deps.stdout(USAGE3),0;if(!deps.isTTY)return deps.stderr("connect-bitbucket needs an interactive terminal: it opens a browser and asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectBitbucketRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runBitbucketConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No Bitbucket connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`${MCP_PACKAGE_NAME}@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT=`Run /install-bridge now, with no arguments \u2014 that is the entire invocation. You are in the install-spawn context: that is a mode this prompt establishes in plain language, not a command-line option, so never convert it into --context install-spawn or any other flag. The Bridge MCP tools named throughout this command are available only inside this currently running agent session \u2014 never invoke them through Bash or by constructing a direct HTTP request. /install-bridge owns and automatically executes its own internal stages; do not reproduce, narrate as separate manual steps, or hand-execute those stages yourself. Never place the API-key value in a Bash command, its arguments, printed output, a log line, an error message, or a file you write; verify connectivity with the in-session ping MCP tool only \u2014 never through an environment export, echo, or any other shell probe. The command owns its own Stage 2 decision and evaluates it in a fixed order from the install manifest \u2014 do NOT force an outcome. \`configured == true\` is JOIN MODE; otherwise a manifest whose \`greenfield_setup_path\` field is already set is GREENFIELD REPORT-ONLY; otherwise the command's Stage 2b classifies the repository as greenfield or established. In JOIN MODE and in GREENFIELD REPORT-ONLY the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; the closing interaction is the welcome plus the concise capability report. An established classification runs the ordinary fresh-configuration flow unchanged. A greenfield classification hands off to the packaged \`greenfield-setup\` recipe \u2014 the command fetches it with get_pipeline_recipe and completes every returned step in THIS same session, then resumes /install-bridge reporting from the recipe's own read-after-write manifest. Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it. That field is exactly two availability sections in server order \u2014 'Available now' first, then 'Needs setup' \u2014 and each item in them carries display_name, description, how_to_use, and (in the 'Needs setup' section only) availability_text. Render each item's display_name, its description, and its how_to_use verbatim when that value is a non-empty string; render availability_text verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, asterisk, or footnote on an 'Available now' item. An item's \`kind\` ('tool' or 'workflow') is contract metadata only \u2014 render both kinds identically, with no label, icon, or separate list, and never branch on it. Keep an empty section visible with a short neutral line rather than dropping it, and after both sections close with a pointer to the Bridge MCP server README at https://www.npmjs.com/package/${MCP_PACKAGE_NAME} for the complete tool documentation, including when either or both sections are empty. Do not render the obsolete five-section report, and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall back to the complete tool_capabilities catalog or the workflows collection. If concise_tool_capabilities is missing or malformed, take the command's single fallback line and render nothing else in its place. The same manifest response also carries a SEPARATE top-level human_setup field (contract v9) \u2014 an ordered list naming each remaining unconfigured integration and what finishing it unlocks, additive over the same checklist integrations already carries. It is never a third section of concise_tool_capabilities; render it as its own separate block, after the two-section report above. Absent (an older compatible manifest), malformed (not a list, or an entry missing display_text), or a valid empty list all render NOTHING for this block \u2014 no heading, no fabricated completion line, no invented setup advice; the two-section report above still renders unaffected either way. A valid non-empty list renders under the heading 'What finishing setup unlocks', one compact item per entry in the server's given order, using human_setup[].display_text as that item's entire substantive content, byte-for-byte \u2014 never summarized, reworded, sorted, filtered, merged, or recomposed from configure_in, setup_instructions, required_for, integrations, or any tool/workflow catalog. The same server-authority rule the concise capability report already follows extends to this field. Plain-language narration (BAPI-1174): explain each major action you take \u2014 reading the manifest, applying configuration, persisting the routing credential, rendering the capability report \u2014 in a short, plain-language sentence covering what is happening and why it matters to the person running this install, rather than in internal shorthand. Narration you compose yourself must never name an internal stage number, this command's contract version, a raw internal MCP tool identifier (like get_install_manifest, apply_install_manifest, or invite_member), or a manifest field key (like command_contract_version, next_step, or configured) \u2014 describe what is actually happening instead. This restriction is scoped to narration you author: it does not apply to a user-invocable command name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, start-tickets, and the like) or to any value a stage marks as server-authored and rendered verbatim (the concise capability report, human_setup display text, a next_step.command string) \u2014 those are never reworded, curated, or filtered by you either way. In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT print or summarize its SUCCESSFUL result anywhere \u2014 no 'Stored routing credential for \u2026' line, and no restatement of that target or path in any later summary or Return section \u2014 because the install-bridge CLI has already reported where the credential landed and a second sentence is a duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge invocation Stage 6 reports its success exactly as it does today. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. Close with the server's own next step, which you must read at runtime rather than assume. Inspect the \`next_step\` object on the manifest response the flow already used: on the fresh-configuration path that is the Stage 7 post-apply read-after-write manifest; after a greenfield handoff it is the read-after-write manifest the greenfield-setup recipe returned; and in JOIN MODE and GREENFIELD REPORT-ONLY it is the Stage 2 manifest (neither performs a read-after-write). Look at \`next_step.command\`: when it is a NON-EMPTY string, recommend exactly that command, verbatim, in inline code \u2014 do not rename it or substitute a command you happen to know. When it is an empty string, make NO next-command recommendation at all and say nothing in its place: no heading, no placeholder, no 'nothing to do' line. The server decides eligibility (repository state AND whether this caller can run the command), so do NOT apply a role rule or a learned-state rule of your own. Do NOT run the returned command yourself \u2014 only recommend it; running it is the human's next explicit invocation. For supporting context around a recommended command, use the manifest's own next_step.post_install_indexing and next_step.post_install_scheduler text rather than inventing an explanation for it. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count. The two greenfield paths report the same way: GREENFIELD REPORT-ONLY states that greenfield setup was already completed and that zero changes were made, and a completed greenfield handoff states which setup path the recipe recorded \u2014 the recipe made the only apply, so neither path invents an applied count or apply buckets of its own.`,DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:",` npx -y ${MCP_PACKAGE_NAME} install [flags]`,"","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and shows","the next step the server returned \u2014 when it returns one.","If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report plus that same conditional next step (a b2b admin is","additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with two numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1 or 2 (one","blank or invalid answer re-prompts once, then exits with guidance). Answering 1","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). Answering 2 opens a single hidden token prompt: paste whatever","you were given and its SHAPE picks the route \u2014 a bootstrap invite (bapi_inv_\u2026)","is redeemed to CREATE a new project and mint your first admin key, while a","Bridge API key connects to the project that already exists. You are","never asked which kind of token you hold. That question is asked ONLY for a","bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive","terminal keeps the existing deterministic behavior and no prompt.","","On that bare run the route is INFERRED, so it is only announced \u2014 the detected","credential type and where it leads are printed and the run proceeds. There is no","confirmation question and no way to decline, because you never declared a route","for the value to contradict.","","When you DO declare a route with a flag or env var, the confirmations stay, and","the two directions are deliberately asymmetric. Both are detected by SHAPE alone","\u2014 no probe request is sent \u2014 BEFORE any project-name prompt, credential write,","or network call:"," - a bootstrap invite (bapi_inv_\u2026) passed as an API key is announced, then"," confirmed on a terminal before it is redeemed; a non-interactive run keeps"," today's automatic switch but prints the notice."," - an API-key-shaped value passed as an invite is announced and may switch in"," place only after you confirm on a terminal \u2014 non-interactive runs exit with"," guidance instead, so a script is never silently redirected.","A value that is neither shape is left on the route you chose, and is not","announced.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is announced and instead redeemed to"," CREATE a new project and its first admin key \u2014 the same as"," --invite \u2014 so it skips repository lookup entirely (--email"," and --invite remain the explicit, preferred entry points for"," a new project). Passing the flag or setting the env var"," DECLARES the key route, so that switch is confirmed on a"," terminal; the bare run's hidden token prompt infers instead"," and only announces. NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically. If the"," key does not identify exactly one project, the run STOPS \u2014"," it never guesses \u2014 and asks you to re-run with --repo <name>,",` or to rotate the key at ${setupUrl} (Security page).`," If instead the server is older or resolution fails"," transiently, you are asked to type the registered name, with"," NO pre-filled default (and"," --repo is required when stdin is non-interactive). In the"," existing-project flow it MUST match the server-side repo"," registration (it keys the credential store as bapi:<repo>)."," In either new-project flow (--email, --invite, answering 2"," above, or a token that turns out to be an invite) it NAMES"," creates, so you are asked to name a new project \u2014 with a"," suggested default \u2014 rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what answering 2 on a"," bare run reaches. The email is NOT a secret (it is shown as"," you type), but it is never printed to a log. Mutually"," exclusive with --api-key and --invite.",""," VERIFIED: Bridge emails a 6-digit code to that address and"," this command prompts you for it. Nothing is created until"," the code is entered \u2014 no workspace, no project, no API key."," Delivery is asynchronous, so the message arrives shortly"," after you are prompted rather than instantly. The code"," expires 4 hours after it is requested, and you can ask"," for a new one if it does not arrive; earlier codes keep"," working until one is accepted.",""," REQUIRES A TERMINAL: there is no flag or environment"," variable for the code \u2014 either would put a live credential"," into your shell history and process list. For unattended"," setup use --api-key <key> or --invite <token> instead.",""," RESUMABLE: a self-serve run that is interrupted saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running RESUMES that attempt \u2014 at the code prompt"," if the code has not been entered yet, or at the exchange if"," it has \u2014 so a retry never sends a second code and never"," creates a second workspace. The code itself is never saved."," Never copy, display, or hand-remove that record; if the saved"," invite has genuinely expired the CLI asks before discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --codex-profile on|off Opt in to Bridge workflow MCP approvals for"," Codex workers, or explicitly decline. With a"," Codex TTY selection, the installer asks."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent|codex Explicit agent choice for post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no supported auto-launch path (e.g. Copilot) opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it. Codex"," is accepted only for manual continuation: neither"," this bootstrap nor start-tickets launches it yet."," -h, --help Show this help.","","Subcommands:"," install conductor Bootstrap Epic Conductor v2 for this repository"," (BAPI-679). Runs a unified READ-ONLY doctor that"," composes the conductor, executor-preflight,"," install-status, and executor service-unit"," diagnostics with server-side"," readiness (supervisor defaults, empty required-"," check detection, GitHub App credentials and the"," `actions` permission, reconciler tick liveness)."," The resolved BAPI_BASE_URL is confirmed BEFORE"," any network call \u2014 the doctor's readiness GET"," carries the repo API key, and the URL silently"," defaults to production when unset. A fatal"," finding aborts BEFORE any write. A separate"," consent immediately precedes the bootstrap POST,"," which writes the safe project-default supervisor"," posture in one transaction; it optionally"," scaffolds a parameterized claude-review workflow"," (its own separate consent), then closes the local"," out-of-the-box gap in two more phases, each behind"," its OWN consent: tool visibility adds the"," `conductor` token to existing .mcp.json /"," .vscode/mcp.json / .cursor/mcp.json entries (never"," Codex or Copilot, and never creating a file) and"," prints the MCP-client restart advisory; local"," observability installs the managed conductor Git"," hooks and then reports native ledger loadability"," read-only. It finally prints the capability"," matrix and re-runs the doctor. `--dry-run`"," performs NO write of any kind and no write-consent"," prompt (the base-URL confirmation still applies)."," Executor service units are generated by"," `executor install-service`; starting one stays"," operator-managed, so a run reports"," `complete-with-manual-steps` until a live"," executor is actually observed. See"," `install conductor --help`.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
8021
+ `)}}async function resolveConnectBitbucketRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path44.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openBitbucketInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS2=["Connect Bitbucket","Complete Bitbucket Forge install in browser","Verify connection","Choose repository","Confirm connection"];function renderStep2(deps,index){deps.stderr(`[${index+1}/${STEPS2.length}] ${STEPS2[index]}`)}function candidateLabel2(c){let slugForm=`${c.workspace_slug}/${c.repository_slug}`;return c.display_name&&c.display_name!==slugForm&&c.display_name!==c.repository_slug?`${slugForm} (${c.display_name})`:slugForm}var OUTCOME_MESSAGES2={expired:"The connection request expired before the Forge app reported back. Run connect-bitbucket again.",invalid:"This connection request is no longer valid. Run connect-bitbucket again.","no-repositories":"The Bitbucket Forge app installation did not include any repositories Bridge can access. Re-run connect-bitbucket and grant access to at least one repository."},FAILURE_MESSAGES2={network:"Could not reach Bridge API. Check your network, then run connect-bitbucket again.",timeout:"Bridge API did not respond in time. Run connect-bitbucket again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-bitbucket again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-bitbucket again shortly.",deadline:"Timed out waiting for the Bitbucket Forge app. If you completed the install and pasted the code, run connect-bitbucket again to pick up the connection."};function reportNoConnection2(deps,detail){return deps.stderr(""),deps.stderr("No Bitbucket connection was made."),deps.stderr(detail),1}async function chooseCandidate2(deps,candidates){if(candidates.length===1){let only=candidates[0],answer2=(await deps.promptLine(`Connect ${candidateLabel2(only)}? [y/N]: `)).trim().toLowerCase();return answer2!=="y"&&answer2!=="yes"?(deps.stderr(""),deps.stderr("No Bitbucket connection was made. Nothing was changed."),null):only}deps.stderr(""),deps.stderr("Your Bitbucket Forge installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel2(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(answer.toLowerCase()==="q"||!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No Bitbucket connection was made. No repository was selected."),null;let selected=candidates[index-1];return deps.stderr(`Selected ${candidateLabel2(selected)}.`),selected}async function runBitbucketConnectionFlow(deps,api,repoName){renderStep2(deps,0);let minted=await mintBitbucketConnection(api,repoName);if(!minted.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[minted.kind]);renderStep2(deps,1),deps.stderr("Opening Bitbucket\u2026");let opened=await openBitbucketInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection2(deps,opened.error);deps.stderr(`Paste this code into the installed app's admin panel: ${minted.value.state}`),renderStep2(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS2/6e4);deps.stderr(`Waiting for the Bitbucket Forge app\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollBitbucketConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="connected")return deps.stdout(`Connected ${result.repoDisplayName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection2(deps,OUTCOME_MESSAGES2[result.status]??OUTCOME_MESSAGES2.invalid);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection2(deps,OUTCOME_MESSAGES2["no-repositories"]);renderStep2(deps,3);let selected=await chooseCandidate2(deps,candidates);if(!selected)return 1;renderStep2(deps,4);let confirmed=await confirmBitbucketConnection(api,repoName,minted.value.state,selected.workspace_uuid,selected.repository_uuid);if(!confirmed.ok)return reportNoConnection2(deps,FAILURE_MESSAGES2[confirmed.kind]);let displayName=confirmed.value.workspaceSlug&&confirmed.value.repositorySlug?`${confirmed.value.workspaceSlug}/${confirmed.value.repositorySlug}`:confirmed.value.repoName;return deps.stdout(`Connected ${displayName}.`),0}async function runConnectBitbucketCli(argv,injected){let deps=injected??createDefaultConnectBitbucketDeps();try{let parsed=parseConnectBitbucketArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE3),1;if(parsed.value.help)return deps.stdout(USAGE3),0;if(!deps.isTTY)return deps.stderr("connect-bitbucket needs an interactive terminal: it opens a browser and asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectBitbucketRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runBitbucketConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No Bitbucket connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`${MCP_PACKAGE_NAME}@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT=`Run /install-bridge now, with no arguments \u2014 that is the entire invocation. You are in the install-spawn context: that is a mode this prompt establishes in plain language, not a command-line option, so never convert it into --context install-spawn or any other flag. The Bridge MCP tools named throughout this command are available only inside this currently running agent session \u2014 never invoke them through Bash or by constructing a direct HTTP request. /install-bridge owns and automatically executes its own internal stages; do not reproduce, narrate as separate manual steps, or hand-execute those stages yourself. Never place the API-key value in a Bash command, its arguments, printed output, a log line, an error message, or a file you write; verify connectivity with the in-session ping MCP tool only \u2014 never through an environment export, echo, or any other shell probe. The command owns its own Stage 2 decision and evaluates it in a fixed order from the install manifest \u2014 do NOT force an outcome. \`configured == true\` is JOIN MODE; otherwise a manifest whose \`greenfield_setup_path\` field is already set is GREENFIELD REPORT-ONLY; otherwise the command's Stage 2b classifies the repository as greenfield or established. In JOIN MODE and in GREENFIELD REPORT-ONLY the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; the closing interaction is the welcome plus the concise capability report. An established classification runs the ordinary fresh-configuration flow unchanged. A greenfield classification hands off to the packaged \`greenfield-setup\` recipe \u2014 the command fetches it with get_pipeline_recipe and completes every returned step in THIS same session, then resumes /install-bridge reporting from the recipe's own read-after-write manifest. Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it. That field is exactly two availability sections in server order \u2014 'Available now' first, then 'Needs setup' \u2014 and each item in them carries display_name, description, how_to_use, and (in the 'Needs setup' section only) availability_text. Render each item's display_name, its description, and its how_to_use verbatim when that value is a non-empty string; render availability_text verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, asterisk, or footnote on an 'Available now' item. An item's \`kind\` ('tool' or 'workflow') is contract metadata only \u2014 render both kinds identically, with no label, icon, or separate list, and never branch on it. Keep an empty section visible with a short neutral line rather than dropping it, and after both sections close with a pointer to the Bridge MCP server README at https://www.npmjs.com/package/${MCP_PACKAGE_NAME} for the complete tool documentation, including when either or both sections are empty. Do not render the obsolete five-section report, and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall back to the complete tool_capabilities catalog or the workflows collection. If concise_tool_capabilities is missing or malformed, take the command's single fallback line and render nothing else in its place. The same manifest response also carries a SEPARATE top-level human_setup field (contract v9) \u2014 an ordered list naming each remaining unconfigured integration and what finishing it unlocks, additive over the same checklist integrations already carries. It is never a third section of concise_tool_capabilities; render it as its own separate block, after the two-section report above. Absent (an older compatible manifest), malformed (not a list, or an entry missing display_text), or a valid empty list all render NOTHING for this block \u2014 no heading, no fabricated completion line, no invented setup advice; the two-section report above still renders unaffected either way. A valid non-empty list renders under the heading 'What finishing setup unlocks', one compact item per entry in the server's given order, using human_setup[].display_text as that item's entire substantive content, byte-for-byte \u2014 never summarized, reworded, sorted, filtered, merged, or recomposed from configure_in, setup_instructions, required_for, integrations, or any tool/workflow catalog. The same server-authority rule the concise capability report already follows extends to this field. Plain-language narration (BAPI-1174): explain each major action you take \u2014 reading the manifest, applying configuration, persisting the routing credential, rendering the capability report \u2014 in a short, plain-language sentence covering what is happening and why it matters to the person running this install, rather than in internal shorthand. Narration you compose yourself must never name an internal stage number, this command's contract version, a raw internal MCP tool identifier (like get_install_manifest, apply_install_manifest, or invite_member), or a manifest field key (like command_contract_version, next_step, or configured) \u2014 describe what is actually happening instead. This restriction is scoped to narration you author: it does not apply to a user-invocable command name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, start-tickets, and the like) or to any value a stage marks as server-authored and rendered verbatim (the concise capability report, human_setup display text, a next_step.command string) \u2014 those are never reworded, curated, or filtered by you either way. In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT print or summarize its SUCCESSFUL result anywhere \u2014 no 'Stored routing credential for \u2026' line, and no restatement of that target or path in any later summary or Return section \u2014 because the install-bridge CLI has already reported where the credential landed and a second sentence is a duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge invocation Stage 6 reports its success exactly as it does today. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. Close with the server's own next step, which you must read at runtime rather than assume. Inspect the \`next_step\` object on the manifest response the flow already used: on the fresh-configuration path that is the Stage 7 post-apply read-after-write manifest; after a greenfield handoff it is the read-after-write manifest the greenfield-setup recipe returned; and in JOIN MODE and GREENFIELD REPORT-ONLY it is the Stage 2 manifest (neither performs a read-after-write). Look at \`next_step.command\`: when it is a NON-EMPTY string, recommend exactly that command, verbatim, in inline code \u2014 do not rename it or substitute a command you happen to know. When it is an empty string, make NO next-command recommendation at all and say nothing in its place: no heading, no placeholder, no 'nothing to do' line. The server decides eligibility (repository state AND whether this caller can run the command), so do NOT apply a role rule or a learned-state rule of your own. Do NOT run the returned command yourself \u2014 only recommend it; running it is the human's next explicit invocation. For supporting context around a recommended command, use the manifest's own next_step.post_install_indexing and next_step.post_install_scheduler text rather than inventing an explanation for it. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count. The two greenfield paths report the same way: GREENFIELD REPORT-ONLY states that greenfield setup was already completed and that zero changes were made, and a completed greenfield handoff states which setup path the recipe recorded \u2014 the recipe made the only apply, so neither path invents an applied count or apply buckets of its own.`,DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:",` npx -y ${MCP_PACKAGE_NAME} install [flags]`,"","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and shows","the next step the server returned \u2014 when it returns one.","If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report plus that same conditional next step (a b2b admin is","additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with two numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1 or 2 (one","blank or invalid answer re-prompts once, then exits with guidance). Answering 1","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). Answering 2 opens a single hidden token prompt: paste whatever","you were given and its SHAPE picks the route \u2014 a bootstrap invite (bapi_inv_\u2026)","is redeemed to CREATE a new project and mint your first admin key, while a","Bridge API key connects to the project that already exists. You are","never asked which kind of token you hold. That question is asked ONLY for a","bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive","terminal keeps the existing deterministic behavior and no prompt.","","On that bare run the route is INFERRED, so it is only announced \u2014 the detected","credential type and where it leads are printed and the run proceeds. There is no","confirmation question and no way to decline, because you never declared a route","for the value to contradict.","","When you DO declare a route with a flag or env var, the confirmations stay, and","the two directions are deliberately asymmetric. Both are detected by SHAPE alone","\u2014 no probe request is sent \u2014 BEFORE any project-name prompt, credential write,","or network call:"," - a bootstrap invite (bapi_inv_\u2026) passed as an API key is announced, then"," confirmed on a terminal before it is redeemed; a non-interactive run keeps"," today's automatic switch but prints the notice."," - an API-key-shaped value passed as an invite is announced and may switch in"," place only after you confirm on a terminal \u2014 non-interactive runs exit with"," guidance instead, so a script is never silently redirected.","A value that is neither shape is left on the route you chose, and is not","announced.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is announced and instead redeemed to"," CREATE a new project and its first admin key \u2014 the same as"," --invite \u2014 so it skips repository lookup entirely (--email"," and --invite remain the explicit, preferred entry points for"," a new project). Passing the flag or setting the env var"," DECLARES the key route, so that switch is confirmed on a"," terminal; the bare run's hidden token prompt infers instead"," and only announces. NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically. If the"," key does not identify exactly one project, the run STOPS \u2014"," it never guesses \u2014 and asks you to re-run with --repo <name>,",` or to rotate the key at ${setupUrl} (Security page).`," If instead the server is older or resolution fails"," transiently, you are asked to type the registered name, with"," NO pre-filled default (and"," --repo is required when stdin is non-interactive). In the"," existing-project flow it MUST match the server-side repo"," registration (it keys the credential store as bapi:<repo>)."," In either new-project flow (--email, --invite, answering 2"," above, or a token that turns out to be an invite) it NAMES"," creates, so you are asked to name a new project \u2014 with a"," suggested default \u2014 rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what answering 2 on a"," bare run reaches. The email is NOT a secret (it is shown as"," you type), but it is never printed to a log. Mutually"," exclusive with --api-key and --invite.",""," VERIFIED: Bridge emails a 6-digit code to that address and"," this command prompts you for it. Nothing is created until"," the code is entered \u2014 no workspace, no project, no API key."," Delivery is asynchronous, so the message arrives shortly"," after you are prompted rather than instantly. The code"," expires 4 hours after it is requested, and you can ask"," for a new one if it does not arrive; earlier codes keep"," working until one is accepted.",""," REQUIRES A TERMINAL: there is no flag or environment"," variable for the code \u2014 either would put a live credential"," into your shell history and process list. For unattended"," setup use --api-key <key> or --invite <token> instead.",""," RESUMABLE: a self-serve run that is interrupted saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running RESUMES that attempt \u2014 at the code prompt"," if the code has not been entered yet, or at the exchange if"," it has \u2014 so a retry never sends a second code and never"," creates a second workspace. The code itself is never saved."," Never copy, display, or hand-remove that record; if the saved"," invite has genuinely expired the CLI asks before discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --codex-profile on|off Opt in to Bridge workflow MCP approvals for"," Codex workers, or explicitly decline. With a"," Codex TTY selection, the installer asks"," with Yes as the default (press Enter)."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent|codex Explicit agent choice for post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no supported auto-launch path (e.g. Copilot) opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it. Codex"," is accepted only for manual continuation: neither"," this bootstrap nor start-tickets launches it yet."," -h, --help Show this help.","","Subcommands:"," install conductor Bootstrap Epic Conductor v2 for this repository"," (BAPI-679). Runs a unified READ-ONLY doctor that"," composes the conductor, executor-preflight,"," install-status, and executor service-unit"," diagnostics with server-side"," readiness (supervisor defaults, empty required-"," check detection, GitHub App credentials and the"," `actions` permission, reconciler tick liveness)."," The resolved BAPI_BASE_URL is confirmed BEFORE"," any network call \u2014 the doctor's readiness GET"," carries the repo API key, and the URL silently"," defaults to production when unset. A fatal"," finding aborts BEFORE any write. A separate"," consent immediately precedes the bootstrap POST,"," which writes the safe project-default supervisor"," posture in one transaction; it optionally"," scaffolds a parameterized claude-review workflow"," (its own separate consent), then closes the local"," out-of-the-box gap in two more phases, each behind"," its OWN consent: tool visibility adds the"," `conductor` token to existing .mcp.json /"," .vscode/mcp.json / .cursor/mcp.json entries (never"," Codex or Copilot, and never creating a file) and"," prints the MCP-client restart advisory; local"," observability installs the managed conductor Git"," hooks and then reports native ledger loadability"," read-only. It finally prints the capability"," matrix and re-runs the doctor. `--dry-run`"," performs NO write of any kind and no write-consent"," prompt (the base-URL confirmation still applies)."," Executor service units are generated by"," `executor install-service`; starting one stays"," operator-managed, so a run reports"," `complete-with-manual-steps` until a live"," executor is actually observed. See"," `install conductor --help`.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
8022
8022
  `)}function parseInstallBridgeArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getInstallBridgeUsage()};let apiKey,repo,force=!1,dryRun=!1,agentName,invite,email,tools,codexProfile,inviteSupplied=!1,apiKeySupplied=!1,emailSupplied=!1,readValue=(arg,flag,i)=>arg.startsWith(`${flag}=`)?{value:arg.slice(flag.length+1),nextIndex:i}:i+1>=argv.length?{error:`${flag} requires a value.`}:{value:argv[i+1],nextIndex:i+1};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--force"){force=!0;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--api-key"||arg.startsWith("--api-key=")){let r=readValue(arg,"--api-key",i);if("error"in r)return{status:"error",message:r.error};apiKey=r.value,apiKeySupplied=!0,i=r.nextIndex;continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(inviteSupplied=!0,arg.startsWith("--invite="))invite=arg.slice(9);else{let next=argv[i+1];typeof next=="string"&&!next.startsWith("-")&&(invite=next,i+=1)}continue}if(arg==="--email"||arg.startsWith("--email=")){let r=readValue(arg,"--email",i);if("error"in r)return{status:"error",message:r.error};if(r.value.trim().length===0)return{status:"error",message:"--email requires a non-empty value."};email=r.value.trim(),emailSupplied=!0,i=r.nextIndex;continue}if(arg==="--repo"||arg.startsWith("--repo=")){let r=readValue(arg,"--repo",i);if("error"in r)return{status:"error",message:r.error};repo=r.value,i=r.nextIndex;continue}if(arg==="--tools"||arg.startsWith("--tools=")){let r=readValue(arg,"--tools",i);if("error"in r)return{status:"error",message:r.error};let parsed=parseToolsSelection(r.value);if("error"in parsed)return{status:"error",message:parsed.error};tools=parsed.tools,i=r.nextIndex;continue}if(arg==="--codex-profile"||arg.startsWith("--codex-profile=")){let r=readValue(arg,"--codex-profile",i);if("error"in r)return{status:"error",message:r.error};if(r.value!=="on"&&r.value!=="off")return{status:"error",message:"--codex-profile must be on or off."};codexProfile=r.value,i=r.nextIndex;continue}if(arg==="--agent"||arg.startsWith("--agent=")){let r=readValue(arg,"--agent",i);if("error"in r)return{status:"error",message:r.error};if(!isAgentName(r.value))return{status:"error",message:`Invalid --agent value: '${r.value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=r.value,i=r.nextIndex;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. install does not accept positional arguments.`}}return inviteSupplied&&apiKeySupplied?{status:"error",message:"--invite and --api-key are mutually exclusive: a bootstrap invite creates your API key, it does not consume an existing one."}:emailSupplied&&apiKeySupplied?{status:"error",message:"--email and --api-key are mutually exclusive: self-serve signup creates your API key, it does not consume an existing one."}:emailSupplied&&inviteSupplied?{status:"error",message:"--email and --invite are mutually exclusive: use --email for self-serve signup (no pre-issued invite), or --invite to redeem an invite you already have."}:{status:"ok",options:{apiKey,repo,force,dryRun,agentName,invite,inviteMode:inviteSupplied,email,tools,codexProfile}}}function parseToolsSelection(value){let raw=value.split(",").map(s=>s.trim()).filter(s=>s.length>0),seen=new Set;for(let id of raw){if(!isHostPlatformId(id)){let allowed=HOST_PLATFORM_ORDER.join(", ");return{error:`Invalid --tools value: '${id}' (allowed tools: ${allowed}).`}}seen.add(id)}return{tools:HOST_PLATFORM_ORDER.filter(id=>seen.has(id))}}function promptSecretViaReadline(promptText,input=process.stdin,output=process.stderr){return new Promise(resolve2=>{let rl=readline4.createInterface({input,output,terminal:!0}),mutable=rl,muted=!1;mutable._writeToOutput=s=>{muted?s.includes(promptText)&&output.write(promptText):output.write(s)};let answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),output.write(`
8023
8023
  `),resolve2(answer.trim())}),muted=!0})}var INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND=`npx -y ${MCP_PACKAGE_NAME} connect-github`,INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect GitHub."," This installs the Bridge GitHub App so you can implement and review tickets with Bridge. It opens"," github.com in your browser; no GitHub credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`],INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT="Connect GitHub? [y/N]: ";async function offerGithubConnection(repoName,baseUrl,deps,log,session){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}for(let line of INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)===0?session.vcsConnected=!0:log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`)}}var INSTALL_BRIDGE_CONNECT_BITBUCKET_COMMAND=`npx -y ${MCP_PACKAGE_NAME} connect-bitbucket`,INSTALL_BRIDGE_BITBUCKET_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect Bitbucket."," This installs the Bridge Bitbucket Forge app so you can implement and review tickets with Bridge."," It opens Bitbucket in your browser; no Bitbucket credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_BITBUCKET_COMMAND}`],INSTALL_BRIDGE_BITBUCKET_CONNECT_PROMPT="Connect Bitbucket? [y/N]: ";async function offerBitbucketConnection(repoName,baseUrl,deps,log,session){if(!(!deps.isTTY||!deps.promptLine)&&!session.vcsConnected)try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl,apiKey:cred.credentials.apiKey},state=await fetchBitbucketConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read Bitbucket configuration status; skipping the Bitbucket offer.");return}for(let line of INSTALL_BRIDGE_BITBUCKET_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_BITBUCKET_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return;let connectDeps=createDefaultConnectBitbucketDeps();await runBitbucketConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: Bitbucket was not connected. Your install is complete \u2014 connect Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`)}catch{log(` note: the Bitbucket connection offer could not run. Your install is complete \u2014 connect Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`)}}var INSTALL_BRIDGE_AGENT_NOTES_PROMPT="Add cross-platform agent notes? [y/N]: ";function buildAgentNotesOfferContext(targets){return["","Step 4c \u2014 optional: cross-platform agent notes.",` This creates or refreshes a Bridge-owned region in ${targets.join(" and ")} with situation-`," to-tool guidance for a few Bridge MCP tools. Only that marked region is created or"," refreshed \u2014 any existing content in the file, inside or outside it, is otherwise preserved."]}async function offerAgentGuidanceNotes(deps,log,targets){if(targets.length===0)return{kind:"not-applicable"};if(!deps.isTTY||!deps.promptLine)return{kind:"headless-skip"};try{for(let line of buildAgentNotesOfferContext(targets))log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_AGENT_NOTES_PROMPT)).trim().toLowerCase();return answer==="y"||answer==="yes"?{kind:"consented"}:{kind:"declined"}}catch{return log(" note: the agent-notes offer could not run. No notes files were written."),{kind:"declined"}}}function buildAgentNotesSummaryLine(offer,result,writeFailed,unsupportedPlatforms){let unsupportedNote=unsupportedPlatforms.length>0?` (${unsupportedPlatforms.join(", ")}: no supported agent-notes convention)`:"";if(offer.kind==="not-applicable")return unsupportedPlatforms.length>0?`Agent notes: not written${unsupportedNote}.`:void 0;if(offer.kind==="headless-skip")return`Agent notes: skipped (non-interactive session)${unsupportedNote}.`;if(offer.kind==="declined")return`Agent notes: skipped (declined)${unsupportedNote}.`;if(writeFailed||!result)return`Agent notes: could not be written (unexpected error)${unsupportedNote}.`;let written=result.files.filter(f=>f.status==="written").map(f=>f.target),unchanged=result.files.filter(f=>f.status==="skipped").map(f=>f.target),failed=result.files.filter(f=>f.status==="failed").map(f=>f.target),parts=[];return written.length>0&&parts.push(`written to ${written.join(", ")}`),unchanged.length>0&&parts.push(`already up to date in ${unchanged.join(", ")}`),failed.length>0&&parts.push(`failed for ${failed.join(", ")}`),`Agent notes: ${parts.join("; ")}${unsupportedNote}.`}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline4.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,delete sanitized[INSTALL_REEXEC_KEY_SOURCE_ENV],sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn8(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let startTicketsDeps=createDefaultStartTicketsDeps(),isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os22.homedir,isTTY,readFile:p=>readFile15(p,"utf-8"),writeFile:(p,data,options)=>writeFile8(p,data,options),mkdir:(p,options)=>mkdir9(p,options),stat:p=>stat10(p),rename:(a,b)=>rename4(a,b),chmod:(p,m)=>chmod2(p,m),unlink:p=>unlink4(p),open:async(p,flags,mode)=>{let handle=await open4(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn8),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,lookupSelfServeBootstrapPending:lookupSelfServeBootstrapPendingCredential,prepareSelfServeBootstrapChallenge,now:()=>Date.now(),discardBootstrapPending:discardBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps,log:m=>console.log(m),errorLog:m=>console.error(m),debugLog:m=>{process.env.BAPI_INSTALL_DEBUG&&console.error(m)},conductorRunDoctor:params=>runConductorInstallDoctor({access:params.access,accessError:params.accessError,fetch:params.fetch,reviewPolicySource:params.reviewPolicySource,readWorkflowFile:()=>params.readFile(claudeReviewWorkflowPath(params.cwd)),classifyWorkflowDrift:async()=>{let probeDeps={runCommand:startTicketsDeps.runCommand,cwd:params.cwd},baseRef=await resolveCurrentBranch(probeDeps),defaultRef=await resolveRepositoryDefaultBranch(probeDeps);return probeClaudeReviewWorkflowDrift(probeDeps,{baseRef:baseRef??"",defaultRef})},projectRoot:params.cwd,inspectProfileToken:(projectRoot,token)=>inspectBridgeApiProfileToken(projectRoot,token,{readFile:params.readFile}),inspectExecutorServiceState:params.inspectExecutorServiceState,installDoctorDeps:{env:params.env,cwd:params.cwd,platform:process.platform,homedir:os22.homedir,readFile:params.readFile,stat:p=>stat10(p),fetch:(...args)=>fetch(...args)}}),conductorResolveAccess:async(repoName,baseUrl,mergedDeps)=>{let result=await resolveConductorBridgeApiAccess({env:{...mergedDeps.env,BAPI_BASE_URL:baseUrl},cwd:mergedDeps.cwd,homedir:mergedDeps.homedir,platform:mergedDeps.platform,readFile:mergedDeps.readFile,stat:p=>mergedDeps.stat(p),repoName});return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}},conductorWriteWorkflow:(cwd,content,options,mergedDeps)=>writeClaudeReviewWorkflow(cwd,content,{readFile:mergedDeps.readFile,mkdir:(p,o)=>mergedDeps.mkdir(p,o),writeFile:(p,data)=>mergedDeps.writeFile(p,data)},options),conductorRecordArtifact:async(cwd,relPath,mergedDeps)=>{let result=await recordInstalledProjectArtifact(cwd,relPath,{readFile:mergedDeps.readFile,writeFile:(p,data)=>mergedDeps.writeFile(p,data),rename:mergedDeps.rename,mkdir:(p,o)=>mergedDeps.mkdir(p,o),unlink:mergedDeps.unlink});return result.ok?{ok:!0}:{ok:!1,error:result.error}},conductorRunSetupEpicDryRun:async argv=>({exitCode:await runSetupEpicCli(argv)}),lookupExecutable:name=>defaultExecutablePresenceProbe(name),runLifecycleCommand:(executable,args)=>defaultShellFreeRunner(executable,args),getuid:()=>typeof process.getuid=="function"?process.getuid():null,monotonicNow:()=>Number(process.hrtime.bigint()/1000000n),sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms))}}function defaultShellFreeRunner(executable,args){return new Promise((resolve2,reject)=>{let child=spawn8(executable,args,{shell:!1,stdio:["ignore","pipe","pipe"]}),stdout="",stderr="";child.stdout?.on("data",chunk=>{stdout+=chunk.toString("utf-8")}),child.stderr?.on("data",chunk=>{stderr+=chunk.toString("utf-8")}),child.on("error",reject),child.on("close",code=>resolve2({exitCode:code??1,stdout,stderr}))})}async function defaultExecutablePresenceProbe(name){try{return(await defaultShellFreeRunner(name,["--version"])).exitCode===0}catch{return!1}}function buildConductorExecutorDeps(deps,baseUrl){let homeDir=deps.homedir(),lookupExecutable=deps.lookupExecutable??defaultExecutablePresenceProbe,runLifecycleCommand=deps.runLifecycleCommand??defaultShellFreeRunner;return{platform:deps.platform,homeDir,uid:deps.getuid?deps.getuid():null,envPath:deps.env.PATH??deps.env.Path??"",resolveInvocation:()=>resolvePackagedExecutorInvocation(),runPreflight:input=>collectExecutorInstallPreflight(input,{lookupExecutable:name=>lookupExecutable(name),homedir:deps.homedir,readFile:deps.readFile}),writeUnit:plan=>writeExecutorServicePlan(plan,{stat:filePath=>deps.stat(filePath),mkdir:(dirPath,options)=>deps.mkdir(dirPath,options),writeFile:(filePath,content)=>deps.writeFile(filePath,content),readFile:deps.readFile}),recordUnit:async unitPath=>{let result=await recordInstalledExecutorServiceUnit(deps.cwd,unitPath,homeDir,{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data),rename:deps.rename,mkdir:(p,o)=>deps.mkdir(p,o),unlink:deps.unlink});return result.ok?{ok:!0}:{ok:!1,error:result.error}},startService:plan=>startExecutorService(plan,runLifecycleCommand),now:deps.monotonicNow??(()=>Number(process.hrtime.bigint()/1000000n)),sleep:deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms)))}}function buildConductorLocalDeps(deps){return{mergeProfileToken:()=>mergeBridgeApiProfileToken(deps.cwd,CONDUCTOR_PROFILE_TOKEN),inspectProfileToken:()=>inspectBridgeApiProfileToken(deps.cwd,CONDUCTOR_PROFILE_TOKEN,{readFile:deps.readFile}),installGitHooks:async()=>installConductorGitHooks({cwd:deps.cwd}),probeNativeLedger:()=>collectConductorNativeLedgerSafe(),recordCapability:async capability=>{let result=await recordInstalledConductorCapability(deps.cwd,capability,{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data),rename:deps.rename,mkdir:(p,o)=>deps.mkdir(p,o),unlink:deps.unlink});return result.ok?{ok:!0}:{ok:!1,error:result.error}}}}function buildDoctorServiceStateInspector(deps){let runLifecycleCommand=deps.runLifecycleCommand??defaultShellFreeRunner,homeDir=deps.homedir(),uid=deps.getuid?deps.getuid():null;return async({executorId})=>deps.platform!=="darwin"&&deps.platform!=="linux"?"unknown":(await inspectExecutorServiceState({platform:deps.platform,unitPath:deps.platform==="darwin"?executorLaunchdPlistPathForId(executorId,homeDir):executorSystemdUnitPathForId(executorId,homeDir),serviceIdentifier:deps.platform==="darwin"?executorLaunchdLabelForId(executorId):executorSystemdUnitNameForId(executorId),uid},runLifecycleCommand)).state}var INSTALL_REEXEC_KEY_SOURCE_ENV="BAPI_INTERNAL_KEY_SOURCE",INSTALL_REEXEC_KEY_SOURCE_FLAG="flag";async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim(),source:"flag"};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0){let source=deps.env[INSTALL_REEXEC_KEY_SOURCE_ENV]===INSTALL_REEXEC_KEY_SOURCE_FLAG?"flag":"env";return{ok:!0,value:fromEnv.trim(),source}}if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Paste your Bridge token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered,source:"prompt"}:{ok:!1,error:`No Bridge token entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}return{ok:!1,error:`A Bridge token is required (no interactive terminal is available to prompt for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,error:"A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE environment variable (no interactive terminal is available to prompt for it). Note that both forms expose the token to your shell history and process list \u2014 prefer running 'install --invite' interactively."}}var INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE=/^[^@\s]+@[^@\s]+$/,INSTALL_BRIDGE_SIGNUP_EMAIL_HINT="Enter a valid email address (e.g. you@example.com).";async function resolveSignupEmail(options,deps){if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY};if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};let promptLine=deps.promptLine;for(let attempt=0;attempt<2;attempt+=1){let raw=await promptLine("Email for Bridge workspace setup (required): "),entered=String(raw??"").trim();if(INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(entered))return{ok:!0,value:entered};attempt===0&&deps.log(INSTALL_BRIDGE_SIGNUP_EMAIL_HINT)}return{ok:!1,error:"No email entered."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="Welcome to Bridge - do you have a token yet?",INSTALL_BRIDGE_ONBOARDING_CHOICES=["1. I need a token","2. I already have a token"],INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT="Enter 1 or 2: ",INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT="Enter 1 or 2.",INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE="Re-run install: on a bare interactive run answer 1 to request a new token or 2 if you already have one (an invite or an API key \u2014 either one is accepted at the hidden prompt). Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch,credentialIntent:"declared"};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch,credentialIntent:"declared"};let promptLine=deps.promptLine;try{deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);for(let choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)deps.log(choice);for(let attempt=0;attempt<2;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();if(answer==="1")return{ok:!0,branch:{kind:"need-key",method:"self-serve"},credentialIntent:"inferred"};if(answer==="2")return{ok:!0,branch:{kind:"have-key"},credentialIntent:"inferred"};attempt===0&&deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT)}return{ok:!1,error:`No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}catch{return{ok:!1,error:`Could not read your answer from the terminal. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to prompt for it). It must match the server-side repository registration exactly."};if(mode!=="new-project"){let answer=(await deps.promptLine("Repo name (must match server-side registration): ")).trim();return answer.length>0?{ok:!0,value:answer}:{ok:!1,error:"No repo name provided."}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path45.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let answer=(await deps.promptLine(`Name your new Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let answer=(await deps.promptLine("Name your new Bridge project: ")).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline4.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
8024
8024
  ${promptText}
@@ -8037,7 +8037,7 @@ ${promptText}
8037
8037
  `),INSTALL_BRIDGE_SIGNUP_BLOCKED="Error: Bridge signup is not available from your current network \u2014 your network/IP address appears to be blocked. Try again from a different network, or contact Bridge if you believe this is a mistake.";function formatSelfServeUnexpectedStatus(status){return`Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`}function maskSignupEmail(email){let trimmed=(email??"").trim(),at=trimmed.lastIndexOf("@");return at<=0||at===trimmed.length-1?"your email address":`${trimmed[0]}***${trimmed.slice(at)}`}function formatChallengeRemaining(expiresAtIso,nowMs){let expiresMs=Date.parse(expiresAtIso);if(Number.isNaN(expiresMs))return"";let minutes=Math.floor((expiresMs-nowMs)/6e4);return minutes<=0?"This code has expired.":minutes===1?"This code is valid for about another minute.":minutes<60?`This code is valid for about another ${minutes} minutes.`:minutes<120?"This code is valid for about another hour.":`This code is valid for about another ${Math.floor(minutes/60)} hours.`}async function promptForVerificationCode(deps,log,maxAttempts=3){if(!deps.promptLine)return null;for(let attempt=0;attempt<maxAttempts;attempt+=1){let entered;try{entered=(await deps.promptLine(INSTALL_BRIDGE_CODE_PROMPT)).trim()}catch{return null}if(entered.length===0)return null;if(new RegExp(`^[0-9]{${SIGNUP_VERIFICATION_CODE_LENGTH}}$`).test(entered))return entered;log(` That does not look like a code \u2014 it is ${SIGNUP_VERIFICATION_CODE_LENGTH} digits.`)}return null}function buildSelfServeVerifyUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap/self-serve/verify`}function buildSelfServeResendUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap/self-serve/resend`}async function requestSelfServeChallenge(deps,baseUrl,email){let resp;try{resp=await deps.fetch(buildSelfServeMintUrl(baseUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({invitee_email:email}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"network-error"}}if(resp.ok){let handle,expiresAt;try{let body=await resp.json();handle=body?.handle,expiresAt=body?.expires_at}catch{return{ok:!1,category:"failed"}}return typeof handle!="string"||handle.trim().length===0||typeof expiresAt!="string"||expiresAt.trim().length===0?{ok:!1,category:"failed"}:{ok:!0,handle:handle.trim(),expiresAt:expiresAt.trim()}}return resp.status===429?{ok:!1,category:"rate-limited"}:resp.status===503?{ok:!1,category:"unavailable"}:resp.status===400||resp.status===422?{ok:!1,category:"invalid"}:resp.status===403?{ok:!1,category:"blocked"}:{ok:!1,category:"unexpected",status:resp.status}}async function verifySelfServeChallenge(deps,baseUrl,handle,code){let resp;try{resp=await deps.fetch(buildSelfServeVerifyUrl(baseUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({handle,code}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"network-error"}}if(resp.ok){let token,trialGranted;try{let body=await resp.json();token=body?.token,trialGranted=body?.trial_granted}catch{return{ok:!1,category:"failed"}}return typeof token!="string"||token.trim().length===0||!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?{ok:!1,category:"failed"}:typeof trialGranted=="boolean"?{ok:!0,token,trialGranted}:{ok:!0,token}}return resp.status===401?{ok:!1,category:"rejected"}:resp.status===429?{ok:!1,category:"rate-limited"}:resp.status>=500?{ok:!1,category:"server-error"}:resp.status===403?{ok:!1,category:"blocked"}:{ok:!1,category:"unexpected",status:resp.status}}async function resendSelfServeChallenge(deps,baseUrl,handle){let resp;try{resp=await deps.fetch(buildSelfServeResendUrl(baseUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({handle}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"failed"}}if(resp.ok){let expiresAt,resendsRemaining;try{let body=await resp.json();expiresAt=body?.expires_at,resendsRemaining=body?.resends_remaining}catch{return{ok:!1,category:"failed"}}return typeof expiresAt!="string"||expiresAt.trim().length===0?{ok:!1,category:"failed"}:{ok:!0,expiresAt:expiresAt.trim(),resendsRemaining:typeof resendsRemaining=="number"&&Number.isFinite(resendsRemaining)?resendsRemaining:0}}if(resp.status===429){let detail="";try{let body=await resp.json();detail=typeof body?.detail=="string"?body.detail:""}catch{detail=""}return{ok:!1,category:detail.toLowerCase().includes("very recently")?"cooldown":"rate-limited"}}return resp.status===401?{ok:!1,category:"rejected"}:{ok:!1,category:"failed"}}var BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE=["The Bridge API rejected the bootstrap invite (HTTP 401).","","Either the invite is invalid, expired, or revoked \u2014 or it was ALREADY redeemed from this","machine and the local secret has since been lost (e.g. ~/.config/bridge was deleted).","","If it was already redeemed, you cannot recover it yourself:"," \u2022 Re-running will NOT work: each run without the original local secret sends a new one,"," which cannot match what the server stored, so it will keep returning 401."," \u2022 A new bootstrap invite will NOT work either: your repo name is globally unique and is"," now taken by the project you already created, so it cannot be redeemed again.","","Ask your Bridge API operator to recover it for you: they revoke the orphaned key","(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project","(POST /setup/keys), then send you that key. Run install with --api-key <that key>."].join(`
8038
8038
  `),BOOTSTRAP_INVITE_REJECTED_MESSAGE="The Bridge API rejected the bootstrap invite (HTTP 401). The invite is invalid, expired, or revoked \u2014 ask your Bridge API operator for a new one. (Your locally-stored secret was sent unchanged, so this is not a lost-secret problem.)",BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE="The Bridge API rejected the stored signup invite (HTTP 401) \u2014 it is invalid, expired, or revoked. Your saved signup attempt cannot be resumed with it.";function buildBootstrapRetryAdvice(selfServeSignupMode){return selfServeSignupMode?"Re-run install and choose the email option \u2014 your previous attempt will resume.":"Re-run install with the same bootstrap invite \u2014 the redemption will replay and return the same key."}async function resolveCredentialConflictConsent(prepared,ctx){if(prepared.ok||prepared.kind!=="credential-conflict")return prepared;if(!ctx.isTTY||!ctx.promptLine)return ctx.errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),null;let answer=(await ctx.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();return answer!=="y"&&answer!=="yes"?(ctx.errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),null):(ctx.grantConsent(),ctx.retry())}var INSTALL_BRIDGE_INSTALL_STATE_PATH=".bridge/install-state.json",INSTALL_BRIDGE_COMMIT_ASSETS_HEADING="Commit these generated files so your team inherits the same Bridge setup:";function buildCommitGeneratedAssetsNotice(safePaths,gitignoredPaths){let normalize=value=>{let trailingSlash=/\/+$/.test(value.trim()),collapsed=value.trim().replace(/\\/g,"/").replace(/\/+/g,"/").replace(/^\.\//,"").replace(/\/+$/,"");return trailingSlash?`${collapsed}/`:collapsed},excluded=new Set;for(let raw of gitignoredPaths){let normalized=normalize(raw);normalized.length!==0&&(excluded.add(normalized),excluded.add(normalized.replace(/\/+$/,"")),excluded.add(`${normalized.replace(/\/+$/,"")}/`))}let seen=new Set,lines=[];for(let raw of safePaths){let normalized=normalize(raw);normalized.length!==0&&(excluded.has(normalized)||seen.has(normalized)||(seen.add(normalized),lines.push(` ${normalized}`)))}return lines.length===0?"":[INSTALL_BRIDGE_COMMIT_ASSETS_HEADING,...lines].join(`
8039
8039
  `)}function describeDryRunRepoLine(repo,deferredHint){if(repo.kind==="deferred")return`Repo name: (not yet known \u2014 ${deferredHint})`;let source=repo.kind==="explicit"?"from --repo":"from BAPI_REPO_NAME";return`Repo name: ${repo.value} (${source})`}function repoNamePlaceholder(repo){return repo.kind==="deferred"?"<repo name \u2014 not yet known>":repo.value}function describeDryRunRootLine(projectRootPresent){return projectRootPresent?"Project root: .git found":"Project root: no .git found here \u2014 a real run would ask to continue (default No)"}function buildDryRunPreview(ctx){if(ctx.bootstrapInvite||ctx.selfServeSignup)return buildBootstrapDryRunPreview(ctx);let pingStep=ctx.repo.kind==="deferred"?["Step 2 \u2014 connectivity ping: deferred (the repo name is not yet known)."]:[`Step 2 \u2014 connectivity ping (before any durable key write): GET ${buildPingUrl(ctx.baseUrl,ctx.repo.value)} (X-API-Key: ${REDACTED_API_KEY})`],hostConfigStep=ctx.host.kind==="deferred"?["Step 3 \u2014 write per-host MCP config: deferred (tool selection is not yet known \u2014"," a real run asks interactively on a TTY, or auto-detects non-interactively)."]:["Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...ctx.host.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${repoNamePlaceholder(ctx.repo)}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${ctx.baseUrl}, BAPI_DOCS_DIR=${ctx.docsDir}`),...ctx.host.manualEditors.length>0?[` ${ctx.host.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[]],credentialStep=ctx.repo.kind==="deferred"?[`Step 4 \u2014 persist routing credential: deferred (the repo name is not yet known) at ${ctx.credentialStorePath}`]:[`Step 4 \u2014 persist routing credential: target bapi:${ctx.repo.value} at ${ctx.credentialStorePath}`];return["install --dry-run (no writes, no network, no spawns, no credential resolved or prompted for)",describeDryRunRepoLine(ctx.repo,"a real run would resolve it from your API key, or prompt if it cannot be"),`Base URL: ${ctx.baseUrl}`,`Docs dir: ${ctx.docsDir}`,describeDryRunRootLine(ctx.projectRootPresent),`Agent: ${ctx.host.kind==="explicit"?describePlannedLaunchAgent(ctx.host.launch):"(deferred \u2014 tool selection is not yet known)"}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",...pingStep,...hostConfigStep,`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY removed): ${ctx.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,...credentialStep,...buildCommitAssetsPreviewLines(ctx.commitSafeAssets,ctx.host.kind==="explicit"?ctx.host.configTargets:[]),...ctx.host.kind==="explicit"?buildLaunchStepPreview(ctx.host.launch):buildDeferredLaunchStepPreview(),...ctx.host.kind==="explicit"?buildAgentNotesPreviewLines(ctx.host.notesTargets??[]):[]]}function buildCommitAssetsPreviewLines(commitSafeAssets,configTargets){let notice=buildCommitGeneratedAssetsNotice(commitSafeAssets??[],[...configTargets,INSTALL_BRIDGE_INSTALL_STATE_PATH]);return notice.length>0?["",notice]:[]}function describePlannedLaunchAgent(launch){return launch.kind==="spawn"?`${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) \u2014 opens only after Y/N consent`:launch.kind==="choose-one"?`choose one of ${launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ")} \u2014 the chosen tool opens only after Y/N consent`:launch.reason==="empty-selection"?"none \u2014 no tools selected":launch.reason==="unsupported-agent-workflow"?"none \u2014 selected agent cannot run the install-bridge workflow; manual continuation is printed":"none \u2014 no launchable tool selected; manual continuation is printed"}function buildLaunchStepPreview(launch){let githubLines=["Step 4b \u2014 optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state"," via the install manifest and, only when it is unconfigured and the terminal is",` interactive, offer '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/,"")}' before the agent session starts.`," It installs the Bridge GitHub App so you can implement and review tickets with Bridge."," It opens github.com in your browser, and shares no GitHub credential with Bridge; it",` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`];if(launch.kind==="spawn")return[...githubLines,`Step 5 \u2014 agent session (${toolLabelForLaunchAgent(launch.agent)}): on a TTY the wizard first asks`," 'Open a \u2026 session to do that now? (Y/n)'; only on consent is the full command below"," stored in a restricted launch script (mode 0600, under the system temp dir) and a short"," sourced runner spawned (the script itself is NOT written in --dry-run):",` ${launch.spawnCommand}`];if(launch.kind==="choose-one"){let labels=launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ");return[...githubLines,`Step 5 \u2014 agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`," asks which single tool to open, then asks Y/N consent before spawning that one session."]}return[...githubLines,launch.reason==="empty-selection"?"Step 5 \u2014 no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool.":launch.reason==="unsupported-agent-workflow"?"Step 5 \u2014 no automatic launch: the selected agent cannot run the install-bridge workflow. The deterministic setup completes and manual continuation is printed.":"Step 5 \u2014 no automatic launch: no selected tool can auto-launch this workflow. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured)."]}function buildDeferredLaunchStepPreview(){return["Step 4b \u2014 GitHub connect offer and agent session: deferred (tool selection is"," not yet known \u2014 a real run resolves it first, then follows from there)."]}function buildAgentNotesPreviewLines(notesTargets){return notesTargets.length===0?[]:["Step 4c \u2014 optional: cross-platform agent notes (SKIPPED in --dry-run): if the terminal is",` interactive, offer to create or refresh a Bridge-owned marker region in ${notesTargets.join(" and ")}`," with situation-to-tool guidance for a few Bridge MCP tools; defaults to No, and"," existing content outside that region is always preserved. No notes files are"," written in --dry-run."]}function buildBootstrapDryRunPreview(ctx){let pendingTarget=`bootstrap-pending:${repoNamePlaceholder(ctx.repo)}`,header=ctx.selfServeSignup?"install --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)":"install --invite --dry-run (no writes, no network, no spawns, no secret generated)",createdBy=ctx.selfServeSignup?"the self-serve exchange":"the exchange",repoLine=ctx.repo.kind==="deferred"?"Repo name: (not yet known \u2014 a real run would prompt for a new, globally-unique project name)":`Repo name: ${ctx.repo.value} (would be created by ${createdBy}; globally unique)`,selfServeStep=ctx.selfServeSignup?["Step 2\xB7pre \u2014 self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace"," signup is requested, no mint call is made, no email is sent or transmitted, and",` ${pendingTarget} is neither read nor written;`," a real run would first check that record and RESUME a saved attempt if one"," exists, otherwise request a fresh workspace for your email and durably store the"," returned invite token alongside the key_secret \u2014 which then feeds the SAME"," redemption protocol below."]:[],exchangeUrl=buildBootstrapExchangeUrl(ctx.baseUrl),pingStep=ctx.repo.kind==="deferred"?["Step 2c \u2014 connectivity ping with the newly-minted key: deferred (the repo name is not yet known)."]:[`Step 2c \u2014 connectivity ping with the newly-minted key (before any durable key write): GET ${buildPingUrl(ctx.baseUrl,ctx.repo.value)} (X-API-Key: ${REDACTED_API_KEY})`],hostConfigStep=ctx.host.kind==="deferred"?["Step 3 \u2014 write per-host MCP config: deferred (tool selection is not yet known \u2014"," a real run asks interactively on a TTY, or auto-detects non-interactively)."]:["Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...ctx.host.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${repoNamePlaceholder(ctx.repo)}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${ctx.baseUrl}, BAPI_DOCS_DIR=${ctx.docsDir}`),...ctx.host.manualEditors.length>0?[` ${ctx.host.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[]],promoteStep=ctx.repo.kind==="deferred"?[`Step 4 \u2014 promote ${pendingTarget} to its final credential (only after the exchange succeeds) at ${ctx.credentialStorePath}`]:[`Step 4 \u2014 promote ${pendingTarget} \u2192 bapi:${ctx.repo.value} at ${ctx.credentialStorePath} (only after the exchange succeeds)`];return[header,repoLine,`Base URL: ${ctx.baseUrl}`,`Docs dir: ${ctx.docsDir}`,describeDryRunRootLine(ctx.projectRootPresent),`Agent: ${ctx.host.kind==="explicit"?describePlannedLaunchAgent(ctx.host.launch):"(deferred \u2014 tool selection is not yet known)"}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",...selfServeStep,`Step 2a \u2014 generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${ctx.credentialStorePath}`," BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.","Step 2b \u2014 redeem the bootstrap invite (replaces the pre-flight ping \u2014 there is no key yet):",` POST ${exchangeUrl}`,` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${repoNamePlaceholder(ctx.repo)}", "key_secret": "${REDACTED_API_KEY}"}`,...pingStep,...hostConfigStep,`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY / BAPI_INVITE removed): ${ctx.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,...promoteStep,...buildCommitAssetsPreviewLines(ctx.commitSafeAssets,ctx.host.kind==="explicit"?ctx.host.configTargets:[]),...ctx.host.kind==="explicit"?buildLaunchStepPreview(ctx.host.launch):buildDeferredLaunchStepPreview(),...ctx.host.kind==="explicit"?buildAgentNotesPreviewLines(ctx.host.notesTargets??[]):[]]}async function detectManualEditors(deps){let exists=async p=>{try{return await deps.stat(p),!0}catch{return!1}},windsurf=await exists(path45.join(deps.cwd,".windsurf"))||await exists(path45.join(deps.cwd,".windsurfrules")),codex=await exists(path45.join(deps.homedir(),".codex"));return{windsurf,codex}}function manualEditorNames(editors){let names=[];return editors.windsurf&&names.push("Windsurf"),editors.codex&&names.push("Codex"),names}function formatNeedKeyCredentialStoreLine(repoName,credentialStorePath){return`Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" \u2014 copy BAPI_API_KEY from there.`}function formatProjectCredentialStoreLine(repoName,credentialStorePath){return`We wrote your project credentials to: ${credentialStorePath} (target "bapi:${repoName}")`}function buildManualHostInstructions(entry,editors,needKey){if(!editors.windsurf&&!editors.codex)return null;let redactedEnv={...entry.env,BAPI_API_KEY:REDACTED_API_KEY},lines=["","Detected an editor whose MCP config is global and cannot be written automatically.","Add the server manually (replace <REDACTED> with your key):"];if(editors.windsurf){let windsurfSnippet=JSON.stringify({mcpServers:{[MCP_SERVER_NAME]:{command:entry.command,args:entry.args,env:redactedEnv}}},null,2);lines.push(""," Windsurf \u2192 ~/.codeium/windsurf/mcp_config.json:",windsurfSnippet)}return editors.codex&&lines.push("",` Codex \u2192 ~/.codex/config.toml (add an [mcp_servers.${MCP_SERVER_NAME}] table with the`," same command/args/env shown above, BAPI_API_KEY set to your key)."),needKey&&lines.push("",` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),lines.join(`
8040
- `)}var INSTALL_BRIDGE_DOCTOR_COMMAND=`npx -y ${MCP_PACKAGE_NAME} doctor`,INSTALL_BRIDGE_DOCTOR_POINTER=`Diagnose with: ${INSTALL_BRIDGE_DOCTOR_COMMAND}`,INSTALL_BRIDGE_UNCLASSIFIED_CAUSE="unexpected error (run with BAPI_INSTALL_DEBUG=1 for details)",INSTALL_BRIDGE_DISPATCH_STEP_LABEL="install-bridge dispatch",INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL="debug (BAPI_INSTALL_DEBUG) raw message:",INSTALL_BRIDGE_DEBUG_STACK_LABEL="debug (BAPI_INSTALL_DEBUG) raw stack:",INSTALL_BRIDGE_FILESYSTEM_CAUSES=new Map([["EACCES","permission denied"],["EPERM","permission denied"],["ENOSPC","no space left on device"],["EROFS","read-only file system"]]),INSTALL_BRIDGE_NETWORK_CODES=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN"]);function extractInstallBridgeErrorCode(error){try{if(typeof error!="object"||error===null)return;let code=error.code;return typeof code=="string"?code:void 0}catch{return}}function classifyInstallBridgeFailure(error){let code=extractInstallBridgeErrorCode(error);if(code===void 0)return INSTALL_BRIDGE_UNCLASSIFIED_CAUSE;let filesystemCause=INSTALL_BRIDGE_FILESYSTEM_CAUSES.get(code);return filesystemCause!==void 0?filesystemCause:INSTALL_BRIDGE_NETWORK_CODES.has(code)?"network error":INSTALL_BRIDGE_UNCLASSIFIED_CAUSE}function readInstallBridgeDebugValue(read){try{let value=read();return typeof value=="string"?value:value==null?void 0:String(value)}catch{return}}function buildInstallBridgeFailureLines(error,context){let lines=[`Error: install failed at: ${context.step}`,` cause: ${classifyInstallBridgeFailure(error)}`,` ${INSTALL_BRIDGE_DOCTOR_POINTER}`];if(context.resumeAdvice&&lines.push(` ${context.resumeAdvice}`),context.debugEnabled){let isError=error instanceof Error,rawMessage=readInstallBridgeDebugValue(isError?()=>error.message:()=>String(error)),rawStack=isError?readInstallBridgeDebugValue(()=>error.stack):void 0;lines.push(` ${INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL} ${rawMessage??"<unavailable>"}`),rawStack&&lines.push(` ${INSTALL_BRIDGE_DEBUG_STACK_LABEL} ${rawStack}`)}return lines}function buildInstallBridgeBaseUrlError(value){return`BAPI_BASE_URL must be an absolute http(s) URL (got: "${value}")`}function resolveInstallBridgeBaseUrl(env){let supplied=env.BAPI_BASE_URL,candidate=typeof supplied=="string"?supplied.trim():DEFAULT_BAPI_BASE_URL2;if(candidate.length===0)return{ok:!1,error:buildInstallBridgeBaseUrlError("")};let parsed;try{parsed=new URL(candidate)}catch{return{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}}return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}:{ok:!0,value:candidate}}var INSTALL_BRIDGE_STEP_LABELS={scaffold:"Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026",selfServeMint:"Step 2/5 \u2014 requesting Bridge self-serve setup\u2026",inviteRedeem:"Step 2/5 \u2014 redeeming the bootstrap invite\u2026",verifyConnectivity:"Step 2/5 \u2014 verifying connectivity\u2026",writeHostConfigs:"Step 3/5 \u2014 writing per-host MCP config\u2026",persistCredential:"Step 4/5 \u2014 persisting routing credential\u2026"};function buildInstallBridgeLaunchStepLabel(agent){return`Step 5/5 \u2014 opening a ${toolLabelForLaunchAgent(agent)} session for /install-bridge configuration + concise capability report\u2026`}var INSTALL_BRIDGE_CWD_BANNER_PREFIX="Installing Bridge into: ",INSTALL_BRIDGE_NO_GIT_WARNING="This doesn't look like a project root (no .git found)",INSTALL_BRIDGE_NO_GIT_PROMPT=`${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continue? [y/N]: `,INSTALL_BRIDGE_NO_GIT_ABORT="Aborted \u2014 run install from your project root, or re-run and confirm to continue.",INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING=`Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continuing (non-interactive).`;async function hasProjectRootMarker2(deps){return hasProjectRootMarker(deps.cwd,deps.stat)}async function resolveValidatedPremiumModelAlias(deps,agent){let candidate=resolveModelAlias(agent,"premium");if(!candidate)return null;let validation=await validateResolvedModelAliasForAgent(deps.startTicketsDeps,agent,candidate);return validation.ok?validation.alias:(deps.debugLog(`install-bridge: ${validation.warning}`),null)}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,reportFatal=(...lines)=>{for(let line of lines)errorLog(line);errorLog(` ${INSTALL_BRIDGE_DOCTOR_POINTER}`)},fatal=(...lines)=>(reportFatal(...lines),1),baseUrlResult=resolveInstallBridgeBaseUrl(deps.env),usageBaseUrl=baseUrlResult.ok?baseUrlResult.value:DEFAULT_BAPI_BASE_URL2;if(argv[0]==="conductor"){if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let conductorBaseUrl=baseUrlResult.value,conductorDeps={env:deps.env,cwd:deps.cwd,log:deps.log,errorLog:deps.errorLog,promptLine:deps.promptLine,resolveBaseUrl:()=>({ok:!0,value:conductorBaseUrl}),resolveAccess:(repoName2,resolvedBaseUrl)=>(deps.conductorResolveAccess??(async()=>({ok:!1,error:"conductor access seam unavailable"})))(repoName2,resolvedBaseUrl,deps),fetchJson:deps.fetch,postJson:deps.fetch,runDoctor:params=>(deps.conductorRunDoctor??(async()=>({legacyConductor:null,legacyExecutorPreflight:null,legacyInstallChecks:null,executorService:null,readiness:null,sections:[{id:"conductor-doctor",label:"Conductor install doctor",status:"fatal",detail:"the unified doctor seam is unavailable"}]})))({...params,fetch:deps.fetch,cwd:deps.cwd,env:deps.env,readFile:deps.readFile,inspectExecutorServiceState:buildDoctorServiceStateInspector(deps)}),writeWorkflow:(cwd,content,options2)=>(deps.conductorWriteWorkflow??(async()=>({ok:!1,error:"workflow seam unavailable"})))(cwd,content,options2,deps),recordArtifact:(cwd,relPath)=>(deps.conductorRecordArtifact??(async()=>({ok:!1,error:"install-state seam unavailable"})))(cwd,relPath,deps),runSetupEpicDryRun:setupArgv=>(deps.conductorRunSetupEpicDryRun??(async()=>({exitCode:1})))(setupArgv),executorProvisioning:buildConductorExecutorDeps(deps,conductorBaseUrl),localProvisioning:buildConductorLocalDeps(deps)};return runInstallBridgeConductorCli(argv.slice(1),conductorDeps)}let parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(getInstallBridgeUsage(usageBaseUrl)),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage(usageBaseUrl)),1;let options=parsed.options;if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let baseUrl=baseUrlResult.value,setupUrl=buildInstallBridgeSetupUrl(baseUrl);if(deps.env={...deps.env,BAPI_BASE_URL:baseUrl},options.dryRun){log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`);let repo=typeof options.repo=="string"&&options.repo.trim().length>0?{kind:"explicit",value:options.repo.trim()}:typeof deps.env.BAPI_REPO_NAME=="string"&&deps.env.BAPI_REPO_NAME.trim().length>0?{kind:"configured",value:deps.env.BAPI_REPO_NAME.trim()}:{kind:"deferred"},projectRootPresent=await hasProjectRootMarker2(deps),onboardingBranch=resolveInstallBridgeOnboardingBranch(options,deps.env),bootstrapInvite=onboardingBranch.kind==="need-key",selfServeSignup=onboardingBranch.kind==="need-key"&&onboardingBranch.method==="self-serve",dryRunPlatforms=await resolveDryRunHostPlatforms(deps,options),host;if(dryRunPlatforms!==void 0){let dryRunTargets=hostConfigTargetsForPlatforms(dryRunPlatforms),launchDecision2=resolveInstallBridgeLaunchDecision(dryRunPlatforms,options.agentName),launch;if(launchDecision2.kind==="spawn"){let spec=resolveAgentSpec(launchDecision2.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision2.agent}'.`),1;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);launch={kind:"spawn",agent:launchDecision2.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}else launchDecision2.kind==="choose-one"?launch={kind:"choose-one",agents:launchDecision2.agents}:launch={kind:"manual",reason:launchDecision2.reason};host={kind:"explicit",configTargets:dryRunTargets.map(t=>t.relPath),manualEditors:manualEditorNames(await detectManualEditors(deps)),launch,notesTargets:resolveAgentNotesTargets(dryRunPlatforms).targets}}else host={kind:"deferred"};let dryRunScaffoldAssets=await resolveInitScaffoldAssets({cwd:deps.cwd,env:deps.env,stat:deps.stat}),dryRunCredentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir});for(let line of buildDryRunPreview({baseUrl,docsDir:deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,credentialStorePath:dryRunCredentialStorePath,prewarmCommand:buildPrewarmCommandPreview(),commitSafeAssets:dryRunScaffoldAssets.commitSafePaths,projectRootPresent,repo,host,bootstrapInvite,selfServeSignup}))log(line);return 0}if(log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`),!await hasProjectRootMarker2(deps))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);let branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,credentialIntent=branchResult.credentialIntent,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="",apiKeySource;if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;if(inviteToken=inviteResult.value,classifyEnteredCredential(inviteToken)!=="invite"&&isHighEntropyApiKeyShape(inviteToken)){if(log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE),credentialIntent==="declared"&&!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED),1;apiKey=inviteToken,inviteToken="",bootstrapInviteMode=!1,selfServeSignupMode=!1,apiKeySource=void 0}}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;if(classifyEnteredCredential(keyResult.value)==="invite"){if(log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE),credentialIntent==="declared")if(deps.isTTY&&deps.promptLine){if(!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED),1}else log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1,apiKeySource=void 0}else credentialIntent==="inferred"&&isHighEntropyApiKeyShape(keyResult.value.trim())&&log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE),apiKey=keyResult.value,apiKeySource=keyResult.source}let docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,serverRepoResolutionStatus;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(serverRepoResolutionStatus=resolution.status,resolution.status==="resolved")repoName=resolution.repoName;else{if(resolution.status==="unresolved")return fatal(INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR,` Or rotate the key at ${setupUrl} (Security page) if it should identify exactly one project.`);{log(INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE);let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options);if(options.codexProfile&&!selectedPlatforms.includes("codex"))return errorLog("Error: --codex-profile requires Codex in --tools or the interactive selection."),1;let codexProfileChoice=options.codexProfile;if(selectedPlatforms.includes("codex")&&!options.dryRun&&!codexProfileChoice&&deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("Install the opt-in Bridge Codex approval profile for spawned workers? (y/N) ")).trim().toLowerCase();codexProfileChoice=answer==="y"||answer==="yes"?"on":"off"}let targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let scaffoldAssets=await resolveInitScaffoldAssets({cwd:deps.cwd,env:deps.env,stat:deps.stat});if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;let currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,bootstrapExchangeSucceeded=!1;try{currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,log(currentStep),await deps.runInit(deps.cwd,{creationContext:{repoName,baseUrl,credentialAvailable:!0}});let inviteFingerprint="";if(bootstrapInviteMode){let resumedSelfServeReplay=!1,keySecret="",reusedPendingSecret=!1,liveChallenge=null;if(selfServeSignupMode){let lookup=await deps.lookupSelfServeBootstrapPending({repoName,now:deps.now()},credentialWriteDeps);if(!lookup.ok)return fatal(`Error: ${lookup.error}`);lookup.state==="resumable"?(inviteToken=lookup.replayToken,inviteFingerprint=lookup.inviteFingerprint,keySecret=lookup.keySecret,resumedSelfServeReplay=!0,reusedPendingSecret=!0,log(` resuming your previous signup attempt for ${repoName}`)):lookup.state==="challenge"&&(liveChallenge={handle:lookup.challengeHandle,expiresAt:lookup.challengeExpiresAt,email:lookup.signupEmail})}let mintAndPrepareSelfServe=async()=>{let challengeHandle,challengeExpiresAt,challengeEmail;if(liveChallenge)challengeHandle=liveChallenge.handle,challengeExpiresAt=liveChallenge.expiresAt,challengeEmail=liveChallenge.email,log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`),log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);else{let requested=await requestSelfServeChallenge(deps,baseUrl,signupEmail);if(!requested.ok)return requested.category==="rate-limited"?reportFatal(`Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`):requested.category==="unavailable"?reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE):requested.category==="invalid"?reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again."):requested.category==="blocked"?reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED):requested.category==="unexpected"?reportFatal(formatSelfServeUnexpectedStatus(requested.status??0)):requested.category==="network-error"?reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE):reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED),!1;challengeHandle=requested.handle,challengeExpiresAt=requested.expiresAt,challengeEmail=signupEmail;let recorded=await deps.prepareSelfServeBootstrapChallenge({repoName,signupEmail:challengeEmail,challengeHandle,challengeExpiresAt},credentialWriteDeps);if(!recorded.ok)return reportFatal(`Error: could not record the signup attempt (${recorded.kind}). ${recorded.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1;log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`)}log(` Enter the ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to finish setting up your workspace.`);let remaining=formatChallengeRemaining(challengeExpiresAt,deps.now());remaining&&log(` ${remaining}`);let resendOffered=!1,rejections=0;for(;;){let code=await promptForVerificationCode(deps,log);if(code===null)return reportFatal("Error: No verification code entered \u2014 nothing has been set up. Re-run this command to continue; the code already emailed to you stays valid until it expires."),!1;let verified=await verifySelfServeChallenge(deps,baseUrl,challengeHandle,code);if(verified.ok){inviteToken=verified.token,verified.trialGranted===!1&&log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);break}if(verified.category==="rate-limited")return reportFatal(`Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`),!1;if(verified.category==="server-error")return reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR),!1;if(verified.category==="blocked")return reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED),!1;if(verified.category==="unexpected")return reportFatal(formatSelfServeUnexpectedStatus(verified.status??0)),!1;if(verified.category==="network-error")return reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR),!1;if(verified.category==="failed")return reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED),!1;if(rejections+=1,rejections>=INSTALL_BRIDGE_MAX_CODE_REJECTIONS)return reportFatal(INSTALL_BRIDGE_VERIFICATION_GIVING_UP),!1;if(errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED),!resendOffered&&deps.isTTY&&deps.promptLine&&(resendOffered=!0,await requestCredentialRouteSwitchConfirmation(deps,"Send a new code? [y/N]: "))){let resent=await resendSelfServeChallenge(deps,baseUrl,challengeHandle);if(resent.ok)log(` ${INSTALL_BRIDGE_RESEND_SENT}`);else if(resent.category==="cooldown")log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);else if(resent.category==="rate-limited")log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);else{if(resent.category==="rejected")return reportFatal(`Error: ${INSTALL_BRIDGE_VERIFICATION_REJECTED}`),!1;log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`)}}}inviteFingerprint=fingerprintBootstrapInvite(inviteToken);let prepareSelfServePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite,selfServeReplayToken:inviteToken},credentialWriteDeps),prep=await resolveCredentialConflictConsent(await prepareSelfServePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareSelfServePending(!0)});return prep===null?!1:prep.ok?(keySecret=prep.keySecret,reusedPendingSecret=prep.reused,!0):(reportFatal(`Error: could not durably store the signup credential (${prep.kind}). ${prep.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1)};if(selfServeSignupMode&&!resumedSelfServeReplay&&(currentStep=INSTALL_BRIDGE_STEP_LABELS.selfServeMint,log(currentStep),!await mintAndPrepareSelfServe()))return 1;if(!selfServeSignupMode){inviteFingerprint=fingerprintBootstrapInvite(inviteToken),currentStep=INSTALL_BRIDGE_STEP_LABELS.inviteRedeem,log(currentStep);let prepareInvitePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite},credentialWriteDeps),prepared=await resolveCredentialConflictConsent(await prepareInvitePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareInvitePending(!0)});if(prepared===null)return 1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error}`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused}let exchange,selfServeRefreshed=!1;for(;;){for(exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(exchange.ok)break;if(exchange.kind==="invalid-invite"&&resumedSelfServeReplay&&!selfServeRefreshed){if(!deps.isTTY||!deps.promptLine)return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE,`The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} has been left untouched. Re-run install on an interactive terminal to discard it and start a fresh signup.`);errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);let consented=!1;try{let answer=(await deps.promptLine(`Discard the saved signup at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} and start a fresh one? [y/N]: `)).trim().toLowerCase();consented=answer==="y"||answer==="yes"}catch{consented=!1}if(!consented)return errorLog("Aborted: the saved signup attempt was left unchanged (nothing was discarded)."),1;let discarded=await deps.discardBootstrapPending({repoName,inviteFingerprint},credentialWriteDeps);if(!discarded.ok)return fatal(`Error: could not discard the saved signup (${discarded.kind}). ${discarded.error}`);if(log("Requesting a fresh Bridge self-serve setup\u2026"),!await mintAndPrepareSelfServe())return 1;selfServeRefreshed=!0,resumedSelfServeReplay=!1;continue}return fatal(exchange.kind==="invalid-invite"?reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE:`Error: ${exchange.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`)}if(bootstrapExchangeSucceeded=!0,exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return fatal(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`);repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||(currentStep=INSTALL_BRIDGE_STEP_LABELS.verifyConnectivity,log(currentStep));let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey,bootstrapInviteMode?void 0:apiKeySource);if(!ping.ok)return fatal(bootstrapInviteMode?`Error: ${ping.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`:`Error: ${ping.message}`);log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir),secretFreeEntry=buildInstallBridgeSecretFreeServerEntry(deps.cwd,repoName,baseUrl,docsDir);currentStep=INSTALL_BRIDGE_STEP_LABELS.writeHostConfigs,log(currentStep);let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let trackedState=new Map;for(let target of targets)trackedState.set(target.relPath,await isTrackedProjectConfig(deps,target.relPath));let writeResult=await writeHostConfigs(deps,targets,{real:entry,secretFree:secretFreeEntry},trackedState,{repoName,credentialStorePath});if(writeResult.duplicateRegistrations.length>0){errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);for(let relPath of writeResult.duplicateRegistrations)errorLog(` ${relPath}`);return errorLog(" Nothing was written. Re-run install-bridge once one entry is removed."),1}for(let{relPath,mode}of writeResult.written)log(mode==="secret-free"?` registered the Bridge MCP server in ${relPath} (secret-free \u2014 key resolved at runtime)`:` registered the Bridge MCP server in ${relPath}`);for(let{relPath}of writeResult.skipped)log(` skipped ${relPath} \u2014 existing config could not be parsed safely; left untouched`);let needKeyManual=bootstrapInviteMode?{repoName,credentialStorePath}:void 0,globalProvisioning=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry,needKeyManual);for(let line of globalProvisioning.logLines)log(line);if(globalProvisioning.duplicateRegistrations.length>0){errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);for(let displayPath of globalProvisioning.duplicateRegistrations)errorLog(` ${displayPath}`);return errorLog(" That host was left untouched. Re-run install-bridge once one entry is removed."),1}if(selectedPlatforms.includes("codex")){let home=codexHome(deps.env,deps.homedir());try{let profileDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8",mode:384}),mkdir:deps.mkdir},result=codexProfileChoice?await configureBridgeCodexProfile(home,codexProfileChoice==="on"?"enable":"decline",profileDeps):await inspectBridgeCodexProfile(home,deps.readFile);result.status==="active"?log(" Codex Bridge approval profile installed; spawned workers use --profile bridge."):result.status==="declined"||result.status==="missing"?log(" Codex Bridge approval profile inactive; spawned workers use existing Codex approvals. Opt in with --codex-profile=on."):log(` Codex Bridge approval profile ${result.status}; spawned workers will not use it. Inspect ${home}/bridge.config.toml and rerun with --codex-profile=on.`)}catch{log(" Codex Bridge approval profile could not be configured; spawned workers will not use it. Inspect CODEX_HOME and rerun with --codex-profile=on.")}}let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors,needKeyManual);manualInstructions&&log(manualInstructions);try{await ensureGitignored(deps.cwd,INSTALL_BRIDGE_INSTALL_STATE_PATH,gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapExchangeSucceeded){currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return fatal(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);log(formatProjectCredentialStoreLine(repoName,promoted.path))}else{currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(formatProjectCredentialStoreLine(repoName,result.path)):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install, then verify with 'npx -y ${MCP_PACKAGE_NAME} doctor'.`)}catch{log(` warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y ${MCP_PACKAGE_NAME} doctor'.`)}}let commitAssetsNotice=buildCommitGeneratedAssetsNotice(scaffoldAssets.commitSafePaths,[...targets.map(t=>t.relPath),INSTALL_BRIDGE_INSTALL_STATE_PATH]);commitAssetsNotice.length>0&&(log(""),log(commitAssetsNotice));let vcsSessionState={vcsConnected:!1};await offerGithubConnection(repoName,baseUrl,deps,log,vcsSessionState),await offerBitbucketConnection(repoName,baseUrl,deps,log,vcsSessionState);let notesPlan=resolveAgentNotesTargets(selectedPlatforms),notesOffer=await offerAgentGuidanceNotes(deps,log,notesPlan.targets),notesResult,notesWriteFailed=!1;if(notesOffer.kind==="consented")try{notesResult=await writeAgentNotes({cwd:deps.cwd,selectedPlatforms,guidance:AGENT_GUIDANCE,readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir});for(let file of notesResult.files)file.status==="failed"&&errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`)}catch{notesWriteFailed=!0,errorLog(" warning: could not write agent notes (unexpected error).")}let notesSummaryLine=buildAgentNotesSummaryLine(notesOffer,notesResult,notesWriteFailed,notesPlan.unsupportedPlatforms);if(notesSummaryLine&&log(notesSummaryLine),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok||errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){currentStep=buildInstallBridgeLaunchStepLabel(finalAgentName),log(currentStep);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});if(!spawnResult.ok)return errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0;let handoffToolLabel=toolLabelForLaunchAgent(finalAgentName);return log(""),log(`install setup steps complete. A fresh ${handoffToolLabel} session is now applying configuration, presenting the concise capability report, and showing any next step the server provides.`),log(`In the new tab: approve the workspace and the '${MCP_SERVER_NAME}' MCP server if ${handoffToolLabel} asks \u2014 configuration can't proceed until you do.`),log(`If that tab is closed or fails, open a session in ${handoffToolLabel} and run /install-bridge.`),log(`NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards at ${setupUrl} (Get Started page \u2014 install status panel) or via the session's 'Applied N of M' summary.`),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}catch(error){if(error instanceof InitPreflightError){errorLog("Error: these MCP launcher configs cannot be safely reconciled:");for(let failure of error.failures)errorLog(` ${failure.relPath}: ${failure.reason}`);return errorLog(" Nothing was changed. Resolve these by hand, then re-run install-bridge."),errorLog(INSTALL_BRIDGE_DOCTOR_POINTER),1}let resumeAdvice=bootstrapInviteMode&&bootstrapExchangeSucceeded?buildBootstrapRetryAdvice(selfServeSignupMode):void 0,lines=buildInstallBridgeFailureLines(error,{step:currentStep,debugEnabled:!!deps.env.BAPI_INSTALL_DEBUG,...resumeAdvice?{resumeAdvice}:{}});for(let line of lines)errorLog(line);return 1}}init_version_generated();import{spawn as spawn9}from"child_process";init_version_generated();init_mcp_identity();import{writeFile as writeFile9,mkdir as mkdir10,readFile as readFile16}from"fs/promises";import path46 from"path";import os23 from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`;function getCachePath(){return path46.join(os23.homedir(),".config",...MCP_PACKAGE_NAME.split("/"),"update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path46.dirname(cachePath),latestVersion=null;try{let raw=await readFile16(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir10(cacheDir,{recursive:!0}),await writeFile9(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}init_mcp_identity();var INSTALL_REEXEC_SENTINEL="--internal-reexec",INSTALL_REEXEC_FAILURE_MESSAGE="Bridge API install failed: could not run the latest installer.";function stripInternalReexecSentinels(argv){let args=[],sentinelPresent=!1;for(let arg of argv){if(arg===INSTALL_REEXEC_SENTINEL){sentinelPresent=!0;continue}args.push(arg)}return{args,sentinelPresent}}function prepareInstallReexecArguments(argv){let forwardedArgs=[],secretEnv={},provenanceEnv={};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--api-key"||arg.startsWith("--api-key=")){let value,consumedNext=!1;if(arg.startsWith("--api-key=")?value=arg.slice(10):i+1<argv.length&&(value=argv[i+1],consumedNext=!0),value===void 0||value.length===0){forwardedArgs.push(arg);continue}secretEnv.BAPI_API_KEY=value,provenanceEnv[INSTALL_REEXEC_KEY_SOURCE_ENV]=INSTALL_REEXEC_KEY_SOURCE_FLAG,consumedNext&&(i+=1);continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(arg.startsWith("--invite=")){let value=arg.slice(9);if(value.length===0){forwardedArgs.push(arg);continue}secretEnv.BAPI_INVITE=value,forwardedArgs.push("--invite");continue}let next=argv[i+1];forwardedArgs.push("--invite"),typeof next=="string"&&!next.startsWith("-")&&next.length>0&&(secretEnv.BAPI_INVITE=next,i+=1);continue}forwardedArgs.push(arg)}return{forwardedArgs,secretEnv,provenanceEnv}}async function runInstallBridgeWithLatestCli(argv,deps={}){let resolveLatest=deps.fetchLatestVersion??(()=>fetchLatestVersion()),runLocal=deps.runInstallBridgeCli??runInstallBridgeCli,doSpawn=deps.spawn??spawn9,env=deps.env??process.env,platform=deps.platform??process.platform,cwd=deps.cwd??process.cwd(),errorLog=deps.errorLog??(message=>console.error(message)),localVersion=deps.localVersion??VERSION,{args:cleanedArgs,sentinelPresent}=stripInternalReexecSentinels(argv),isHelpInvocation=cleanedArgs.includes("-h")||cleanedArgs.includes("--help");if(sentinelPresent||cleanedArgs[0]==="conductor"||cleanedArgs.includes("--dry-run")||isHelpInvocation)return runLocal(cleanedArgs);let latestVersion=localVersion,fetched=null;try{fetched=await resolveLatest()}catch{fetched=null}if(fetched&&(latestVersion=fetched),!isNewerVersion(localVersion,latestVersion))return runLocal(cleanedArgs);let{forwardedArgs,secretEnv,provenanceEnv}=prepareInstallReexecArguments(cleanedArgs),npxCmd=platform==="win32"?"npx.cmd":"npx",childArgs=["-y",`${MCP_PACKAGE_NAME}@latest`,"install",INSTALL_REEXEC_SENTINEL,...forwardedArgs];return new Promise(resolve2=>{let settled=!1,settle2=code=>{settled||(settled=!0,resolve2(code))},fail3=()=>{errorLog(INSTALL_REEXEC_FAILURE_MESSAGE),settle2(1)},child;try{child=doSpawn(npxCmd,childArgs,{shell:!1,stdio:"inherit",cwd,env:{...env,...secretEnv,...provenanceEnv}})}catch{fail3();return}child.on("close",code=>{if(typeof code=="number"){settle2(code);return}fail3()}),child.on("error",()=>{fail3()})})}init_version_generated();init_init();init_init();import{spawn as spawn10}from"child_process";import{readFile as readFile17,stat as stat11}from"fs/promises";import path47 from"path";init_mcp_identity();var CANONICAL_UPGRADE_COMMAND=`npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`,SAFE_VERSION_RE=/^[0-9A-Za-z.\-+]{1,64}$/;function sanitizeVersion(value){return typeof value=="string"&&SAFE_VERSION_RE.test(value)?value:"unknown"}function formatUpdateAdvice(currentVersion,latestVersion){return`Bridge API MCP server update available: running ${sanitizeVersion(currentVersion)}, latest ${sanitizeVersion(latestVersion)} \u2014 run ${CANONICAL_UPGRADE_COMMAND}`}function formatToolSurfaceUpdateAdvisory(currentVersion,latestVersion){return`NOTE: this Bridge API MCP server is running ${sanitizeVersion(currentVersion)}, but ${sanitizeVersion(latestVersion)} is published. Some tools may be missing or renamed in this older build. To update, run: ${CANONICAL_UPGRADE_COMMAND}`}init_launcher_config_inspection();init_mcp_identity();var INTERNAL_REEXEC_FLAG="--internal-reexec";function defaultReexec(cwd,oldVersion){let npxCmd=process.platform==="win32"?"npx.cmd":"npx";return new Promise(resolve2=>{let child=spawn10(npxCmd,["-y",`${MCP_PACKAGE_NAME}@latest`,"upgrade",INTERNAL_REEXEC_FLAG,"--old-version",oldVersion],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})})}function localInstallPath(cwd){return path47.join(cwd,"node_modules",...MCP_PACKAGE_NAME.split("/"))}async function defaultHasCompetingLocalInstall(cwd){try{return await stat11(localInstallPath(cwd)),!0}catch{return!1}}function defaultRemoveCompetingLocalInstall(cwd){let npmCmd=process.platform==="win32"?"npm.cmd":"npm";return new Promise(resolve2=>{let child=spawn10(npmCmd,["uninstall",MCP_PACKAGE_NAME],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code===0)),child.on("error",()=>resolve2(!1))})}function createDefaultUpgradeCliDeps(cwd){return{cwd,fetchLatestVersion,readFile:filePath=>readFile17(filePath,"utf-8"),hasCompetingLocalInstall:defaultHasCompetingLocalInstall,removeCompetingLocalInstall:defaultRemoveCompetingLocalInstall,runInit,reexecFromLatest:defaultReexec,stdout:m=>console.log(m),stderr:m=>console.error(m),env:process.env}}async function resolveUpgradeTarget(argv,deps){let fetched=await deps.fetchLatestVersion();if(fetched)return{version:fetched,confidence:"registry-confirmed"};if(argv.includes(INTERNAL_REEXEC_FLAG))return{version:VERSION,confidence:"internal-reexec"};let asserted=deps.env?.BAPI_UPGRADE_ASSUME_LATEST;return typeof asserted=="string"&&asserted.trim().toLowerCase()==="true"?{version:VERSION,confidence:"latest-invocation-confirmed"}:{version:VERSION,confidence:"running-version-only"}}function formatDryRunLine(found){let{relPath,targetVersion}=found,keyNote=found.legacyRegistration?` [\`${found.registrationKey}\` entry]`:"";switch(found.action){case"repin":return` ${relPath}: ${found.currentVersion} -> ${targetVersion}${keyNote}`;case"already-current":return` ${relPath}: already ${targetVersion}${keyNote}`;case"normalize-unpinned":return` ${relPath}: would normalize ${found.spec} -> ${MCP_PACKAGE_NAME}@${targetVersion}${keyNote}`;case"add":return` ${relPath}: would add ${MCP_PACKAGE_NAME}@${targetVersion}`;case"duplicate-registration":return` ${relPath}: blocked \u2014 ${describeLauncherReason("duplicate-registration")}`;case"create":return` ${relPath}: would create with ${MCP_PACKAGE_NAME}@${targetVersion}`;case"skip-worktree-shim":return` ${relPath}: skipped \u2014 worktree mcp-invoke shim (no published-package pin applies)`;case"skip-inactive":return null;default:return null}}function formatResultLine(action){let{relPath}=action;switch(action.kind){case"repinned":return` ${relPath}: ${action.fromVersion} -> ${action.toVersion}`;case"already-current":return` ${relPath}: already ${action.toVersion}`;case"normalized":return` ${relPath}: normalized ${action.fromSpec} -> ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"added":return` ${relPath}: added ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"created":return` ${relPath}: created with ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"skipped-worktree-shim":return` ${relPath}: skipped \u2014 worktree mcp-invoke shim preserved`;case"skipped-ahead":return` ${relPath}: skipped \u2014 already pinned ahead at ${action.fromVersion}`;case"skipped-inactive":return null;default:return null}}function isEveryLauncherAlreadyCurrent(actions){let applicable=actions.filter(a=>a.kind!=="skipped-worktree-shim"&&a.kind!=="skipped-inactive"&&a.kind!=="skipped-ahead");return applicable.length===0?!1:applicable.every(a=>a.kind==="already-current")}async function runUpgradeCli(argv,injected){let deps={...createDefaultUpgradeCliDeps(injected?.cwd??process.cwd()),...injected},{stdout,stderr}=deps,isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes(INTERNAL_REEXEC_FLAG),target=await resolveUpgradeTarget(argv,deps);if(target.confidence==="registry-confirmed"&&isNewerVersion(VERSION,target.version)&&!isInternalReexec&&!isDryRun)return stdout(`Update available: ${VERSION} -> ${target.version}. Re-executing from @latest...`),deps.reexecFromLatest(deps.cwd,VERSION);if(target.confidence==="running-version-only")return stderr("Bridge API upgrade aborted: could not confirm the latest published version (the npm registry lookup failed) and this process was not started from @latest."),stderr(` Nothing was changed. Re-run the canonical command: ${CANONICAL_UPGRADE_COMMAND}`),1;let isCreatable=deps.isCreatable??(relPath=>relPath===".mcp.json"),inspections;try{inspections=await inspectLauncherConfigs({cwd:deps.cwd,targetVersion:target.version,readFile:deps.readFile,isCreatable,targets:LAUNCHER_CONFIG_TARGETS})}catch(err){let msg=err instanceof Error?err.message:String(err);return stderr(`Bridge API upgrade failed: could not inspect launcher configs: ${msg}`),1}let blocking=inspections.filter(isPrerequisiteFailure),ahead=inspections.filter(i=>i.action==="ahead");if(blocking.length>0||ahead.length>0){stderr("Bridge API upgrade aborted \u2014 these configs cannot be safely reconciled:");for(let found of[...blocking,...ahead])stderr(` ${found.relPath}: ${describeLauncherReason(found.reason)}`);return stderr(" Nothing was changed. Resolve these by hand, then re-run the upgrade."),1}if(isDryRun){stdout(`
8040
+ `)}var INSTALL_BRIDGE_DOCTOR_COMMAND=`npx -y ${MCP_PACKAGE_NAME} doctor`,INSTALL_BRIDGE_DOCTOR_POINTER=`Diagnose with: ${INSTALL_BRIDGE_DOCTOR_COMMAND}`,INSTALL_BRIDGE_UNCLASSIFIED_CAUSE="unexpected error (run with BAPI_INSTALL_DEBUG=1 for details)",INSTALL_BRIDGE_DISPATCH_STEP_LABEL="install-bridge dispatch",INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL="debug (BAPI_INSTALL_DEBUG) raw message:",INSTALL_BRIDGE_DEBUG_STACK_LABEL="debug (BAPI_INSTALL_DEBUG) raw stack:",INSTALL_BRIDGE_FILESYSTEM_CAUSES=new Map([["EACCES","permission denied"],["EPERM","permission denied"],["ENOSPC","no space left on device"],["EROFS","read-only file system"]]),INSTALL_BRIDGE_NETWORK_CODES=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN"]);function extractInstallBridgeErrorCode(error){try{if(typeof error!="object"||error===null)return;let code=error.code;return typeof code=="string"?code:void 0}catch{return}}function classifyInstallBridgeFailure(error){let code=extractInstallBridgeErrorCode(error);if(code===void 0)return INSTALL_BRIDGE_UNCLASSIFIED_CAUSE;let filesystemCause=INSTALL_BRIDGE_FILESYSTEM_CAUSES.get(code);return filesystemCause!==void 0?filesystemCause:INSTALL_BRIDGE_NETWORK_CODES.has(code)?"network error":INSTALL_BRIDGE_UNCLASSIFIED_CAUSE}function readInstallBridgeDebugValue(read){try{let value=read();return typeof value=="string"?value:value==null?void 0:String(value)}catch{return}}function buildInstallBridgeFailureLines(error,context){let lines=[`Error: install failed at: ${context.step}`,` cause: ${classifyInstallBridgeFailure(error)}`,` ${INSTALL_BRIDGE_DOCTOR_POINTER}`];if(context.resumeAdvice&&lines.push(` ${context.resumeAdvice}`),context.debugEnabled){let isError=error instanceof Error,rawMessage=readInstallBridgeDebugValue(isError?()=>error.message:()=>String(error)),rawStack=isError?readInstallBridgeDebugValue(()=>error.stack):void 0;lines.push(` ${INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL} ${rawMessage??"<unavailable>"}`),rawStack&&lines.push(` ${INSTALL_BRIDGE_DEBUG_STACK_LABEL} ${rawStack}`)}return lines}function buildInstallBridgeBaseUrlError(value){return`BAPI_BASE_URL must be an absolute http(s) URL (got: "${value}")`}function resolveInstallBridgeBaseUrl(env){let supplied=env.BAPI_BASE_URL,candidate=typeof supplied=="string"?supplied.trim():DEFAULT_BAPI_BASE_URL2;if(candidate.length===0)return{ok:!1,error:buildInstallBridgeBaseUrlError("")};let parsed;try{parsed=new URL(candidate)}catch{return{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}}return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}:{ok:!0,value:candidate}}var INSTALL_BRIDGE_STEP_LABELS={scaffold:"Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026",selfServeMint:"Step 2/5 \u2014 requesting Bridge self-serve setup\u2026",inviteRedeem:"Step 2/5 \u2014 redeeming the bootstrap invite\u2026",verifyConnectivity:"Step 2/5 \u2014 verifying connectivity\u2026",writeHostConfigs:"Step 3/5 \u2014 writing per-host MCP config\u2026",persistCredential:"Step 4/5 \u2014 persisting routing credential\u2026"};function buildInstallBridgeLaunchStepLabel(agent){return`Step 5/5 \u2014 opening a ${toolLabelForLaunchAgent(agent)} session for /install-bridge configuration + concise capability report\u2026`}var INSTALL_BRIDGE_CWD_BANNER_PREFIX="Installing Bridge into: ",INSTALL_BRIDGE_NO_GIT_WARNING="This doesn't look like a project root (no .git found)",INSTALL_BRIDGE_NO_GIT_PROMPT=`${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continue? [y/N]: `,INSTALL_BRIDGE_NO_GIT_ABORT="Aborted \u2014 run install from your project root, or re-run and confirm to continue.",INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING=`Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continuing (non-interactive).`;async function hasProjectRootMarker2(deps){return hasProjectRootMarker(deps.cwd,deps.stat)}async function resolveValidatedPremiumModelAlias(deps,agent){let candidate=resolveModelAlias(agent,"premium");if(!candidate)return null;let validation=await validateResolvedModelAliasForAgent(deps.startTicketsDeps,agent,candidate);return validation.ok?validation.alias:(deps.debugLog(`install-bridge: ${validation.warning}`),null)}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,reportFatal=(...lines)=>{for(let line of lines)errorLog(line);errorLog(` ${INSTALL_BRIDGE_DOCTOR_POINTER}`)},fatal=(...lines)=>(reportFatal(...lines),1),baseUrlResult=resolveInstallBridgeBaseUrl(deps.env),usageBaseUrl=baseUrlResult.ok?baseUrlResult.value:DEFAULT_BAPI_BASE_URL2;if(argv[0]==="conductor"){if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let conductorBaseUrl=baseUrlResult.value,conductorDeps={env:deps.env,cwd:deps.cwd,log:deps.log,errorLog:deps.errorLog,promptLine:deps.promptLine,resolveBaseUrl:()=>({ok:!0,value:conductorBaseUrl}),resolveAccess:(repoName2,resolvedBaseUrl)=>(deps.conductorResolveAccess??(async()=>({ok:!1,error:"conductor access seam unavailable"})))(repoName2,resolvedBaseUrl,deps),fetchJson:deps.fetch,postJson:deps.fetch,runDoctor:params=>(deps.conductorRunDoctor??(async()=>({legacyConductor:null,legacyExecutorPreflight:null,legacyInstallChecks:null,executorService:null,readiness:null,sections:[{id:"conductor-doctor",label:"Conductor install doctor",status:"fatal",detail:"the unified doctor seam is unavailable"}]})))({...params,fetch:deps.fetch,cwd:deps.cwd,env:deps.env,readFile:deps.readFile,inspectExecutorServiceState:buildDoctorServiceStateInspector(deps)}),writeWorkflow:(cwd,content,options2)=>(deps.conductorWriteWorkflow??(async()=>({ok:!1,error:"workflow seam unavailable"})))(cwd,content,options2,deps),recordArtifact:(cwd,relPath)=>(deps.conductorRecordArtifact??(async()=>({ok:!1,error:"install-state seam unavailable"})))(cwd,relPath,deps),runSetupEpicDryRun:setupArgv=>(deps.conductorRunSetupEpicDryRun??(async()=>({exitCode:1})))(setupArgv),executorProvisioning:buildConductorExecutorDeps(deps,conductorBaseUrl),localProvisioning:buildConductorLocalDeps(deps)};return runInstallBridgeConductorCli(argv.slice(1),conductorDeps)}let parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(getInstallBridgeUsage(usageBaseUrl)),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage(usageBaseUrl)),1;let options=parsed.options;if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let baseUrl=baseUrlResult.value,setupUrl=buildInstallBridgeSetupUrl(baseUrl);if(deps.env={...deps.env,BAPI_BASE_URL:baseUrl},options.dryRun){log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`);let repo=typeof options.repo=="string"&&options.repo.trim().length>0?{kind:"explicit",value:options.repo.trim()}:typeof deps.env.BAPI_REPO_NAME=="string"&&deps.env.BAPI_REPO_NAME.trim().length>0?{kind:"configured",value:deps.env.BAPI_REPO_NAME.trim()}:{kind:"deferred"},projectRootPresent=await hasProjectRootMarker2(deps),onboardingBranch=resolveInstallBridgeOnboardingBranch(options,deps.env),bootstrapInvite=onboardingBranch.kind==="need-key",selfServeSignup=onboardingBranch.kind==="need-key"&&onboardingBranch.method==="self-serve",dryRunPlatforms=await resolveDryRunHostPlatforms(deps,options),host;if(dryRunPlatforms!==void 0){let dryRunTargets=hostConfigTargetsForPlatforms(dryRunPlatforms),launchDecision2=resolveInstallBridgeLaunchDecision(dryRunPlatforms,options.agentName),launch;if(launchDecision2.kind==="spawn"){let spec=resolveAgentSpec(launchDecision2.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision2.agent}'.`),1;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);launch={kind:"spawn",agent:launchDecision2.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}else launchDecision2.kind==="choose-one"?launch={kind:"choose-one",agents:launchDecision2.agents}:launch={kind:"manual",reason:launchDecision2.reason};host={kind:"explicit",configTargets:dryRunTargets.map(t=>t.relPath),manualEditors:manualEditorNames(await detectManualEditors(deps)),launch,notesTargets:resolveAgentNotesTargets(dryRunPlatforms).targets}}else host={kind:"deferred"};let dryRunScaffoldAssets=await resolveInitScaffoldAssets({cwd:deps.cwd,env:deps.env,stat:deps.stat}),dryRunCredentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir});for(let line of buildDryRunPreview({baseUrl,docsDir:deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,credentialStorePath:dryRunCredentialStorePath,prewarmCommand:buildPrewarmCommandPreview(),commitSafeAssets:dryRunScaffoldAssets.commitSafePaths,projectRootPresent,repo,host,bootstrapInvite,selfServeSignup}))log(line);return 0}if(log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`),!await hasProjectRootMarker2(deps))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);let branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,credentialIntent=branchResult.credentialIntent,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="",apiKeySource;if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;if(inviteToken=inviteResult.value,classifyEnteredCredential(inviteToken)!=="invite"&&isHighEntropyApiKeyShape(inviteToken)){if(log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE),credentialIntent==="declared"&&!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED),1;apiKey=inviteToken,inviteToken="",bootstrapInviteMode=!1,selfServeSignupMode=!1,apiKeySource=void 0}}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;if(classifyEnteredCredential(keyResult.value)==="invite"){if(log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE),credentialIntent==="declared")if(deps.isTTY&&deps.promptLine){if(!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED),1}else log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1,apiKeySource=void 0}else credentialIntent==="inferred"&&isHighEntropyApiKeyShape(keyResult.value.trim())&&log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE),apiKey=keyResult.value,apiKeySource=keyResult.source}let docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,serverRepoResolutionStatus;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(serverRepoResolutionStatus=resolution.status,resolution.status==="resolved")repoName=resolution.repoName;else{if(resolution.status==="unresolved")return fatal(INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR,` Or rotate the key at ${setupUrl} (Security page) if it should identify exactly one project.`);{log(INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE);let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options);if(options.codexProfile&&!selectedPlatforms.includes("codex"))return errorLog("Error: --codex-profile requires Codex in --tools or the interactive selection."),1;let codexProfileChoice=options.codexProfile;if(selectedPlatforms.includes("codex")&&!options.dryRun&&!codexProfileChoice&&deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("Install the opt-in Bridge Codex approval profile for spawned workers? (Y/n) ")).trim().toLowerCase();codexProfileChoice=answer===""||answer==="y"||answer==="yes"?"on":"off"}let targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let scaffoldAssets=await resolveInitScaffoldAssets({cwd:deps.cwd,env:deps.env,stat:deps.stat});if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen;let modelAlias=await resolveValidatedPremiumModelAlias(deps,spec);finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform,modelAlias)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;let currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,bootstrapExchangeSucceeded=!1;try{currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,log(currentStep),await deps.runInit(deps.cwd,{creationContext:{repoName,baseUrl,credentialAvailable:!0}});let inviteFingerprint="";if(bootstrapInviteMode){let resumedSelfServeReplay=!1,keySecret="",reusedPendingSecret=!1,liveChallenge=null;if(selfServeSignupMode){let lookup=await deps.lookupSelfServeBootstrapPending({repoName,now:deps.now()},credentialWriteDeps);if(!lookup.ok)return fatal(`Error: ${lookup.error}`);lookup.state==="resumable"?(inviteToken=lookup.replayToken,inviteFingerprint=lookup.inviteFingerprint,keySecret=lookup.keySecret,resumedSelfServeReplay=!0,reusedPendingSecret=!0,log(` resuming your previous signup attempt for ${repoName}`)):lookup.state==="challenge"&&(liveChallenge={handle:lookup.challengeHandle,expiresAt:lookup.challengeExpiresAt,email:lookup.signupEmail})}let mintAndPrepareSelfServe=async()=>{let challengeHandle,challengeExpiresAt,challengeEmail;if(liveChallenge)challengeHandle=liveChallenge.handle,challengeExpiresAt=liveChallenge.expiresAt,challengeEmail=liveChallenge.email,log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`),log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);else{let requested=await requestSelfServeChallenge(deps,baseUrl,signupEmail);if(!requested.ok)return requested.category==="rate-limited"?reportFatal(`Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`):requested.category==="unavailable"?reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE):requested.category==="invalid"?reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again."):requested.category==="blocked"?reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED):requested.category==="unexpected"?reportFatal(formatSelfServeUnexpectedStatus(requested.status??0)):requested.category==="network-error"?reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE):reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED),!1;challengeHandle=requested.handle,challengeExpiresAt=requested.expiresAt,challengeEmail=signupEmail;let recorded=await deps.prepareSelfServeBootstrapChallenge({repoName,signupEmail:challengeEmail,challengeHandle,challengeExpiresAt},credentialWriteDeps);if(!recorded.ok)return reportFatal(`Error: could not record the signup attempt (${recorded.kind}). ${recorded.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1;log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`)}log(` Enter the ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to finish setting up your workspace.`);let remaining=formatChallengeRemaining(challengeExpiresAt,deps.now());remaining&&log(` ${remaining}`);let resendOffered=!1,rejections=0;for(;;){let code=await promptForVerificationCode(deps,log);if(code===null)return reportFatal("Error: No verification code entered \u2014 nothing has been set up. Re-run this command to continue; the code already emailed to you stays valid until it expires."),!1;let verified=await verifySelfServeChallenge(deps,baseUrl,challengeHandle,code);if(verified.ok){inviteToken=verified.token,verified.trialGranted===!1&&log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);break}if(verified.category==="rate-limited")return reportFatal(`Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`),!1;if(verified.category==="server-error")return reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR),!1;if(verified.category==="blocked")return reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED),!1;if(verified.category==="unexpected")return reportFatal(formatSelfServeUnexpectedStatus(verified.status??0)),!1;if(verified.category==="network-error")return reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR),!1;if(verified.category==="failed")return reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED),!1;if(rejections+=1,rejections>=INSTALL_BRIDGE_MAX_CODE_REJECTIONS)return reportFatal(INSTALL_BRIDGE_VERIFICATION_GIVING_UP),!1;if(errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED),!resendOffered&&deps.isTTY&&deps.promptLine&&(resendOffered=!0,await requestCredentialRouteSwitchConfirmation(deps,"Send a new code? [y/N]: "))){let resent=await resendSelfServeChallenge(deps,baseUrl,challengeHandle);if(resent.ok)log(` ${INSTALL_BRIDGE_RESEND_SENT}`);else if(resent.category==="cooldown")log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);else if(resent.category==="rate-limited")log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);else{if(resent.category==="rejected")return reportFatal(`Error: ${INSTALL_BRIDGE_VERIFICATION_REJECTED}`),!1;log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`)}}}inviteFingerprint=fingerprintBootstrapInvite(inviteToken);let prepareSelfServePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite,selfServeReplayToken:inviteToken},credentialWriteDeps),prep=await resolveCredentialConflictConsent(await prepareSelfServePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareSelfServePending(!0)});return prep===null?!1:prep.ok?(keySecret=prep.keySecret,reusedPendingSecret=prep.reused,!0):(reportFatal(`Error: could not durably store the signup credential (${prep.kind}). ${prep.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1)};if(selfServeSignupMode&&!resumedSelfServeReplay&&(currentStep=INSTALL_BRIDGE_STEP_LABELS.selfServeMint,log(currentStep),!await mintAndPrepareSelfServe()))return 1;if(!selfServeSignupMode){inviteFingerprint=fingerprintBootstrapInvite(inviteToken),currentStep=INSTALL_BRIDGE_STEP_LABELS.inviteRedeem,log(currentStep);let prepareInvitePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite},credentialWriteDeps),prepared=await resolveCredentialConflictConsent(await prepareInvitePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareInvitePending(!0)});if(prepared===null)return 1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error}`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused}let exchange,selfServeRefreshed=!1;for(;;){for(exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(exchange.ok)break;if(exchange.kind==="invalid-invite"&&resumedSelfServeReplay&&!selfServeRefreshed){if(!deps.isTTY||!deps.promptLine)return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE,`The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} has been left untouched. Re-run install on an interactive terminal to discard it and start a fresh signup.`);errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);let consented=!1;try{let answer=(await deps.promptLine(`Discard the saved signup at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} and start a fresh one? [y/N]: `)).trim().toLowerCase();consented=answer==="y"||answer==="yes"}catch{consented=!1}if(!consented)return errorLog("Aborted: the saved signup attempt was left unchanged (nothing was discarded)."),1;let discarded=await deps.discardBootstrapPending({repoName,inviteFingerprint},credentialWriteDeps);if(!discarded.ok)return fatal(`Error: could not discard the saved signup (${discarded.kind}). ${discarded.error}`);if(log("Requesting a fresh Bridge self-serve setup\u2026"),!await mintAndPrepareSelfServe())return 1;selfServeRefreshed=!0,resumedSelfServeReplay=!1;continue}return fatal(exchange.kind==="invalid-invite"?reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE:`Error: ${exchange.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`)}if(bootstrapExchangeSucceeded=!0,exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return fatal(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`);repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||(currentStep=INSTALL_BRIDGE_STEP_LABELS.verifyConnectivity,log(currentStep));let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey,bootstrapInviteMode?void 0:apiKeySource);if(!ping.ok)return fatal(bootstrapInviteMode?`Error: ${ping.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`:`Error: ${ping.message}`);log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir),secretFreeEntry=buildInstallBridgeSecretFreeServerEntry(deps.cwd,repoName,baseUrl,docsDir);currentStep=INSTALL_BRIDGE_STEP_LABELS.writeHostConfigs,log(currentStep);let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let trackedState=new Map;for(let target of targets)trackedState.set(target.relPath,await isTrackedProjectConfig(deps,target.relPath));let writeResult=await writeHostConfigs(deps,targets,{real:entry,secretFree:secretFreeEntry},trackedState,{repoName,credentialStorePath});if(writeResult.duplicateRegistrations.length>0){errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);for(let relPath of writeResult.duplicateRegistrations)errorLog(` ${relPath}`);return errorLog(" Nothing was written. Re-run install-bridge once one entry is removed."),1}for(let{relPath,mode}of writeResult.written)log(mode==="secret-free"?` registered the Bridge MCP server in ${relPath} (secret-free \u2014 key resolved at runtime)`:` registered the Bridge MCP server in ${relPath}`);for(let{relPath}of writeResult.skipped)log(` skipped ${relPath} \u2014 existing config could not be parsed safely; left untouched`);let needKeyManual=bootstrapInviteMode?{repoName,credentialStorePath}:void 0,globalProvisioning=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry,needKeyManual);for(let line of globalProvisioning.logLines)log(line);if(globalProvisioning.duplicateRegistrations.length>0){errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);for(let displayPath of globalProvisioning.duplicateRegistrations)errorLog(` ${displayPath}`);return errorLog(" That host was left untouched. Re-run install-bridge once one entry is removed."),1}if(selectedPlatforms.includes("codex")){let home=codexHome(deps.env,deps.homedir());try{let profileDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8",mode:384}),mkdir:deps.mkdir},result=codexProfileChoice?await configureBridgeCodexProfile(home,codexProfileChoice==="on"?"enable":"decline",profileDeps):await inspectBridgeCodexProfile(home,deps.readFile);result.status==="active"?log(" Codex Bridge approval profile installed; spawned workers use --profile bridge."):result.status==="declined"||result.status==="missing"?log(" Codex Bridge approval profile inactive; spawned workers use existing Codex approvals. Opt in with --codex-profile=on."):log(` Codex Bridge approval profile ${result.status}; spawned workers will not use it. Inspect ${home}/bridge.config.toml and rerun with --codex-profile=on.`)}catch{log(" Codex Bridge approval profile could not be configured; spawned workers will not use it. Inspect CODEX_HOME and rerun with --codex-profile=on.")}}let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors,needKeyManual);manualInstructions&&log(manualInstructions);try{await ensureGitignored(deps.cwd,INSTALL_BRIDGE_INSTALL_STATE_PATH,gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapExchangeSucceeded){currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return fatal(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);log(formatProjectCredentialStoreLine(repoName,promoted.path))}else{currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(formatProjectCredentialStoreLine(repoName,result.path)):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install, then verify with 'npx -y ${MCP_PACKAGE_NAME} doctor'.`)}catch{log(` warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y ${MCP_PACKAGE_NAME} doctor'.`)}}let commitAssetsNotice=buildCommitGeneratedAssetsNotice(scaffoldAssets.commitSafePaths,[...targets.map(t=>t.relPath),INSTALL_BRIDGE_INSTALL_STATE_PATH]);commitAssetsNotice.length>0&&(log(""),log(commitAssetsNotice));let vcsSessionState={vcsConnected:!1};await offerGithubConnection(repoName,baseUrl,deps,log,vcsSessionState),await offerBitbucketConnection(repoName,baseUrl,deps,log,vcsSessionState);let notesPlan=resolveAgentNotesTargets(selectedPlatforms),notesOffer=await offerAgentGuidanceNotes(deps,log,notesPlan.targets),notesResult,notesWriteFailed=!1;if(notesOffer.kind==="consented")try{notesResult=await writeAgentNotes({cwd:deps.cwd,selectedPlatforms,guidance:AGENT_GUIDANCE,readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir});for(let file of notesResult.files)file.status==="failed"&&errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`)}catch{notesWriteFailed=!0,errorLog(" warning: could not write agent notes (unexpected error).")}let notesSummaryLine=buildAgentNotesSummaryLine(notesOffer,notesResult,notesWriteFailed,notesPlan.unsupportedPlatforms);if(notesSummaryLine&&log(notesSummaryLine),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok||errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){currentStep=buildInstallBridgeLaunchStepLabel(finalAgentName),log(currentStep);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});if(!spawnResult.ok)return errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0;let handoffToolLabel=toolLabelForLaunchAgent(finalAgentName);return log(""),log(`install setup steps complete. A fresh ${handoffToolLabel} session is now applying configuration, presenting the concise capability report, and showing any next step the server provides.`),log(`In the new tab: approve the workspace and the '${MCP_SERVER_NAME}' MCP server if ${handoffToolLabel} asks \u2014 configuration can't proceed until you do.`),log(`If that tab is closed or fails, open a session in ${handoffToolLabel} and run /install-bridge.`),log(`NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards at ${setupUrl} (Get Started page \u2014 install status panel) or via the session's 'Applied N of M' summary.`),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}catch(error){if(error instanceof InitPreflightError){errorLog("Error: these MCP launcher configs cannot be safely reconciled:");for(let failure of error.failures)errorLog(` ${failure.relPath}: ${failure.reason}`);return errorLog(" Nothing was changed. Resolve these by hand, then re-run install-bridge."),errorLog(INSTALL_BRIDGE_DOCTOR_POINTER),1}let resumeAdvice=bootstrapInviteMode&&bootstrapExchangeSucceeded?buildBootstrapRetryAdvice(selfServeSignupMode):void 0,lines=buildInstallBridgeFailureLines(error,{step:currentStep,debugEnabled:!!deps.env.BAPI_INSTALL_DEBUG,...resumeAdvice?{resumeAdvice}:{}});for(let line of lines)errorLog(line);return 1}}init_version_generated();import{spawn as spawn9}from"child_process";init_version_generated();init_mcp_identity();import{writeFile as writeFile9,mkdir as mkdir10,readFile as readFile16}from"fs/promises";import path46 from"path";import os23 from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`;function getCachePath(){return path46.join(os23.homedir(),".config",...MCP_PACKAGE_NAME.split("/"),"update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path46.dirname(cachePath),latestVersion=null;try{let raw=await readFile16(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir10(cacheDir,{recursive:!0}),await writeFile9(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}init_mcp_identity();var INSTALL_REEXEC_SENTINEL="--internal-reexec",INSTALL_REEXEC_FAILURE_MESSAGE="Bridge API install failed: could not run the latest installer.";function stripInternalReexecSentinels(argv){let args=[],sentinelPresent=!1;for(let arg of argv){if(arg===INSTALL_REEXEC_SENTINEL){sentinelPresent=!0;continue}args.push(arg)}return{args,sentinelPresent}}function prepareInstallReexecArguments(argv){let forwardedArgs=[],secretEnv={},provenanceEnv={};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--api-key"||arg.startsWith("--api-key=")){let value,consumedNext=!1;if(arg.startsWith("--api-key=")?value=arg.slice(10):i+1<argv.length&&(value=argv[i+1],consumedNext=!0),value===void 0||value.length===0){forwardedArgs.push(arg);continue}secretEnv.BAPI_API_KEY=value,provenanceEnv[INSTALL_REEXEC_KEY_SOURCE_ENV]=INSTALL_REEXEC_KEY_SOURCE_FLAG,consumedNext&&(i+=1);continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(arg.startsWith("--invite=")){let value=arg.slice(9);if(value.length===0){forwardedArgs.push(arg);continue}secretEnv.BAPI_INVITE=value,forwardedArgs.push("--invite");continue}let next=argv[i+1];forwardedArgs.push("--invite"),typeof next=="string"&&!next.startsWith("-")&&next.length>0&&(secretEnv.BAPI_INVITE=next,i+=1);continue}forwardedArgs.push(arg)}return{forwardedArgs,secretEnv,provenanceEnv}}async function runInstallBridgeWithLatestCli(argv,deps={}){let resolveLatest=deps.fetchLatestVersion??(()=>fetchLatestVersion()),runLocal=deps.runInstallBridgeCli??runInstallBridgeCli,doSpawn=deps.spawn??spawn9,env=deps.env??process.env,platform=deps.platform??process.platform,cwd=deps.cwd??process.cwd(),errorLog=deps.errorLog??(message=>console.error(message)),localVersion=deps.localVersion??VERSION,{args:cleanedArgs,sentinelPresent}=stripInternalReexecSentinels(argv),isHelpInvocation=cleanedArgs.includes("-h")||cleanedArgs.includes("--help");if(sentinelPresent||cleanedArgs[0]==="conductor"||cleanedArgs.includes("--dry-run")||isHelpInvocation)return runLocal(cleanedArgs);let latestVersion=localVersion,fetched=null;try{fetched=await resolveLatest()}catch{fetched=null}if(fetched&&(latestVersion=fetched),!isNewerVersion(localVersion,latestVersion))return runLocal(cleanedArgs);let{forwardedArgs,secretEnv,provenanceEnv}=prepareInstallReexecArguments(cleanedArgs),npxCmd=platform==="win32"?"npx.cmd":"npx",childArgs=["-y",`${MCP_PACKAGE_NAME}@latest`,"install",INSTALL_REEXEC_SENTINEL,...forwardedArgs];return new Promise(resolve2=>{let settled=!1,settle2=code=>{settled||(settled=!0,resolve2(code))},fail3=()=>{errorLog(INSTALL_REEXEC_FAILURE_MESSAGE),settle2(1)},child;try{child=doSpawn(npxCmd,childArgs,{shell:!1,stdio:"inherit",cwd,env:{...env,...secretEnv,...provenanceEnv}})}catch{fail3();return}child.on("close",code=>{if(typeof code=="number"){settle2(code);return}fail3()}),child.on("error",()=>{fail3()})})}init_version_generated();init_init();init_init();import{spawn as spawn10}from"child_process";import{readFile as readFile17,stat as stat11}from"fs/promises";import path47 from"path";init_mcp_identity();var CANONICAL_UPGRADE_COMMAND=`npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`,SAFE_VERSION_RE=/^[0-9A-Za-z.\-+]{1,64}$/;function sanitizeVersion(value){return typeof value=="string"&&SAFE_VERSION_RE.test(value)?value:"unknown"}function formatUpdateAdvice(currentVersion,latestVersion){return`Bridge API MCP server update available: running ${sanitizeVersion(currentVersion)}, latest ${sanitizeVersion(latestVersion)} \u2014 run ${CANONICAL_UPGRADE_COMMAND}`}function formatToolSurfaceUpdateAdvisory(currentVersion,latestVersion){return`NOTE: this Bridge API MCP server is running ${sanitizeVersion(currentVersion)}, but ${sanitizeVersion(latestVersion)} is published. Some tools may be missing or renamed in this older build. To update, run: ${CANONICAL_UPGRADE_COMMAND}`}init_launcher_config_inspection();init_mcp_identity();var INTERNAL_REEXEC_FLAG="--internal-reexec";function defaultReexec(cwd,oldVersion){let npxCmd=process.platform==="win32"?"npx.cmd":"npx";return new Promise(resolve2=>{let child=spawn10(npxCmd,["-y",`${MCP_PACKAGE_NAME}@latest`,"upgrade",INTERNAL_REEXEC_FLAG,"--old-version",oldVersion],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})})}function localInstallPath(cwd){return path47.join(cwd,"node_modules",...MCP_PACKAGE_NAME.split("/"))}async function defaultHasCompetingLocalInstall(cwd){try{return await stat11(localInstallPath(cwd)),!0}catch{return!1}}function defaultRemoveCompetingLocalInstall(cwd){let npmCmd=process.platform==="win32"?"npm.cmd":"npm";return new Promise(resolve2=>{let child=spawn10(npmCmd,["uninstall",MCP_PACKAGE_NAME],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code===0)),child.on("error",()=>resolve2(!1))})}function createDefaultUpgradeCliDeps(cwd){return{cwd,fetchLatestVersion,readFile:filePath=>readFile17(filePath,"utf-8"),hasCompetingLocalInstall:defaultHasCompetingLocalInstall,removeCompetingLocalInstall:defaultRemoveCompetingLocalInstall,runInit,reexecFromLatest:defaultReexec,stdout:m=>console.log(m),stderr:m=>console.error(m),env:process.env}}async function resolveUpgradeTarget(argv,deps){let fetched=await deps.fetchLatestVersion();if(fetched)return{version:fetched,confidence:"registry-confirmed"};if(argv.includes(INTERNAL_REEXEC_FLAG))return{version:VERSION,confidence:"internal-reexec"};let asserted=deps.env?.BAPI_UPGRADE_ASSUME_LATEST;return typeof asserted=="string"&&asserted.trim().toLowerCase()==="true"?{version:VERSION,confidence:"latest-invocation-confirmed"}:{version:VERSION,confidence:"running-version-only"}}function formatDryRunLine(found){let{relPath,targetVersion}=found,keyNote=found.legacyRegistration?` [\`${found.registrationKey}\` entry]`:"";switch(found.action){case"repin":return` ${relPath}: ${found.currentVersion} -> ${targetVersion}${keyNote}`;case"already-current":return` ${relPath}: already ${targetVersion}${keyNote}`;case"normalize-unpinned":return` ${relPath}: would normalize ${found.spec} -> ${MCP_PACKAGE_NAME}@${targetVersion}${keyNote}`;case"add":return` ${relPath}: would add ${MCP_PACKAGE_NAME}@${targetVersion}`;case"duplicate-registration":return` ${relPath}: blocked \u2014 ${describeLauncherReason("duplicate-registration")}`;case"create":return` ${relPath}: would create with ${MCP_PACKAGE_NAME}@${targetVersion}`;case"skip-worktree-shim":return` ${relPath}: skipped \u2014 worktree mcp-invoke shim (no published-package pin applies)`;case"skip-inactive":return null;default:return null}}function formatResultLine(action){let{relPath}=action;switch(action.kind){case"repinned":return` ${relPath}: ${action.fromVersion} -> ${action.toVersion}`;case"already-current":return` ${relPath}: already ${action.toVersion}`;case"normalized":return` ${relPath}: normalized ${action.fromSpec} -> ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"added":return` ${relPath}: added ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"created":return` ${relPath}: created with ${MCP_PACKAGE_NAME}@${action.toVersion}`;case"skipped-worktree-shim":return` ${relPath}: skipped \u2014 worktree mcp-invoke shim preserved`;case"skipped-ahead":return` ${relPath}: skipped \u2014 already pinned ahead at ${action.fromVersion}`;case"skipped-inactive":return null;default:return null}}function isEveryLauncherAlreadyCurrent(actions){let applicable=actions.filter(a=>a.kind!=="skipped-worktree-shim"&&a.kind!=="skipped-inactive"&&a.kind!=="skipped-ahead");return applicable.length===0?!1:applicable.every(a=>a.kind==="already-current")}async function runUpgradeCli(argv,injected){let deps={...createDefaultUpgradeCliDeps(injected?.cwd??process.cwd()),...injected},{stdout,stderr}=deps,isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes(INTERNAL_REEXEC_FLAG),target=await resolveUpgradeTarget(argv,deps);if(target.confidence==="registry-confirmed"&&isNewerVersion(VERSION,target.version)&&!isInternalReexec&&!isDryRun)return stdout(`Update available: ${VERSION} -> ${target.version}. Re-executing from @latest...`),deps.reexecFromLatest(deps.cwd,VERSION);if(target.confidence==="running-version-only")return stderr("Bridge API upgrade aborted: could not confirm the latest published version (the npm registry lookup failed) and this process was not started from @latest."),stderr(` Nothing was changed. Re-run the canonical command: ${CANONICAL_UPGRADE_COMMAND}`),1;let isCreatable=deps.isCreatable??(relPath=>relPath===".mcp.json"),inspections;try{inspections=await inspectLauncherConfigs({cwd:deps.cwd,targetVersion:target.version,readFile:deps.readFile,isCreatable,targets:LAUNCHER_CONFIG_TARGETS})}catch(err){let msg=err instanceof Error?err.message:String(err);return stderr(`Bridge API upgrade failed: could not inspect launcher configs: ${msg}`),1}let blocking=inspections.filter(isPrerequisiteFailure),ahead=inspections.filter(i=>i.action==="ahead");if(blocking.length>0||ahead.length>0){stderr("Bridge API upgrade aborted \u2014 these configs cannot be safely reconciled:");for(let found of[...blocking,...ahead])stderr(` ${found.relPath}: ${describeLauncherReason(found.reason)}`);return stderr(" Nothing was changed. Resolve these by hand, then re-run the upgrade."),1}if(isDryRun){stdout(`
8041
8041
  [Dry Run] Upgrade preview:`),target.confidence==="registry-confirmed"&&isNewerVersion(VERSION,target.version)&&!isInternalReexec&&stdout(`- Would re-exec from @latest to upgrade the CLI ${VERSION} -> ${target.version}`),stdout(`- Would detect and remove competing local install in node_modules/${MCP_PACKAGE_NAME} if present.`),stdout(`- Launcher pins (target ${MCP_PACKAGE_NAME}@${target.version}):`);let lines=inspections.map(formatDryRunLine).filter(l=>l!==null);if(lines.length===0)stdout(" (no applicable launcher configs found)");else for(let line of lines)stdout(line);return isEveryLauncherAlreadyCurrent(toPlannedActions(inspections))&&stdout("- Would print: Already up-to-date."),stdout("- Would refresh scaffolded artifacts (commands, agents, pipelines)."),stdout("- Would print a reminder to reload/restart your MCP."),0}if(await deps.hasCompetingLocalInstall(deps.cwd)){if(stdout("Found a competing local installation in node_modules. Removing so npx resolves the pinned version..."),!await deps.removeCompetingLocalInstall(deps.cwd))return stderr(`Bridge API upgrade failed: could not remove node_modules/${MCP_PACKAGE_NAME}.`),stderr(` Remove it manually (rm -rf node_modules/${MCP_PACKAGE_NAME}), then re-run the upgrade.`),1;stdout(" Removed competing local install.")}let initResult;try{stdout(`
8042
8042
  Reconciling launcher pins to ${MCP_PACKAGE_NAME}@${target.version}...
8043
8043
  `),initResult=await deps.runInit(deps.cwd,{targetVersion:target.version,suppressConfigSummary:!0})}catch(err){if(err instanceof InitPreflightError){stderr("Bridge API upgrade aborted \u2014 these configs cannot be safely reconciled:");for(let failure of err.failures)stderr(` ${failure.relPath}: ${failure.reason}`);return 1}let msg=err instanceof Error?err.message:String(err);return stderr(`Bridge API upgrade failed: ${msg}`),1}let verified;try{verified=await inspectLauncherConfigs({cwd:deps.cwd,targetVersion:target.version,readFile:deps.readFile,isCreatable,targets:LAUNCHER_CONFIG_TARGETS})}catch(err){let msg=err instanceof Error?err.message:String(err);return stderr(`Bridge API upgrade failed: could not verify launcher configs after writing: ${msg}`),1}let verifiedByPath=new Map(verified.map(v=>[v.relPath,v])),verificationFailures=[];for(let action of initResult.configActions){if(action.kind==="skipped-worktree-shim"||action.kind==="skipped-inactive"||action.kind==="skipped-ahead"){let after2=verifiedByPath.get(action.relPath);action.kind==="skipped-worktree-shim"&&after2?.action!=="skip-worktree-shim"&&verificationFailures.push(`${action.relPath}: a worktree shim was expected to be preserved but no longer looks like one`);continue}let after=verifiedByPath.get(action.relPath);if(!after){verificationFailures.push(`${action.relPath}: config disappeared after the write`);continue}if(after.action==="duplicate-registration"){verificationFailures.push(`${action.relPath}: reconciliation left TWO Bridge registrations in this config (a canonical entry was written beside a legacy one)`);continue}(after.action!=="already-current"||after.currentVersion!==target.version)&&verificationFailures.push(`${action.relPath}: expected ${MCP_PACKAGE_NAME}@${target.version} after the write, found ${after.spec??"no recognizable package token"}`),action.registrationKey&&after.registrationKey!==action.registrationKey&&verificationFailures.push(`${action.relPath}: expected the Bridge entry to remain under \`${action.registrationKey}\` after the write, found ${after.registrationKey?`\`${after.registrationKey}\``:"no recognized registration"}`)}let resultLines=initResult.configActions.map(formatResultLine).filter(l=>l!==null);if(resultLines.length>0){stdout("Launcher pins:");for(let line of resultLines)stdout(line)}if(stdout("Refreshed scaffolded artifacts (commands, agents, pipelines)."),verificationFailures.length>0){stderr(`
@@ -8088,7 +8088,7 @@ Dispatches (${state.dispatches.length}):`);for(let d of state.dispatches){let ru
8088
8088
  `)),0}catch(error){if(error instanceof BridgeApiError&&error.status===404)return parsed.json?console.log(JSON.stringify({epic_key:parsed.epicKey,status:"unknown",state:null})):console.log("No such epic found."),0;let envelope2=toConductorErrorEnvelope(error);return parsed.json?console.log(JSON.stringify(envelope2)):console.error(`Error: ${formatConductorErrorLine(envelope2)}`),1}}var SUPERVISE_VALUE_FLAGS=new Set(["--run-id","--wake-interval-ms","--global-timeout-ms","--escalation-cooldown-ms"]),SUPERVISE_BOOL_FLAGS=new Set(["--help"]);function parsePositiveIntFlag(values,flag){let raw=values.get(flag);if(raw===void 0)return;if(!/^\d+$/.test(raw.trim()))throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);let n=Number.parseInt(raw.trim(),10);if(!Number.isFinite(n))throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);return n}function parseSuperviseArgs(argv){let{values,bools}=tokenizeFlags(argv,SUPERVISE_VALUE_FLAGS,SUPERVISE_BOOL_FLAGS);if(bools.has("--help"))return{runId:"",overrides:{},help:!0};let runIdRaw=values.get("--run-id");if(runIdRaw===void 0||runIdRaw.trim().length===0)throw new ConductorValidationError('Flag "--run-id" is required for supervise and must be non-empty.');let overrides={},wake=parsePositiveIntFlag(values,"--wake-interval-ms");wake!==void 0&&(overrides.wake_interval_ms=wake);let globalTimeout=parsePositiveIntFlag(values,"--global-timeout-ms");globalTimeout!==void 0&&(overrides.global_timeout_ms=globalTimeout);let cooldown=parsePositiveIntFlag(values,"--escalation-cooldown-ms");return cooldown!==void 0&&(overrides.escalation_cooldown_ms=cooldown),{runId:runIdRaw.trim(),overrides,help:!1}}async function runSuperviseCommand(argv){let parsed=parseSuperviseArgs(argv);if(parsed.help)return console.log(getConductorUsage()),0;let config=resolveSupervisorConfig(parsed.overrides);console.log(`[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms global_timeout=${config.global_timeout_ms}ms`);let{runSupervisor:runSupervisor2}=await Promise.resolve().then(()=>(init_supervisor_runtime(),supervisor_runtime_exports));return(await runSupervisor2({run_id:parsed.runId,config})).exit_code}async function runPurgeCommand(argv){let{bools}=tokenizeFlags(argv,new Set,DIAGNOSTIC_BOOL_FLAGS);if(bools.has("--help"))return console.log(getConductorUsage()),0;let result=await purgeConductorLedger();return bools.has("--json")?(console.log(JSON.stringify(result)),0):(console.log([`Conductor ledger purged (existed: ${result.existed}).`,` events: ${result.deleted.events}`,` messages: ${result.deleted.messages}`,` supervisor_projection: ${result.deleted.supervisor_projection}`].join(`
8089
8089
  `)),0)}async function runEpicImplementerCli(argv){let parsed=parseConductorArgs(argv);if(parsed.kind==="help")return console.log(getConductorUsage()),0;if(parsed.kind==="error")return console.error(`Error: ${parsed.message}`),1;try{switch(parsed.command){case"emit-event":return await runEmitEventCommand(parsed.argv);case"supervise":return await runSuperviseCommand(parsed.argv);case"epic-tick":return await runEpicTickCommand(parsed.argv);case"approve-plan":return await runApprovePlanCommand(parsed.argv);case"epic-status":return await runEpicStatusCommand(parsed.argv);case"send-message":return await runSendMessageCommand(parsed.argv);case"check-messages":return await runCheckMessagesCommand(parsed.argv);case"doctor":return await runDoctorCommand(parsed.argv);case"readiness":{let{runConductorReadinessCommand:runConductorReadinessCommand2}=await Promise.resolve().then(()=>(init_readiness_cli(),readiness_cli_exports));return await runConductorReadinessCommand2(parsed.argv)}case"purge":return await runPurgeCommand(parsed.argv);case"install-git-hooks":return runInstallGitHooksCommand(parsed.argv);case"git-hook":return await runGitHookCommand(parsed.argv);case CONDUCTOR_HOOK_BIN_ACTION:return runHookBinDiagnosticCommand();case"file-scope-guard":return runFileScopeGuardCli();case"stop-run":{let{runStopRunCommand:runStopRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runStopRunCommand2(parsed.argv)}case"abandon-run":{let{runAbandonRunCommand:runAbandonRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAbandonRunCommand2(parsed.argv)}case"unpark":{let{runUnparkCommand:runUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runUnparkCommand2(parsed.argv)}case"adopt-current-head-and-unpark":{let{runAdoptCurrentHeadAndUnparkCommand:runAdoptCurrentHeadAndUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAdoptCurrentHeadAndUnparkCommand2(parsed.argv)}case"scope-reparse":{let{runScopeReparseCommand:runScopeReparseCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runScopeReparseCommand2(parsed.argv)}case"dispatch-remediation":{let{runDispatchRemediationCommand:runDispatchRemediationCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runDispatchRemediationCommand2(parsed.argv)}case"merge-child":{let{runMergeChildCommand:runMergeChildCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runMergeChildCommand2(parsed.argv)}default:return console.error('Error: Unknown command. Run "epic-implementer --help" for usage.'),1}}catch(error){let envelope2=toConductorErrorEnvelope(error);return console.error(`Error: ${formatConductorErrorLine(envelope2)}`),1}}init_pr_ci_producer();import{generateDecisionPageHtml}from"./decision-page-template.js";import{z as z16}from"zod";var ActionableItemSchema=z16.object({id:z16.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores"),question:z16.string().min(1),original_question:z16.string().optional().describe("Optional display-only field: the clarifying question or critique point as originally raised; soft cap ~30 words. Omit it (or pass an empty string) for non-review callers \u2014 the renderer omits the section when it is absent or blank."),why_it_matters:z16.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),recommendation_explanation:z16.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),codebase_evidence:z16.string().optional().describe("Optional display-only field: combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block, which is omitted when this field is absent or blank."),source:z16.string().optional().describe(`Optional source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'. When absent the rendered card emits data-source="".`),recommendation_index:z16.number().int().min(0).describe("0-based index of the recommended option in the options array"),options:z16.array(z16.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),option_consequences:z16.array(z16.string().min(1)).min(2).max(4).describe("Behavioral consequence per branch, parallel to options. Must have 2\u20134 entries; length must equal options.length.")}).superRefine((item,ctx)=>{item.option_consequences.length!==item.options.length&&ctx.addIssue({code:z16.ZodIssueCode.custom,path:["option_consequences"],message:`option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`}),item.recommendation_index>=item.options.length&&ctx.addIssue({code:z16.ZodIssueCode.custom,path:["recommendation_index"],message:`recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`})}),DecisionPageLabelsSchema=z16.object({title:z16.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),intro:z16.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),section_heading:z16.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),improvements_heading:z16.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')}),SystemGoalNfrSchema=z16.object({category:z16.string().min(1).describe("Canonical NFR category, e.g. security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility."),requirement:z16.string().min(1).describe("The non-functional requirement itself."),implication:z16.string().min(1).describe("What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."),status:z16.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved (also surface as an actionable_items card).")}),AcceptanceCriterionSchema=z16.object({id:z16.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores").describe("Stable per-criterion id, e.g. AC-1. Becomes the captured-feedback JSON key."),criterion:z16.string().min(1).describe("What the system must do, stated concretely enough to be verified."),verification:z16.string().min(1).describe("How we would confirm this criterion is met. Required \u2014 a criterion with no way to check it is not yet a criterion."),status:z16.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved.")}),SystemGoalsSchema=z16.object({business_goal:z16.string().min(1).describe("The business goal this work serves."),desired_end_state:z16.string().min(1).describe("The end-state the system should reach."),system_behavior:z16.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),acceptance_criteria:z16.array(AcceptanceCriterionSchema).optional().default([]).describe("What the system must do, as verifiable criteria. Implementation options should be derived from these rather than the reverse."),nfrs:z16.array(SystemGoalNfrSchema).optional().default([])}),ImplementationOrderItemSchema=z16.object({title:z16.string().min(1).describe("Short title of the slice / child ticket."),depends_on:z16.array(z16.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),recommended_after:z16.array(z16.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),rationale:z16.string().min(1).describe("Why this slice sits at this point in the order.")}),DecisionPageInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) is the ticket-review decision-capture page and is unaffected by the planning fields. "pre_ticket_planning" additionally renders the read-only system_goals and implementation_order sections for pre-ticket epic/task framing.'),system_goals:SystemGoalsSchema.optional().describe("pre_ticket_planning only: business goal, desired end-state, system behavior, acceptance criteria (what the system must do), and classified NFRs (the standards it must meet). Acceptance criteria and NFRs render with per-item stance controls. Unresolved (open) NFRs should ALSO be passed as actionable_items so the human can decide them."),implementation_order:z16.array(ImplementationOrderItemSchema).optional().describe("pre_ticket_planning epic surfaces only: read-only recommended implementation order (hard depends_on vs soft recommended_after). No Jira links are created from this."),output_subdir:z16.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). Validated strictly: no absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z16.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html and contain no path separators; the .html suffix is required and never auto-appended.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading). Presentation-only; does not change data-testid hooks or the submitted JSON shape."),actionable_items:z16.array(ActionableItemSchema).optional().default([]).describe("Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."),clear_improvements:z16.array(z16.object({id:z16.string().min(1).describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),title:z16.string().min(1),action:z16.string().min(1),confidence:z16.string().min(1),source:z16.string().min(1).describe("Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user \u2014 the confirmed-improvements list shows title/confidence/action only.")})).optional().default([]).describe("Confirmed improvements displayed as informational list, not submitted.")},DecisionPageInputSchema=z16.object(DecisionPageInputShape),DecisionPageLeanInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'),output_subdir:z16.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z16.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."),content:z16.record(z16.string(),z16.unknown()).optional().describe("Contains deferred heavy payloads like actionable_items or system_goals.")};import{writeFile as writeFile14,mkdir as mkdir15}from"fs/promises";import path56 from"path";function slugify(text3,maxLength=60){return text3.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-").replace(/-+/g,"-").slice(0,maxLength).replace(/-$/,"")}function sanitizeProviderForFilename(provider){return provider.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")||"provider"}function buildCouncilResultFilename(envelope2,row2,subject){let providerSegment=sanitizeProviderForFilename(row2.provider),subjectSlug=subject?slugify(subject):"",shortId=envelope2.council_id.slice(0,8);return subjectSlug?`${subjectSlug}-${shortId}-${providerSegment}.md`:`${envelope2.council_id}-${providerSegment}.md`}async function saveCouncilResultsToDir(envelope2,dir,subject){let savedPaths=[];for(let row2 of envelope2.results){let markdown=row2.markdown;if(!markdown)continue;let filename=buildCouncilResultFilename(envelope2,row2,subject),filePath=path56.join(dir,filename);try{await mkdir15(dir,{recursive:!0}),await writeFile14(filePath,markdown,"utf-8"),savedPaths.push(filePath)}catch{}}return savedPaths}import{createHash as createHash11}from"node:crypto";function deriveIdeaHash(idea){let normalized=String(idea??"").trim().toLowerCase().replace(/\s+/g," ");return createHash11("sha256").update(normalized).digest("hex").slice(0,12)}import path57 from"path";import{Worker}from"worker_threads";import{PNG}from"pngjs";import pixelmatch from"pixelmatch";import{isMainThread,parentPort,workerData}from"worker_threads";var PIXELMATCH_COLOR_THRESHOLD=.1,DEFAULT_PASS_MISMATCH_PCT=2,MAX_DIFF_REGIONS=10;function decodePng(buffer,label){try{let png=PNG.sync.read(Buffer.from(buffer));return!Number.isInteger(png.width)||!Number.isInteger(png.height)||png.width<=0||png.height<=0?{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Decoded ${label} PNG has invalid dimensions.`}:{width:png.width,height:png.height,data:png.data}}catch{return{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Failed to decode ${label} image as PNG.`}}}function extractOverlap(src,srcW,overlapW,overlapH){let out=new Uint8Array(overlapW*overlapH*4);for(let y=0;y<overlapH;y++){let srcRow=y*srcW*4,dstRow=y*overlapW*4;out.set(src.subarray(srcRow,srcRow+overlapW*4),dstRow)}return out}function buildMaskGrid(boxes,unionW,unionH){let grid=new Uint8Array(unionW*unionH);for(let box of boxes){let x0=Math.max(0,Math.floor(box.x)),y0=Math.max(0,Math.floor(box.y)),x1=Math.min(unionW,Math.floor(box.x+box.width)),y1=Math.min(unionH,Math.floor(box.y+box.height));for(let y=y0;y<y1;y++)for(let x=x0;x<x1;x++)grid[y*unionW+x]=1}return grid}function applyMaskToOverlap(buf,overlapW,overlapH,maskGrid,unionW){for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++)if(maskGrid[y*unionW+x]===1){let off=(y*overlapW+x)*4;buf[off]=0,buf[off+1]=0,buf[off+2]=0,buf[off+3]=255}}function extractDiffRegions(mask,width,height,maxRegions){let visited=new Uint8Array(width*height),regions=[],stack=[];for(let start=0;start<mask.length;start++){if(mask[start]===0||visited[start]===1)continue;let minX=width,minY=height,maxX=-1,maxY=-1,pixels=0;for(stack.length=0,stack.push(start),visited[start]=1;stack.length>0;){let idx=stack.pop(),x=idx%width,y=(idx-x)/width;if(pixels++,x<minX&&(minX=x),y<minY&&(minY=y),x>maxX&&(maxX=x),y>maxY&&(maxY=y),x>0){let n=idx-1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(x<width-1){let n=idx+1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y>0){let n=idx-width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y<height-1){let n=idx+width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}}regions.push({x:minX,y:minY,width:maxX-minX+1,height:maxY-minY+1,pixels})}return regions.sort((a,b)=>b.pixels!==a.pixels?b.pixels-a.pixels:a.y!==b.y?a.y-b.y:a.x-b.x),regions.slice(0,Math.max(0,maxRegions))}function computePngVisualDiff(input){let comp=decodePng(input.compPngBuffer,"comp");if("ok"in comp&&comp.ok===!1)return comp;let render=decodePng(input.renderPngBuffer,"render");if("ok"in render&&render.ok===!1)return render;let compImg=comp,renderImg=render,dimensionMatch=compImg.width===renderImg.width&&compImg.height===renderImg.height,unionW=Math.max(compImg.width,renderImg.width),unionH=Math.max(compImg.height,renderImg.height),overlapW=Math.min(compImg.width,renderImg.width),overlapH=Math.min(compImg.height,renderImg.height),maskGrid=buildMaskGrid(input.maskBoxes??[],unionW,unionH),output=new Uint8Array(unionW*unionH*4),diffMask=new Uint8Array(unionW*unionH),differingPixels=0;if(overlapW>0&&overlapH>0){let compOverlap=extractOverlap(compImg.data,compImg.width,overlapW,overlapH),renderOverlap=extractOverlap(renderImg.data,renderImg.width,overlapW,overlapH);applyMaskToOverlap(compOverlap,overlapW,overlapH,maskGrid,unionW),applyMaskToOverlap(renderOverlap,overlapW,overlapH,maskGrid,unionW);let overlapOut=new Uint8Array(overlapW*overlapH*4);try{differingPixels=pixelmatch(compOverlap,renderOverlap,overlapOut,overlapW,overlapH,{threshold:input.pixelmatchColorThreshold,includeAA:!1,diffColor:[255,0,0],diffColorAlt:[255,0,0],aaColor:[255,255,0]})}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Pixel comparison failed."}}for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++){let so=(y*overlapW+x)*4,uo=(y*unionW+x)*4;output[uo]=overlapOut[so],output[uo+1]=overlapOut[so+1],output[uo+2]=overlapOut[so+2],output[uo+3]=255,overlapOut[so]===255&&overlapOut[so+1]===0&&overlapOut[so+2]===0&&(diffMask[y*unionW+x]=1)}}for(let y=0;y<unionH;y++)for(let x=0;x<unionW;x++){let inComp=x<compImg.width&&y<compImg.height,inRender=x<renderImg.width&&y<renderImg.height;if(inComp===inRender||maskGrid[y*unionW+x]===1)continue;let off=(y*unionW+x)*4;output[off]=255,output[off+1]=0,output[off+2]=0,output[off+3]=255,diffMask[y*unionW+x]=1,differingPixels++}let diffRegions=extractDiffRegions(diffMask,unionW,unionH,input.maxRegions??MAX_DIFF_REGIONS),totalPixels=unionW*unionH,mismatchPct=totalPixels>0?differingPixels/totalPixels*100:0,passed=dimensionMatch&&mismatchPct<=input.passMismatchPct,heatmapBase64;try{let png=new PNG({width:unionW,height:unionH});png.data=Buffer.from(output),heatmapBase64=PNG.sync.write(png).toString("base64")}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Failed to encode diff heatmap."}}return{ok:!0,mismatch_pct:mismatchPct,dimension_match:dimensionMatch,passed,diff_regions:diffRegions,comp_dimensions:{width:compImg.width,height:compImg.height},render_dimensions:{width:renderImg.width,height:renderImg.height},differing_pixels:differingPixels,total_pixels:totalPixels,heatmap_base64:heatmapBase64}}if(!isMainThread&&parentPort)try{let result=computePngVisualDiff(workerData);parentPort.postMessage(result)}catch(err){parentPort.postMessage({ok:!1,error:"DIFF_FAILED",status:500,message:`Diff worker failed: ${err instanceof Error?err.message:"unknown error"}`})}var NAV_TIMEOUT_MS=3e4,NETWORK_IDLE_TIMEOUT_MS=15e3,FONTS_READY_TIMEOUT_MS=5e3,SCREENSHOT_TIMEOUT_MS=2e4,MAX_VIEWPORT_DIMENSION=16384,MAX_VIEWPORT_PIXELS=32e6,MAX_COMP_BYTES=25*1024*1024,DETERMINISTIC_CSS="* { animation: none !important; transition: none !important; caret-color: transparent !important; }";function textJson(value){return{type:"text",text:JSON.stringify(value,null,2)}}function errorContent(error,status,message,extra){return{content:[{type:"text",text:JSON.stringify({error,status,message,...extra??{}})}]}}var PNG_MAGIC=[137,80,78,71,13,10,26,10];function sniffImageFormat(bytes){return bytes.length>=8&&PNG_MAGIC.every((b,i)=>bytes[i]===b)?"png":bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255?"jpeg":null}function readPngDimensions(bytes){if(bytes.length<24||sniffImageFormat(bytes)!=="png")return{ok:!1,message:"Not a valid PNG header."};if(bytes[12]!==73||bytes[13]!==72||bytes[14]!==68||bytes[15]!==82)return{ok:!1,message:"PNG IHDR chunk not found."};let width=readUInt32BE(bytes,16),height=readUInt32BE(bytes,20);return width<=0||height<=0?{ok:!1,message:"PNG reports non-positive dimensions."}:{ok:!0,width,height}}function readJpegDimensions(bytes){if(sniffImageFormat(bytes)!=="jpeg")return{ok:!1,message:"Not a valid JPEG header."};let offset=2,len=bytes.length;for(;offset+1<len;){if(bytes[offset]!==255){offset++;continue}let marker=bytes[offset+1];for(;marker===255&&offset+1<len;)offset++,marker=bytes[offset+1];if(offset+=2,marker>=208&&marker<=217||marker===1)continue;if(offset+1>=len)break;let segLen=readUInt16BE(bytes,offset);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204){if(offset+5>=len)break;let height=readUInt16BE(bytes,offset+3),width=readUInt16BE(bytes,offset+5);return width<=0||height<=0?{ok:!1,message:"JPEG SOF reports non-positive dimensions."}:{ok:!0,width,height}}offset+=segLen}return{ok:!1,message:"No supported JPEG SOF marker found."}}function readUInt32BE(b,o){return b[o]*16777216+(b[o+1]<<16)+(b[o+2]<<8)+b[o+3]}function readUInt16BE(b,o){return(b[o]<<8)+b[o+1]}function isPositiveInt(n){return Number.isInteger(n)&&n>0}function resolveViewport(input,compDimensions){let vp=input.viewport??compDimensions;return!isPositiveInt(vp.width)||!isPositiveInt(vp.height)?{ok:!1,error:"INVALID_VIEWPORT",status:400,message:`Viewport must be positive integers, got ${vp.width}x${vp.height}.`}:vp.width>MAX_VIEWPORT_DIMENSION||vp.height>MAX_VIEWPORT_DIMENSION||vp.width*vp.height>MAX_VIEWPORT_PIXELS?{ok:!1,error:"IMAGE_TOO_LARGE",status:413,message:`Requested render area ${vp.width}x${vp.height} exceeds the local pixel guard.`}:{ok:!0,viewport:{width:vp.width,height:vp.height}}}function toUint8(bytes){return bytes instanceof Uint8Array?bytes:Buffer.from(bytes)}async function resolveCompRef(compRef,deps){let candidates=[];if(path57.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path57.resolve(root,compRef));let cwdCandidate=path57.resolve(process.cwd(),compRef);candidates.includes(cwdCandidate)||candidates.push(cwdCandidate)}for(let candidate of candidates)try{let st=await deps.stat(candidate);if(st&&st.isFile())return{ok:!0,bytes:toUint8(await deps.readFile(candidate)),source:"local",sourcePath:candidate,warnings:[]}}catch{}let trimmed=compRef.trim(),lookup=/^\d+$/.test(trimmed)?{kind:"attachment_id",attachment_id:trimmed}:{kind:"filename",filename:compRef},fetched=await deps.fetchAttachmentBytes(lookup);if(!fetched.ok)return{ok:!1,error:fetched.error,status:fetched.status,message:fetched.message};let bytes=toUint8(fetched.bytes),warnings=[];try{let dir=await deps.getDocsPath("visual-diffs"),rawName=fetched.filename||(lookup.kind==="attachment_id"?`attachment-${lookup.attachment_id}`:lookup.filename),base=path57.basename(rawName),target=path57.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path57.resolve(dir)+path57.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(bytes))):warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.")}catch(err){warnings.push(`Attachment comp copy could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`)}return{ok:!0,bytes,source:"attachment",warnings}}async function loadPlaywright(){try{let mod=await import("playwright"),chromium=mod?.chromium??mod?.default?.chromium;return!chromium||typeof chromium.launch!="function"?{ok:!1}:{ok:!0,playwright:{chromium}}}catch{return{ok:!1}}}async function launchBrowser(playwright){try{return{ok:!0,browser:await playwright.chromium.launch({headless:!0})}}catch(err){return{ok:!1,error:"BROWSER_UNAVAILABLE",status:503,message:`Chromium could not be launched: ${err instanceof Error?err.message:"unknown error"}. Run "npx playwright install chromium".`}}}function normalizeMaskBoxes(raw,viewport){let boxes=[];for(let r of raw){let x0=Math.max(0,Math.floor(r.x)),y0=Math.max(0,Math.floor(r.y)),x1=Math.min(viewport.width,Math.ceil(r.x+r.width)),y1=Math.min(viewport.height,Math.ceil(r.y+r.height)),width=x1-x0,height=y1-y0;width>0&&height>0&&boxes.push({x:x0,y:y0,width,height})}return boxes.sort((a,b)=>a.y!==b.y?a.y-b.y:a.x!==b.x?a.x-b.x:a.width!==b.width?a.width-b.width:a.height-b.height),boxes}async function collectMaskBoxes(page,selectors,viewport){if(!selectors||selectors.length===0)return[];let raw=await page.evaluate(sels=>{let out=[];for(let sel of sels)document.querySelectorAll(sel).forEach(el=>{let rect=el.getBoundingClientRect();out.push({x:rect.x,y:rect.y,width:rect.width,height:rect.height})});return out},selectors);return normalizeMaskBoxes(Array.isArray(raw)?raw:[],viewport)}async function captureRenderPng(browser,targetUrl,viewport,maskSelectors){let context=await browser.newContext({viewport,deviceScaleFactor:1}),page;try{page=await context.newPage();try{await page.goto(targetUrl,{timeout:NAV_TIMEOUT_MS,waitUntil:"load"}),await page.waitForLoadState("networkidle",{timeout:NETWORK_IDLE_TIMEOUT_MS})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Failed to load ${targetUrl}: ${err instanceof Error?err.message:"unknown error"}`}}await page.addStyleTag({content:DETERMINISTIC_CSS}),await settleFonts(page);let maskBoxes=await collectMaskBoxes(page,maskSelectors,viewport),png;try{png=await page.screenshot({clip:{x:0,y:0,width:viewport.width,height:viewport.height},timeout:SCREENSHOT_TIMEOUT_MS,animations:"disabled"})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Screenshot capture failed: ${err instanceof Error?err.message:"unknown error"}`}}return{ok:!0,png:toUint8(png),maskBoxes,dimensions:viewport}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function settleFonts(page){try{await Promise.race([page.evaluate(()=>{let d=document;return d.fonts&&d.fonts.ready?d.fonts.ready.then(()=>!0):!0}),new Promise(resolve2=>setTimeout(resolve2,FONTS_READY_TIMEOUT_MS))])}catch{}}function isTimeoutError(err){let msg=err instanceof Error?err.message:String(err??"");return/timeout|timed out|TimeoutError/i.test(msg)}async function normalizeCompToPng(browser,compBytes,format,dims){if(format==="png")return{ok:!0,png:Buffer.from(compBytes)};let context=await browser.newContext({viewport:dims,deviceScaleFactor:1}),page;try{page=await context.newPage(),page.setContent&&await page.setContent("<!doctype html><html><body></body></html>");let dataUrl=`data:image/jpeg;base64,${Buffer.from(compBytes).toString("base64")}`,base64=(await page.evaluate(async arg=>{let img=new Image;await new Promise((resolve2,reject)=>{img.onload=()=>resolve2(),img.onerror=()=>reject(new Error("image load failed")),img.src=arg.url});let canvas=document.createElement("canvas");canvas.width=arg.w,canvas.height=arg.h;let ctx=canvas.getContext("2d");if(!ctx)throw new Error("no 2d context");return ctx.drawImage(img,0,0,arg.w,arg.h),canvas.toDataURL("image/png")},{url:dataUrl,w:dims.width,h:dims.height})).split(",")[1]??"";return base64?{ok:!0,png:Buffer.from(base64,"base64")}:{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:"Browser-side JPEG normalization produced no PNG data."}}catch(err){return{ok:!1,error:"UNSUPPORTED_COMP_IMAGE",status:400,message:`Failed to normalize JPEG comp to PNG: ${err instanceof Error?err.message:"unknown error"}`}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function computeVisualDiffInWorker(args,deps){try{return await runInWorker(args)}catch(err){deps?.logger?.(`visual_diff: worker unavailable, computing inline (${err instanceof Error?err.message:"unknown error"}).`);try{return computePngVisualDiff(args)}catch(inlineErr){return{ok:!1,error:"DIFF_FAILED",status:500,message:`Diff computation failed: ${inlineErr instanceof Error?inlineErr.message:"unknown error"}`}}}}function runInWorker(args){return new Promise((resolve2,reject)=>{let workerRelative="./visual-diff-worker.js",workerUrl=new URL(workerRelative,import.meta.url),settled=!1,worker;try{worker=new Worker(workerUrl,{workerData:args})}catch(err){reject(err);return}worker.once("message",msg=>{settled=!0,resolve2(msg),worker.terminate()}),worker.once("error",err=>{settled||reject(err)}),worker.once("exit",code=>{!settled&&code!==0&&reject(new Error(`diff worker exited with code ${code}`))})})}async function saveHeatmap(heatmapBase64,deps){try{let dir=await deps.getDocsPath("visual-diffs"),target=path57.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path57.resolve(dir)+path57.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(heatmapBase64,"base64")),{ok:!0,path:target}):{ok:!1,warning:"Heatmap not saved: resolved path escaped the visual-diffs directory."}}catch(err){return{ok:!1,warning:`Heatmap could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`}}}async function runVisualDiff(input,deps){try{let warnings=[],resolved=await resolveCompRef(input.comp_ref,deps);if(!resolved.ok)return errorContent(resolved.error,resolved.status,resolved.message);warnings.push(...resolved.warnings);let compBytes=resolved.bytes;if(compBytes.length>MAX_COMP_BYTES)return errorContent("IMAGE_TOO_LARGE",413,`Comp image is ${compBytes.length} bytes, exceeding the ${MAX_COMP_BYTES}-byte guard.`);let format=sniffImageFormat(compBytes);if(!format)return errorContent("UNSUPPORTED_COMP_IMAGE",400,"comp_ref resolved but is not a decodable PNG or JPEG image.");let dims=format==="png"?readPngDimensions(compBytes):readJpegDimensions(compBytes);if(!dims.ok)return errorContent("UNSUPPORTED_COMP_IMAGE",400,dims.message);let compDimensions={width:dims.width,height:dims.height},vp=resolveViewport(input,compDimensions);if(!vp.ok)return errorContent(vp.error,vp.status,vp.message);let loaded=await(deps.loadPlaywright??loadPlaywright)();if(!loaded.ok)return errorContent("BROWSER_UNAVAILABLE",503,'The optional Playwright browser runtime is not available. Install it with "npm i playwright && npx playwright install chromium".');let launch=await launchBrowser(loaded.playwright);if(!launch.ok)return errorContent(launch.error,launch.status,launch.message);let browser=launch.browser,capture,normalized;try{if(capture=await captureRenderPng(browser,input.target_url,vp.viewport,input.mask_selectors),!capture.ok)return errorContent(capture.error,capture.status,capture.message);if(normalized=await normalizeCompToPng(browser,compBytes,format,compDimensions),!normalized.ok)return errorContent(normalized.error,normalized.status,normalized.message)}finally{try{await browser.close()}catch{}}let passMismatchPct=typeof input.threshold=="number"?input.threshold:DEFAULT_PASS_MISMATCH_PCT,diff=await computeVisualDiffInWorker({compPngBuffer:normalized.png,renderPngBuffer:capture.png,maskBoxes:capture.maskBoxes,passMismatchPct,pixelmatchColorThreshold:PIXELMATCH_COLOR_THRESHOLD},deps);if(!diff.ok)return errorContent(diff.error,diff.status,diff.message);let saved=await saveHeatmap(diff.heatmap_base64,deps),heatmapPath=saved.ok?saved.path:null;saved.ok||warnings.push(saved.warning);let result={mismatch_pct:diff.mismatch_pct,dimension_match:diff.dimension_match,diff_regions:diff.diff_regions,heatmap_path:heatmapPath,comp_dimensions:diff.comp_dimensions,render_dimensions:diff.render_dimensions,threshold_used:passMismatchPct,passed:diff.passed};return diff.dimension_match||(result.message=`Render dimensions ${diff.render_dimensions.width}x${diff.render_dimensions.height} differ from comp dimensions ${diff.comp_dimensions.width}x${diff.comp_dimensions.height}. Images were NOT rescaled, so this result is automatically not passed; the diff covers the union region.`),warnings.length>0&&(result.warnings=warnings),{content:[textJson(result),{type:"image",data:diff.heatmap_base64,mimeType:"image/png"}]}}catch(err){return errorContent("VISUAL_DIFF_FAILED",500,`visual_diff failed: ${err instanceof Error?err.message:"unknown error"}`)}}function validateEstimateEpicInput(input){let hasEpic=typeof input.epic_key=="string"&&input.epic_key.trim().length>0,hasKeys=Array.isArray(input.ticket_keys);return hasEpic&&hasKeys?"epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.":!hasEpic&&!hasKeys?"Exactly one of epic_key or ticket_keys is required.":hasKeys&&input.ticket_keys.length===0?"ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.":null}function buildEstimateEpicErrorEnvelope(code,message,extras){return JSON.stringify({error:code,message,...extras??{}},null,2)}async function runEstimateEpic(input,deps){let validationError2=validateEstimateEpicInput(input);if(validationError2)return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("VALIDATION_ERROR",validationError2)}]};let payload={repo_name:deps.repoName};typeof input.epic_key=="string"&&(payload.epic_key=input.epic_key),Array.isArray(input.ticket_keys)&&(payload.ticket_keys=input.ticket_keys),typeof input.allow_partial=="boolean"&&(payload.allow_partial=input.allow_partial);let fetchImpl=deps.fetchImpl??fetch,resp;try{resp=await fetchImpl(deps.buildUrl("/estimate-epic"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(payload)})}catch{return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("NETWORK_ERROR","Failed to reach the Bridge API estimate-epic endpoint.")}]}}return{content:[{type:"text",text:await deps.handleResponse(resp)}]}}init_claude_review_workflow_drift();function createBridgeEpicConductorEligibilityClient(deps){return{async classifyChildren(children){let resp=await(deps.fetchImpl??fetch)(deps.buildUrl("/planning/epic-workflow-eligibility"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify({repo_name:deps.repoName,children})});if(!resp.ok)throw new Error(`epic-workflow-eligibility classification request failed: ${resp.status}`);return await resp.json()}}}var UNAVAILABLE2={status:"unavailable"};function isNonBlankString2(value){return typeof value=="string"&&value.trim().length>0}function isStringArray(value){return Array.isArray(value)&&value.every(item=>typeof item=="string")}function optionalTextOrNull(value){return value==null?"":typeof value=="string"?value:null}function buildCombinedText(child){return[child.title,child.scope,child.description,child.requirements].filter(part=>typeof part=="string"&&part.length>0).join(`
8090
8090
 
8091
- `)}function validateChildren(children){if(!Array.isArray(children)||children.length===0)return null;let validated=[];for(let child of children){if(child===null||typeof child!="object"||!isNonBlankString2(child.id)||!isNonBlankString2(child.title))return null;let scope=optionalTextOrNull(child.scope),description=optionalTextOrNull(child.description),requirements=optionalTextOrNull(child.requirements);if(scope===null||description===null||requirements===null)return null;validated.push({id:child.id,title:child.title,combinedText:buildCombinedText({id:child.id,title:child.title,scope,description,requirements})})}return validated}async function assessEpicConductorEligibility(children,deps){let validated=validateChildren(children);if(validated===null)return console.error("assessEpicConductorEligibility: malformed child collection; unavailable."),UNAVAILABLE2;let response;try{response=await deps.client.classifyChildren(validated.map(child=>({child_id:child.id,combined_text:child.combinedText})))}catch(err){return console.error("assessEpicConductorEligibility: Bridge classification call failed:",err),UNAVAILABLE2}if(response===null||typeof response!="object"||!Array.isArray(response.predictions)||!Number.isInteger(response.total_children)||response.total_children<0||!Number.isInteger(response.predicted_workflow_children)||response.predicted_workflow_children<0||!Number.isInteger(response.containment_children)||response.containment_children<0||typeof response.reason!="string")return console.error("assessEpicConductorEligibility: malformed backend response shape; unavailable."),UNAVAILABLE2;if(response.predictions.length!==validated.length)return console.error("assessEpicConductorEligibility: backend prediction count does not match child count; unavailable."),UNAVAILABLE2;let predictionById=new Map;for(let prediction of response.predictions){if(prediction===null||typeof prediction!="object"||!isNonBlankString2(prediction.child_id)||typeof prediction.predicted_workflow_modified!="boolean"||!isStringArray(prediction.matched_paths)||typeof prediction.predicted_containment_hazard!="boolean"||!isStringArray(prediction.matched_hazards))return console.error("assessEpicConductorEligibility: malformed per-child prediction; unavailable."),UNAVAILABLE2;if(prediction.predicted_containment_hazard!==prediction.matched_hazards.length>0)return console.error(`assessEpicConductorEligibility: child ${prediction.child_id} has a containment flag that disagrees with whether matched hazard evidence is present; unavailable.`),UNAVAILABLE2;if(predictionById.has(prediction.child_id))return console.error("assessEpicConductorEligibility: duplicate child_id in backend response; unavailable."),UNAVAILABLE2;predictionById.set(prediction.child_id,prediction)}let affectedChildren=[],reviewSubsetChildren=0,predictedWorkflowChildren=0,containmentChildren=0;for(let child of validated){let prediction=predictionById.get(child.id);if(!prediction)return console.error(`assessEpicConductorEligibility: backend response is missing child ${child.id}; unavailable.`),UNAVAILABLE2;let reviewPathReferenced=child.combinedText.includes(CLAUDE_REVIEW_WORKFLOW_RELPATH);if(reviewPathReferenced&&!prediction.predicted_workflow_modified)return console.error(`assessEpicConductorEligibility: child ${child.id} references the review workflow but the backend did not mark it merge-blocked; refusing contradictory result.`),UNAVAILABLE2;prediction.predicted_workflow_modified&&(predictedWorkflowChildren+=1,reviewPathReferenced&&(reviewSubsetChildren+=1)),prediction.predicted_containment_hazard&&(containmentChildren+=1),!(!prediction.predicted_workflow_modified&&!prediction.predicted_containment_hazard)&&affectedChildren.push({id:child.id,title:child.title,matchedPaths:[...prediction.matched_paths],requiresHandReview:reviewPathReferenced,requiresContainmentReview:prediction.predicted_containment_hazard,matchedHazards:[...prediction.matched_hazards]})}return containmentChildren!==response.containment_children?(console.error("assessEpicConductorEligibility: containment_children count disagrees with the validated per-child containment flags; unavailable."),UNAVAILABLE2):{status:"assessed",totalChildren:validated.length,predictedWorkflowChildren,reason:response.reason,reviewSubsetChildren,containmentChildren,affectedChildren}}init_git_ci_types();init_git_ci_types();init_done_gate();init_merge_identity();init_local_merge();var DRY_RUN_HINT="set auto_merge_enabled=true on the project default via PUT /jira/epic-runs/supervisor-config/defaults/",UNKNOWN_HINT="the request was sent but its outcome was not observed; repeat this call with identical arguments \u2014 the server's action key makes it idempotent",REQUIRED_CHECKS_EMPTY="required_checks_empty",CI_NOT_GREEN_REASON="ci_not_green",REVIEW_NOT_APPROVED_REASON="review_not_approved";function text2(envelope2){return{content:[{type:"text",text:JSON.stringify(envelope2)}]}}function envelope(merged,outcome2,reason,retryHint,evaluatedHeadSha,prNumber,diagnostics={}){let result={merged,outcome:outcome2,reason,retry_hint:retryHint,evaluated_head_sha:evaluatedHeadSha,pr_number:prNumber};return diagnostics.actual_head_sha!==void 0&&(result.actual_head_sha=diagnostics.actual_head_sha),diagnostics.ci_summary!==void 0&&(result.ci_summary=diagnostics.ci_summary),diagnostics.paths!==void 0&&(result.paths=diagnostics.paths),diagnostics.hint!==void 0&&(result.hint=diagnostics.hint),diagnostics.http_status!==void 0&&(result.http_status=diagnostics.http_status),diagnostics.completion!==void 0&&(result.completion=diagnostics.completion),diagnostics.review_waiver!==void 0&&(result.review_waiver=diagnostics.review_waiver),result}var SHA_RE3=/^[0-9a-fA-F]{40}$/;function isPlainObject12(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function validateInputs(prNumber,expectedHeadSha){return typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0?"invalid_pr_number":typeof expectedHeadSha!="string"||!SHA_RE3.test(expectedHeadSha)?"invalid_expected_head_sha":null}async function readJson2(resp){try{let body=await resp.text();return body.trim().length===0?void 0:JSON.parse(body)}catch{return}}function normalizeCheckNames(raw){let out=[],seen=new Set;for(let candidate of raw){let name=normalizeCheckName(candidate);name===null||seen.has(name)||(seen.add(name),out.push(name))}return out}async function resolveRequiredChecks(deps,expectedHeadSha){let fetchImpl=deps.fetchImpl??fetch,defaultsUrl=deps.buildGetUrl("/epic-runs/supervisor-setup/defaults/",{repo_name:deps.repoName}),defaultsHeaders=await deps.getHeaders(),defaultsResp;try{defaultsResp=await fetchImpl(defaultsUrl,{headers:defaultsHeaders})}catch{return null}if(!defaultsResp.ok)return await deps.handleResponse(defaultsResp).catch(()=>""),null;let defaults=await readJson2(defaultsResp);if(!isPlainObject12(defaults))return null;let rawGateConfig=defaults.done_gate_config;if(rawGateConfig!=null){let parsed=parseDoneGateConfig(rawGateConfig);if(!parsed.enabled||!parsed.valid)return{checks:[],configHash:null,reviewCondition:null};let condition=parsed.conditions.find(c=>c.type===REQUIRED_CI_CHECKS_GREEN);if(condition===void 0||condition.type!==REQUIRED_CI_CHECKS_GREEN)return{checks:[],configHash:null,reviewCondition:null};let review=parsed.conditions.find(c=>c.type===REVIEW_STATE),reviewCondition=review!==void 0&&review.type===REVIEW_STATE?review:null;return{checks:[...condition.required_checks],configHash:parsed.config_hash,reviewCondition}}let resolverUrl=deps.buildUrl("/resolve-ci-checks"),resolverHeaders=await deps.getPostHeaders(),resolverResp;try{resolverResp=await fetchImpl(resolverUrl,{method:"POST",headers:resolverHeaders,body:JSON.stringify({repo_name:deps.repoName,commit_ref:expectedHeadSha})})}catch{return null}if(!resolverResp.ok)return await deps.handleResponse(resolverResp).catch(()=>""),null;let resolved=await readJson2(resolverResp);if(!isPlainObject12(resolved))return null;let detail=resolved.detail;if(!isPlainObject12(detail))return{checks:[],configHash:null,reviewCondition:null};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{checks:[],configHash:null,reviewCondition:null};let requiredNames=rawChecks.filter(entry=>isPlainObject12(entry)&&entry.required===!0).map(entry=>entry.name);return{checks:normalizeCheckNames(requiredNames),configHash:null,reviewCondition:null}}var SUPPORTED_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision"]),REVIEW_UNAVAILABLE_REASON="review_unavailable",REVIEW_SOURCE_UNSUPPORTED_REASON="review_source_unsupported",HEAD_SHA_DRIFT_REASON="head_sha_drift";function refuse2(env){return{kind:"refused",envelope:env}}function withReviewWaiver(env,waiver){return waiver===void 0?env:{...env,review_waiver:waiver}}function verdictIsGenuinelyAbsent(condition,snapshot,rawBody){if(condition.source!=="verdict_protocol")return!0;if(snapshot.sticky_verdict!==null)return!1;let detail=isPlainObject12(rawBody)&&isPlainObject12(rawBody.detail)?rawBody.detail:null;if(detail===null)return!1;let raw=detail.sticky_verdict;return raw==null}function effectiveDisposition(condition){return condition.verdictless_disposition??VERDICTLESS_DISPOSITION_PARK}async function precheckReviewCondition(deps,condition,prNumber,expectedHeadSha){if(!SUPPORTED_REVIEW_SOURCES.has(condition.source))return refuse2(envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber));let unavailable=()=>refuse2(envelope(!1,"review_unavailable",REVIEW_UNAVAILABLE_REASON,"needs_human",expectedHeadSha,prNumber)),reviewUrl=deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/reviews/status`)+`?repo_name=${encodeURIComponent(deps.repoName)}`,reviewHeaders=await deps.getHeaders(),reviewResp;try{reviewResp=await(deps.fetchImpl??fetch)(reviewUrl,{headers:reviewHeaders})}catch{return unavailable()}if(!reviewResp.ok)return await deps.handleResponse(reviewResp).catch(()=>""),unavailable();let reviewBody=await readJson2(reviewResp),snapshot=normalizeReviewSnapshot(reviewBody);if(snapshot===null)return unavailable();if(snapshot.head_sha!==expectedHeadSha){let diagnostics={};return typeof snapshot.head_sha=="string"&&snapshot.head_sha.length>0&&(diagnostics.actual_head_sha=snapshot.head_sha),refuse2(envelope(!1,"refused",HEAD_SHA_DRIFT_REASON,"needs_human",expectedHeadSha,prNumber,diagnostics))}let evaluation=evaluateReviewCondition(condition,snapshot);return evaluation.passed?{kind:"proceed"}:effectiveDisposition(condition)===VERDICTLESS_DISPOSITION_FAIL_OPEN&&evaluation.changesRequested===!1&&verdictIsGenuinelyAbsent(condition,snapshot,reviewBody)?{kind:"waived",waiver:MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON}:refuse2(envelope(!1,"review_not_approved",evaluation.reason,"retry_later",expectedHeadSha,prNumber))}function extractDiagnostics(body){let diagnostics={},events=body.ledger_events;if(!Array.isArray(events))return diagnostics;for(let event of events){if(!isPlainObject12(event)||event.status!=="failed")continue;let details=event.details;if(!isPlainObject12(details))continue;let guard=isPlainObject12(details.guard_outcomes)?details.guard_outcomes:{};if(diagnostics.actual_head_sha===void 0&&typeof guard.actual_head_sha=="string"&&(diagnostics.actual_head_sha=guard.actual_head_sha),diagnostics.ci_summary===void 0){let summary=isPlainObject12(guard.ci_summary)?guard.ci_summary:isPlainObject12(details.ci_summary)?details.ci_summary:void 0;summary!==void 0&&(diagnostics.ci_summary=summary)}diagnostics.paths===void 0&&Array.isArray(guard.paths)&&(diagnostics.paths=guard.paths.filter(p=>typeof p=="string"))}return diagnostics}function hasIncompleteRequiredCheck(ciSummary){if(!isPlainObject12(ciSummary))return!1;let checks=ciSummary.checks;return Array.isArray(checks)?checks.some(check=>isPlainObject12(check)&&check.complete!==!0&&check.present!==!1):!1}function retryHintForFailure(reason,ciSummary){return reason===CI_NOT_GREEN_REASON?hasIncompleteRequiredCheck(ciSummary)?"retry_later":"needs_human":reason===REVIEW_NOT_APPROVED_REASON?"retry_later":"needs_human"}function interpretMergeResponse(body,expectedHeadSha,prNumber){let malformed=()=>envelope(!1,"error","malformed_merge_response","needs_human",expectedHeadSha,prNumber);if(!isPlainObject12(body))return malformed();let status=body.status,reason=typeof body.reason=="string"?body.reason:null;if(typeof status!="string")return malformed();if(status==="succeeded")return body.terminal!==!0?malformed():reason==="already_merged"?envelope(!0,"already_merged",reason,null,expectedHeadSha,prNumber):reason==="merged"?envelope(!0,"merged",reason,null,expectedHeadSha,prNumber):malformed();if(status==="dry_run")return envelope(!1,"dry_run",reason,"needs_human",expectedHeadSha,prNumber,{hint:DRY_RUN_HINT});if(status==="pending_approval")return envelope(!1,"pending_approval",reason,"needs_human",expectedHeadSha,prNumber);if(status==="lease_held")return envelope(!1,"lease_held",reason,"retry_later",expectedHeadSha,prNumber);if(status==="failed"){if(reason===null)return malformed();let diagnostics=extractDiagnostics(body);return envelope(!1,"refused",reason,retryHintForFailure(reason,diagnostics.ci_summary),expectedHeadSha,prNumber,diagnostics)}return malformed()}var LOCAL_APPROVAL_STATUS="approved_for_local_execution",DEFAULT_MERGE_EXECUTION="local",LOCAL_GH_HINTS={local_gh_unavailable:"Install the GitHub CLI (`gh`) on the machine running this MCP server \u2014 local merges execute there.",local_gh_unauthenticated:"Run `gh auth login` in the shell that hosts this MCP server \u2014 local merges use its GitHub session."};async function resolveMergeExecutionMode(deps){try{let resp=await(deps.fetchImpl??fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/",{repo_name:deps.repoName}),{headers:await deps.getHeaders()});if(!resp.ok)return DEFAULT_MERGE_EXECUTION;let body=await readJson2(resp);return isPlainObject12(body)&&body.merge_execution==="server"?"server":DEFAULT_MERGE_EXECUTION}catch{return DEFAULT_MERGE_EXECUTION}}function localApprovalMismatch(body,prNumber,expectedHeadSha,actionKey){return body.pr_number!==prNumber||typeof body.expected_head_sha!="string"||body.expected_head_sha.toLowerCase()!==expectedHeadSha.toLowerCase()||body.action_key!==actionKey}function mergeShaFromLedger(response){let events=Array.isArray(response.ledger_events)?response.ledger_events:[];for(let event of events){if(!isPlainObject12(event)||event.type!=="merge.succeeded")continue;let sha=(isPlainObject12(event.details)?event.details:{}).merge_commit_sha;if(typeof sha=="string"&&SHA_RE3.test(sha))return sha}}async function reportLocalCompletion(deps,prNumber,body){try{let resp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge/complete`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)});return resp.ok?await readJson2(resp):(await deps.handleResponse(resp).catch(()=>""),null)}catch{return null}}async function executeApprovedLocalMerge(deps,approval,prNumber,expectedHeadSha,actionKey){if(localApprovalMismatch(approval,prNumber,expectedHeadSha,actionKey))return envelope(!1,"refused","local_approval_mismatch","needs_human",expectedHeadSha,prNumber);let method=resolveLocalMergeMethod(approval.merge_method),request={repo_name:deps.repoName,pr_number:prNumber,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME},action_key:actionKey},local;try{local=await(deps.runLocalMerge??runApprovedLocalMerge)(request,{method},{env:process.env})}catch{return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason:"gh_merge_failed"}),envelope(!1,"refused","gh_merge_failed","needs_human",expectedHeadSha,prNumber)}let localReason=typeof local.reason=="string"?local.reason:null;if(local.status==="succeeded"){let result=localReason==="already_merged"?"already_merged":"merged",mergeSha=mergeShaFromLedger(local);return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result,...mergeSha?{merge_sha:mergeSha}:{}})===null?envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber,{completion:"unreported"}):envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber)}let reason=localReason??"gh_merge_failed";await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason});let hint=LOCAL_GH_HINTS[reason];return envelope(!1,"refused",reason,"needs_human",expectedHeadSha,prNumber,{...hint?{hint}:{}})}async function mergePullRequestHandler(deps,args){let rawPr=args?.pr_number,rawSha=args?.expected_head_sha,echoedSha=typeof rawSha=="string"?rawSha:null,echoedPr=typeof rawPr=="number"?rawPr:null;try{let invalid=validateInputs(rawPr,rawSha);if(invalid!==null)return text2(envelope(!1,"error",invalid,"needs_human",echoedSha,echoedPr));let prNumber=rawPr,expectedHeadSha=rawSha,resolution=await resolveRequiredChecks(deps,expectedHeadSha);if(resolution===null||resolution.checks.length===0)return text2(envelope(!1,"gate_unresolved",REQUIRED_CHECKS_EMPTY,"needs_human",expectedHeadSha,prNumber));let reviewWaiver;if(resolution.reviewCondition!==null){let decision=await precheckReviewCondition(deps,resolution.reviewCondition,prNumber,expectedHeadSha);if(decision.kind==="refused")return text2(decision.envelope);decision.kind==="waived"&&(reviewWaiver=decision.waiver)}let gateIdentity=buildGateIdentity(DEFAULT_GATE_NAME,resolution.configHash),actionKey=makeMergeActionKey(deps.repoName,prNumber,expectedHeadSha,gateIdentity),executionMode=await resolveMergeExecutionMode(deps),mergeBody={repo_name:deps.repoName,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME,config_hash:resolution.configHash,required_checks:resolution.checks},action_key:actionKey,execution:executionMode},mergeResp;try{mergeResp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(mergeBody)})}catch{return text2(envelope(!1,"unknown","merge_request_not_observed","retry_later",expectedHeadSha,prNumber,{hint:UNKNOWN_HINT,review_waiver:reviewWaiver}))}if(!mergeResp.ok)return await deps.handleResponse(mergeResp).catch(()=>""),mergeResp.status===409?text2(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber,{review_waiver:reviewWaiver})):text2(envelope(!1,"error","merge_request_failed","needs_human",expectedHeadSha,prNumber,{http_status:mergeResp.status,review_waiver:reviewWaiver}));let mergeJson=await readJson2(mergeResp);return executionMode==="local"&&isPlainObject12(mergeJson)&&mergeJson.status===LOCAL_APPROVAL_STATUS?text2(withReviewWaiver(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey),reviewWaiver)):text2(withReviewWaiver(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber),reviewWaiver))}catch{return text2(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}init_env_flags();init_bridge_api_urls();init_index_scope_contract();init_tool_surface_gating();import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),enabled=options.enabled!==!1,started=!1,settled=null,settledPromise=null,warned=!1,listServed=!1,lateNotified=!1;function conclude(result){if(!result||typeof result!="object"||result.updateAvailable!==!0)return NOT_STALE;let{currentVersion,latestVersion}=result;return typeof currentVersion!="string"||currentVersion.length===0||typeof latestVersion!="string"||latestVersion.length===0?NOT_STALE:{stale:!0,currentVersion,latestVersion}}function start(){if(!started){if(started=!0,!enabled){settled=NOT_STALE,settledPromise=Promise.resolve(NOT_STALE);return}settledPromise=(async()=>{let status;try{status=conclude(await check())}catch{status=NOT_STALE}if(settled=status,status.stale&&!warned&&(warned=!0,warn(formatUpdateAdvice(status.currentVersion,status.latestVersion)),listServed&&!lateNotified)){lateNotified=!0;try{options.onLateStale?.()}catch{}}return status})()}}return{start,getStatus:()=>settled??NOT_STALE,whenSettled:async()=>(started||start(),await settledPromise??NOT_STALE),markListServed:()=>{listServed=!0}}}function updateAdvisoryFor(status){return!status.stale||!status.currentVersion||!status.latestVersion?null:formatToolSurfaceUpdateAdvisory(status.currentVersion,status.latestVersion)}var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",INDEX_SCOPE,UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),TOOL_SURFACE_POLL_ENABLED=parseDefaultOffEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_POLL_ENABLED),UPDATE_CHECK_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPDATE_CHECK_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),stat:p=>stat12(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),stat:p=>stat12(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),mkdir:(p,options)=>mkdir16(p,options),writeFile:(p,data,options)=>writeFile15(p,data,options),rename:(oldPath,newPath)=>rename7(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink7(p),open:async(p,flags,mode)=>{let handle=await open6(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}function withIndexScopeHeader(headers,options){return INDEX_SCOPE&&!options.scopeAddressed&&(headers[INDEX_SCOPE_HEADER]=INDEX_SCOPE),headers}async function getGetHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}async function getPostHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath4(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path58.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path58.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path58.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path58.join(path58.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return formatSuccessWithTicketBackend(body,resp.headers.get(TICKET_BACKEND_HEADER))}return await resp.text()}let rawText=await resp.text(),errorCode5=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),detail.error===UNSUPPORTED_IN_LOCAL_MODE_ERROR&&resp.status===409?JSON.stringify({...detail,error:UNSUPPORTED_IN_LOCAL_MODE_ERROR,status:resp.status,message}):JSON.stringify({...detail,error:errorCode5,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode5,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}var MAX_INLINE_TEXT_LENGTH=5e4;function safeTimestampForFilename(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeTicketFileSegment(ticketNumber){return path58.basename(ticketNumber.trim()).replace(/[^A-Za-z0-9_-]/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")||"ticket"}function isContainedSaveTarget(dir,filename){let resolvedDir=path58.resolve(dir);return path58.resolve(resolvedDir,filename).startsWith(resolvedDir+path58.sep)}function saveLocallySucceeded(note){return note.includes("Saved to ")}async function truncateAndSaveIfNeeded2(text3,dir,filename){if(text3.length<=MAX_INLINE_TEXT_LENGTH)return text3;if(!isContainedSaveTarget(dir,filename))return text3+`
8091
+ `)}function validateChildren(children){if(!Array.isArray(children)||children.length===0)return null;let validated=[];for(let child of children){if(child===null||typeof child!="object"||!isNonBlankString2(child.id)||!isNonBlankString2(child.title))return null;let scope=optionalTextOrNull(child.scope),description=optionalTextOrNull(child.description),requirements=optionalTextOrNull(child.requirements);if(scope===null||description===null||requirements===null)return null;validated.push({id:child.id,title:child.title,combinedText:buildCombinedText({id:child.id,title:child.title,scope,description,requirements})})}return validated}async function assessEpicConductorEligibility(children,deps){let validated=validateChildren(children);if(validated===null)return console.error("assessEpicConductorEligibility: malformed child collection; unavailable."),UNAVAILABLE2;let response;try{response=await deps.client.classifyChildren(validated.map(child=>({child_id:child.id,combined_text:child.combinedText})))}catch(err){return console.error("assessEpicConductorEligibility: Bridge classification call failed:",err),UNAVAILABLE2}if(response===null||typeof response!="object"||!Array.isArray(response.predictions)||!Number.isInteger(response.total_children)||response.total_children<0||!Number.isInteger(response.predicted_workflow_children)||response.predicted_workflow_children<0||!Number.isInteger(response.containment_children)||response.containment_children<0||typeof response.reason!="string")return console.error("assessEpicConductorEligibility: malformed backend response shape; unavailable."),UNAVAILABLE2;if(response.predictions.length!==validated.length)return console.error("assessEpicConductorEligibility: backend prediction count does not match child count; unavailable."),UNAVAILABLE2;let predictionById=new Map;for(let prediction of response.predictions){if(prediction===null||typeof prediction!="object"||!isNonBlankString2(prediction.child_id)||typeof prediction.predicted_workflow_modified!="boolean"||!isStringArray(prediction.matched_paths)||typeof prediction.predicted_containment_hazard!="boolean"||!isStringArray(prediction.matched_hazards))return console.error("assessEpicConductorEligibility: malformed per-child prediction; unavailable."),UNAVAILABLE2;if(prediction.predicted_containment_hazard!==prediction.matched_hazards.length>0)return console.error(`assessEpicConductorEligibility: child ${prediction.child_id} has a containment flag that disagrees with whether matched hazard evidence is present; unavailable.`),UNAVAILABLE2;if(predictionById.has(prediction.child_id))return console.error("assessEpicConductorEligibility: duplicate child_id in backend response; unavailable."),UNAVAILABLE2;predictionById.set(prediction.child_id,prediction)}let affectedChildren=[],reviewSubsetChildren=0,predictedWorkflowChildren=0,containmentChildren=0;for(let child of validated){let prediction=predictionById.get(child.id);if(!prediction)return console.error(`assessEpicConductorEligibility: backend response is missing child ${child.id}; unavailable.`),UNAVAILABLE2;let reviewPathReferenced=child.combinedText.includes(CLAUDE_REVIEW_WORKFLOW_RELPATH);if(reviewPathReferenced&&!prediction.predicted_workflow_modified)return console.error(`assessEpicConductorEligibility: child ${child.id} references the review workflow but the backend did not mark it merge-blocked; refusing contradictory result.`),UNAVAILABLE2;prediction.predicted_workflow_modified&&(predictedWorkflowChildren+=1,reviewPathReferenced&&(reviewSubsetChildren+=1)),prediction.predicted_containment_hazard&&(containmentChildren+=1),!(!prediction.predicted_workflow_modified&&!prediction.predicted_containment_hazard)&&affectedChildren.push({id:child.id,title:child.title,matchedPaths:[...prediction.matched_paths],requiresHandReview:reviewPathReferenced,requiresContainmentReview:prediction.predicted_containment_hazard,matchedHazards:[...prediction.matched_hazards]})}return containmentChildren!==response.containment_children?(console.error("assessEpicConductorEligibility: containment_children count disagrees with the validated per-child containment flags; unavailable."),UNAVAILABLE2):{status:"assessed",totalChildren:validated.length,predictedWorkflowChildren,reason:response.reason,reviewSubsetChildren,containmentChildren,affectedChildren}}init_git_ci_types();init_git_ci_types();init_done_gate();init_merge_identity();init_local_merge();var DRY_RUN_HINT="set auto_merge_enabled=true on the project default via PUT /jira/epic-runs/supervisor-config/defaults/",UNKNOWN_HINT="the request was sent but its outcome was not observed; repeat this call with identical arguments \u2014 the server's action key makes it idempotent",REQUIRED_CHECKS_EMPTY="required_checks_empty",CI_NOT_GREEN_REASON="ci_not_green",REVIEW_NOT_APPROVED_REASON="review_not_approved";function text2(envelope2){return{content:[{type:"text",text:JSON.stringify(envelope2)}]}}function envelope(merged,outcome2,reason,retryHint,evaluatedHeadSha,prNumber,diagnostics={}){let result={merged,outcome:outcome2,reason,retry_hint:retryHint,evaluated_head_sha:evaluatedHeadSha,pr_number:prNumber};return diagnostics.actual_head_sha!==void 0&&(result.actual_head_sha=diagnostics.actual_head_sha),diagnostics.ci_summary!==void 0&&(result.ci_summary=diagnostics.ci_summary),diagnostics.paths!==void 0&&(result.paths=diagnostics.paths),diagnostics.hint!==void 0&&(result.hint=diagnostics.hint),diagnostics.http_status!==void 0&&(result.http_status=diagnostics.http_status),diagnostics.completion!==void 0&&(result.completion=diagnostics.completion),diagnostics.review_waiver!==void 0&&(result.review_waiver=diagnostics.review_waiver),result}var SHA_RE3=/^[0-9a-fA-F]{40}$/;function isPlainObject12(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function validateInputs(prNumber,expectedHeadSha){return typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0?"invalid_pr_number":typeof expectedHeadSha!="string"||!SHA_RE3.test(expectedHeadSha)?"invalid_expected_head_sha":null}async function readJson2(resp){try{let body=await resp.text();return body.trim().length===0?void 0:JSON.parse(body)}catch{return}}function normalizeCheckNames(raw){let out=[],seen=new Set;for(let candidate of raw){let name=normalizeCheckName(candidate);name===null||seen.has(name)||(seen.add(name),out.push(name))}return out}async function resolveRequiredChecks(deps,expectedHeadSha){let fetchImpl=deps.fetchImpl??fetch,defaultsUrl=deps.buildGetUrl("/epic-runs/supervisor-setup/defaults/",{repo_name:deps.repoName}),defaultsHeaders=await deps.getHeaders(),defaultsResp;try{defaultsResp=await fetchImpl(defaultsUrl,{headers:defaultsHeaders})}catch{return null}if(!defaultsResp.ok)return await deps.handleResponse(defaultsResp).catch(()=>""),null;let defaults=await readJson2(defaultsResp);if(!isPlainObject12(defaults))return null;let rawGateConfig=defaults.done_gate_config;if(rawGateConfig!=null){let parsed=parseDoneGateConfig(rawGateConfig);if(!parsed.enabled||!parsed.valid)return{checks:[],configHash:null,reviewCondition:null};let condition=parsed.conditions.find(c=>c.type===REQUIRED_CI_CHECKS_GREEN);if(condition===void 0||condition.type!==REQUIRED_CI_CHECKS_GREEN)return{checks:[],configHash:null,reviewCondition:null};let review=parsed.conditions.find(c=>c.type===REVIEW_STATE),reviewCondition=review!==void 0&&review.type===REVIEW_STATE?review:null;return{checks:[...condition.required_checks],configHash:parsed.config_hash,reviewCondition}}let resolverUrl=deps.buildUrl("/resolve-ci-checks"),resolverHeaders=await deps.getPostHeaders(),resolverResp;try{resolverResp=await fetchImpl(resolverUrl,{method:"POST",headers:resolverHeaders,body:JSON.stringify({repo_name:deps.repoName,commit_ref:expectedHeadSha})})}catch{return null}if(!resolverResp.ok)return await deps.handleResponse(resolverResp).catch(()=>""),null;let resolved=await readJson2(resolverResp);if(!isPlainObject12(resolved))return null;let detail=resolved.detail;if(!isPlainObject12(detail))return{checks:[],configHash:null,reviewCondition:null};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{checks:[],configHash:null,reviewCondition:null};let requiredNames=rawChecks.filter(entry=>isPlainObject12(entry)&&entry.required===!0).map(entry=>entry.name);return{checks:normalizeCheckNames(requiredNames),configHash:null,reviewCondition:null}}var SUPPORTED_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision"]),REVIEW_UNAVAILABLE_REASON="review_unavailable",REVIEW_SOURCE_UNSUPPORTED_REASON="review_source_unsupported",HEAD_SHA_DRIFT_REASON="head_sha_drift";function refuse2(env){return{kind:"refused",envelope:env}}function withReviewWaiver(env,waiver){return waiver===void 0?env:{...env,review_waiver:waiver}}function verdictIsGenuinelyAbsent(condition,snapshot,rawBody){if(condition.source!=="verdict_protocol")return!0;if(snapshot.sticky_verdict!==null)return!1;let detail=isPlainObject12(rawBody)&&isPlainObject12(rawBody.detail)?rawBody.detail:null;if(detail===null)return!1;let raw=detail.sticky_verdict;return raw==null}function effectiveDisposition(condition){return condition.verdictless_disposition??VERDICTLESS_DISPOSITION_PARK}async function precheckReviewCondition(deps,condition,prNumber,expectedHeadSha){if(!SUPPORTED_REVIEW_SOURCES.has(condition.source))return refuse2(envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber));let unavailable=()=>refuse2(envelope(!1,"review_unavailable",REVIEW_UNAVAILABLE_REASON,"needs_human",expectedHeadSha,prNumber)),reviewUrl=deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/reviews/status`)+`?repo_name=${encodeURIComponent(deps.repoName)}`,reviewHeaders=await deps.getHeaders(),reviewResp;try{reviewResp=await(deps.fetchImpl??fetch)(reviewUrl,{headers:reviewHeaders})}catch{return unavailable()}if(!reviewResp.ok)return await deps.handleResponse(reviewResp).catch(()=>""),unavailable();let reviewBody=await readJson2(reviewResp),snapshot=normalizeReviewSnapshot(reviewBody);if(snapshot===null)return unavailable();if(snapshot.head_sha!==expectedHeadSha){let diagnostics={};return typeof snapshot.head_sha=="string"&&snapshot.head_sha.length>0&&(diagnostics.actual_head_sha=snapshot.head_sha),refuse2(envelope(!1,"refused",HEAD_SHA_DRIFT_REASON,"needs_human",expectedHeadSha,prNumber,diagnostics))}let evaluation=evaluateReviewCondition(condition,snapshot);return evaluation.passed?{kind:"proceed"}:effectiveDisposition(condition)===VERDICTLESS_DISPOSITION_FAIL_OPEN&&evaluation.changesRequested===!1&&verdictIsGenuinelyAbsent(condition,snapshot,reviewBody)?{kind:"waived",waiver:MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON}:refuse2(envelope(!1,"review_not_approved",evaluation.reason,"retry_later",expectedHeadSha,prNumber))}function extractDiagnostics(body){let diagnostics={},events=body.ledger_events;if(!Array.isArray(events))return diagnostics;for(let event of events){if(!isPlainObject12(event)||event.status!=="failed")continue;let details=event.details;if(!isPlainObject12(details))continue;let guard=isPlainObject12(details.guard_outcomes)?details.guard_outcomes:{};if(diagnostics.actual_head_sha===void 0&&typeof guard.actual_head_sha=="string"&&(diagnostics.actual_head_sha=guard.actual_head_sha),diagnostics.ci_summary===void 0){let summary=isPlainObject12(guard.ci_summary)?guard.ci_summary:isPlainObject12(details.ci_summary)?details.ci_summary:void 0;summary!==void 0&&(diagnostics.ci_summary=summary)}diagnostics.paths===void 0&&Array.isArray(guard.paths)&&(diagnostics.paths=guard.paths.filter(p=>typeof p=="string"))}return diagnostics}function hasIncompleteRequiredCheck(ciSummary){if(!isPlainObject12(ciSummary))return!1;let checks=ciSummary.checks;return Array.isArray(checks)?checks.some(check=>isPlainObject12(check)&&check.complete!==!0&&check.present!==!1):!1}function retryHintForFailure(reason,ciSummary){return reason===CI_NOT_GREEN_REASON?hasIncompleteRequiredCheck(ciSummary)?"retry_later":"needs_human":reason===REVIEW_NOT_APPROVED_REASON?"retry_later":"needs_human"}function interpretMergeResponse(body,expectedHeadSha,prNumber){let malformed=()=>envelope(!1,"error","malformed_merge_response","needs_human",expectedHeadSha,prNumber);if(!isPlainObject12(body))return malformed();let status=body.status,reason=typeof body.reason=="string"?body.reason:null;if(typeof status!="string")return malformed();if(status==="succeeded")return body.terminal!==!0?malformed():reason==="already_merged"?envelope(!0,"already_merged",reason,null,expectedHeadSha,prNumber):reason==="merged"?envelope(!0,"merged",reason,null,expectedHeadSha,prNumber):malformed();if(status==="dry_run")return envelope(!1,"dry_run",reason,"needs_human",expectedHeadSha,prNumber,{hint:DRY_RUN_HINT});if(status==="pending_approval")return envelope(!1,"pending_approval",reason,"needs_human",expectedHeadSha,prNumber);if(status==="lease_held")return envelope(!1,"lease_held",reason,"retry_later",expectedHeadSha,prNumber);if(status==="failed"){if(reason===null)return malformed();let diagnostics=extractDiagnostics(body);return envelope(!1,"refused",reason,retryHintForFailure(reason,diagnostics.ci_summary),expectedHeadSha,prNumber,diagnostics)}return malformed()}var LOCAL_APPROVAL_STATUS="approved_for_local_execution",DEFAULT_MERGE_EXECUTION="local",LOCAL_GH_HINTS={local_gh_unavailable:"Install the GitHub CLI (`gh`) on the machine running this MCP server \u2014 local merges execute there.",local_gh_unauthenticated:"Run `gh auth login` in the shell that hosts this MCP server \u2014 local merges use its GitHub session."};async function resolveMergeExecutionMode(deps){try{let resp=await(deps.fetchImpl??fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/",{repo_name:deps.repoName}),{headers:await deps.getHeaders()});if(!resp.ok)return DEFAULT_MERGE_EXECUTION;let body=await readJson2(resp);return isPlainObject12(body)&&body.merge_execution==="server"?"server":DEFAULT_MERGE_EXECUTION}catch{return DEFAULT_MERGE_EXECUTION}}function localApprovalMismatch(body,prNumber,expectedHeadSha,actionKey){return body.pr_number!==prNumber||typeof body.expected_head_sha!="string"||body.expected_head_sha.toLowerCase()!==expectedHeadSha.toLowerCase()||body.action_key!==actionKey}function mergeShaFromLedger(response){let events=Array.isArray(response.ledger_events)?response.ledger_events:[];for(let event of events){if(!isPlainObject12(event)||event.type!=="merge.succeeded")continue;let sha=(isPlainObject12(event.details)?event.details:{}).merge_commit_sha;if(typeof sha=="string"&&SHA_RE3.test(sha))return sha}}async function reportLocalCompletion(deps,prNumber,body){try{let resp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge/complete`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)});return resp.ok?await readJson2(resp):(await deps.handleResponse(resp).catch(()=>""),null)}catch{return null}}async function executeApprovedLocalMerge(deps,approval,prNumber,expectedHeadSha,actionKey){if(localApprovalMismatch(approval,prNumber,expectedHeadSha,actionKey))return envelope(!1,"refused","local_approval_mismatch","needs_human",expectedHeadSha,prNumber);let method=resolveLocalMergeMethod(approval.merge_method),request={repo_name:deps.repoName,pr_number:prNumber,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME},action_key:actionKey},local;try{local=await(deps.runLocalMerge??runApprovedLocalMerge)(request,{method},{env:process.env})}catch{return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason:"gh_merge_failed"}),envelope(!1,"refused","gh_merge_failed","needs_human",expectedHeadSha,prNumber)}let localReason=typeof local.reason=="string"?local.reason:null;if(local.status==="succeeded"){let result=localReason==="already_merged"?"already_merged":"merged",mergeSha=mergeShaFromLedger(local);return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result,...mergeSha?{merge_sha:mergeSha}:{}})===null?envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber,{completion:"unreported"}):envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber)}let reason=localReason??"gh_merge_failed";await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason});let hint=LOCAL_GH_HINTS[reason];return envelope(!1,"refused",reason,"needs_human",expectedHeadSha,prNumber,{...hint?{hint}:{}})}async function mergePullRequestHandler(deps,args){let rawPr=args?.pr_number,rawSha=args?.expected_head_sha,echoedSha=typeof rawSha=="string"?rawSha:null,echoedPr=typeof rawPr=="number"?rawPr:null;try{let invalid=validateInputs(rawPr,rawSha);if(invalid!==null)return text2(envelope(!1,"error",invalid,"needs_human",echoedSha,echoedPr));let prNumber=rawPr,expectedHeadSha=rawSha,resolution=await resolveRequiredChecks(deps,expectedHeadSha);if(resolution===null||resolution.checks.length===0)return text2(envelope(!1,"gate_unresolved",REQUIRED_CHECKS_EMPTY,"needs_human",expectedHeadSha,prNumber));let reviewWaiver;if(resolution.reviewCondition!==null){let decision=await precheckReviewCondition(deps,resolution.reviewCondition,prNumber,expectedHeadSha);if(decision.kind==="refused")return text2(decision.envelope);decision.kind==="waived"&&(reviewWaiver=decision.waiver)}let gateIdentity=buildGateIdentity(DEFAULT_GATE_NAME,resolution.configHash),actionKey=makeMergeActionKey(deps.repoName,prNumber,expectedHeadSha,gateIdentity),executionMode=await resolveMergeExecutionMode(deps),mergeBody={repo_name:deps.repoName,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME,config_hash:resolution.configHash,required_checks:resolution.checks},action_key:actionKey,execution:executionMode},mergeResp;try{mergeResp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(mergeBody)})}catch{return text2(envelope(!1,"unknown","merge_request_not_observed","retry_later",expectedHeadSha,prNumber,{hint:UNKNOWN_HINT,review_waiver:reviewWaiver}))}if(!mergeResp.ok)return await deps.handleResponse(mergeResp).catch(()=>""),mergeResp.status===409?text2(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber,{review_waiver:reviewWaiver})):text2(envelope(!1,"error","merge_request_failed","needs_human",expectedHeadSha,prNumber,{http_status:mergeResp.status,review_waiver:reviewWaiver}));let mergeJson=await readJson2(mergeResp);return executionMode==="local"&&isPlainObject12(mergeJson)&&mergeJson.status===LOCAL_APPROVAL_STATUS?text2(withReviewWaiver(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey),reviewWaiver)):text2(withReviewWaiver(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber),reviewWaiver))}catch{return text2(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}init_env_flags();init_bridge_api_urls();init_index_scope_contract();init_tool_surface_gating();import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),enabled=options.enabled!==!1,started=!1,settled=null,settledPromise=null,warned=!1,listServed=!1,lateNotified=!1;function conclude(result){if(!result||typeof result!="object"||result.updateAvailable!==!0)return NOT_STALE;let{currentVersion,latestVersion}=result;return typeof currentVersion!="string"||currentVersion.length===0||typeof latestVersion!="string"||latestVersion.length===0?NOT_STALE:{stale:!0,currentVersion,latestVersion}}function start(){if(!started){if(started=!0,!enabled){settled=NOT_STALE,settledPromise=Promise.resolve(NOT_STALE);return}settledPromise=(async()=>{let status;try{status=conclude(await check())}catch{status=NOT_STALE}if(settled=status,status.stale&&!warned&&(warned=!0,warn(formatUpdateAdvice(status.currentVersion,status.latestVersion)),listServed&&!lateNotified)){lateNotified=!0;try{options.onLateStale?.()}catch{}}return status})()}}return{start,getStatus:()=>settled??NOT_STALE,whenSettled:async()=>(started||start(),await settledPromise??NOT_STALE),markListServed:()=>{listServed=!0}}}function updateAdvisoryFor(status){return!status.stale||!status.currentVersion||!status.latestVersion?null:formatToolSurfaceUpdateAdvisory(status.currentVersion,status.latestVersion)}var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",INDEX_SCOPE,UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),TOOL_SURFACE_POLL_ENABLED=parseDefaultOffEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_POLL_ENABLED),UPDATE_CHECK_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPDATE_CHECK_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),stat:p=>stat12(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),stat:p=>stat12(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os26.homedir,platform:process.platform,readFile:p=>readFile21(p,"utf-8"),mkdir:(p,options)=>mkdir16(p,options),writeFile:(p,data,options)=>writeFile15(p,data,options),rename:(oldPath,newPath)=>rename7(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink7(p),open:async(p,flags,mode)=>{let handle=await open6(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}function withIndexScopeHeader(headers,options){return INDEX_SCOPE&&!options.scopeAddressed&&(headers[INDEX_SCOPE_HEADER]=INDEX_SCOPE),headers}async function getGetHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}async function getPostHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath5(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path58.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path58.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path58.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path58.join(path58.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return formatSuccessWithTicketBackend(body,resp.headers.get(TICKET_BACKEND_HEADER))}return await resp.text()}let rawText=await resp.text(),errorCode5=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),detail.error===UNSUPPORTED_IN_LOCAL_MODE_ERROR&&resp.status===409?JSON.stringify({...detail,error:UNSUPPORTED_IN_LOCAL_MODE_ERROR,status:resp.status,message}):JSON.stringify({...detail,error:errorCode5,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode5,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}var MAX_INLINE_TEXT_LENGTH=5e4;function safeTimestampForFilename(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeTicketFileSegment(ticketNumber){return path58.basename(ticketNumber.trim()).replace(/[^A-Za-z0-9_-]/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")||"ticket"}function isContainedSaveTarget(dir,filename){let resolvedDir=path58.resolve(dir);return path58.resolve(resolvedDir,filename).startsWith(resolvedDir+path58.sep)}function saveLocallySucceeded(note){return note.includes("Saved to ")}async function truncateAndSaveIfNeeded2(text3,dir,filename){if(text3.length<=MAX_INLINE_TEXT_LENGTH)return text3;if(!isContainedSaveTarget(dir,filename))return text3+`
8092
8092
 
8093
8093
  ---
8094
8094
  Warning: response was NOT truncated because the save target was rejected as outside the docs directory (dir=${dir}, filename=${filename}).`;let note=await saveLocally(dir,filename,text3);return saveLocallySucceeded(note)?"[Response truncated. Full response saved locally.]\n\n```json\n"+text3.slice(0,MAX_INLINE_TEXT_LENGTH)+"\n```"+note:text3+note+`
@@ -8101,7 +8101,7 @@ Note: Both file_path and ${textLabel} were provided. file_path content was used.
8101
8101
 
8102
8102
  ${recovery}`};return JSON.stringify(envelope2)}function formatTriggerConnectionFailure(retryHint,recovery){let parts=["The connection to Bridge API dropped before the submission outcome could be confirmed, so it is unknown whether the server accepted the request."];recovery&&parts.push(`Check whether it was accepted with a GET to ${recovery.recoveryGetUrl} before retrying.`),parts.push(`If it was not accepted, retry ${retryHint}.`);let envelope2={error:"SERVICE_UNAVAILABLE",status:503,message:parts.join(" ")};return recovery&&(envelope2[recovery.handleName]=recovery.handleValue,envelope2.recovery_get=recovery.recoveryGetUrl),JSON.stringify(envelope2)}function parseAutomationProgress(text3){try{let obj=JSON.parse(text3);if(obj&&typeof obj=="object"&&Array.isArray(obj.stages_in_flight)&&typeof obj.completed_stage_count=="number")return obj}catch{}return null}function emitAutomationProgressNotice(label,elapsedSeconds2,progress){try{server.server.sendLoggingMessage({level:"notice",logger:"bridge-api.progress",data:{label,elapsed_seconds:elapsedSeconds2,status:progress.status??"in_progress",last_progress_at:progress.last_progress_at??null,stages_in_flight:progress.stages_in_flight??[],completed_stage_count:progress.completed_stage_count??0,failed_stages:progress.failed_stages??[]}})}catch{}}async function pollForResult(getUrl,timeoutMs,label,recovery,schedule){let startTime=Date.now(),waitMs=schedule?schedule.initialDelayMs:15e3,latestProgress=null,consecutiveFetchFailures=0,progressSuffix=()=>latestProgress?`
8103
8103
 
8104
- Last observed progress: ${JSON.stringify({status:latestProgress.status??"in_progress",last_progress_at:latestProgress.last_progress_at??null,stages_in_flight:latestProgress.stages_in_flight??[],failed_stages:latestProgress.failed_stages??[],completed_stage_count:latestProgress.completed_stage_count??0})}`:"";for(;;){await new Promise(resolve2=>setTimeout(resolve2,waitMs));let elapsed=Math.round((Date.now()-startTime)/1e3);if(Date.now()-startTime>=timeoutMs){let situation=`${label} timed out after ${Math.round(timeoutMs/1e3)} seconds. The task may still be processing on the server.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}console.error(`${label} in progress... (elapsed: ${elapsed}s)`);let resp;try{resp=await fetch(getUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`${label} poll connection failure ${consecutiveFetchFailures}/${3} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=3){let situation=`${label} stopped polling after ${3} consecutive connection failures.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}schedule?waitMs=schedule.intervalMs:waitMs=defaultPollDelayMs(Date.now()-startTime);continue}if(consecutiveFetchFailures=0,resp.status===404||resp.status===202){let body=await resp.text();if(resp.status===202){let progress=parseAutomationProgress(body);if(progress){if(latestProgress=progress,progress.terminal===!0)return{ok:!1,text:`${label} ended without a result: the server reported a terminal "${progress.status??"failed"}" status.${progressSuffix()}`};emitAutomationProgressNotice(label,elapsed,progress)}}}else{let isOk=resp.ok,text3=await handleResponse(resp);return{ok:isOk,text:text3}}schedule?waitMs=schedule.intervalMs:waitMs=defaultPollDelayMs(Date.now()-startTime)}}function normalizeReviewRounds(rounds){if(rounds===1)return 1;if(rounds===2)return 2}var TICKET_ARTIFACTS={plan:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-plan`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/plan`,saveSubdir:"plans",filename:n=>`${n}-plan.md`,requestErrorPrefix:"Failed to request plan generation: ",confirmationText:n=>`Plan generation requested for ${n}. A fresh existing plan is reused instantly; otherwise processing typically takes 5-12 minutes. Use get_plan with ticket_number "${n}" to retrieve the plan once processing completes.`,pollLabel:n=>`Plan generation for ${n}`,pollSchedule:{initialDelayMs:18e4,intervalMs:6e4}},architecture:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-architecture`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/architecture-plan`,saveSubdir:"architecture",filename:n=>`${n}-architecture-plan.md`,requestErrorPrefix:"Failed to request architecture generation: ",confirmationText:n=>`Architecture generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "tdd" to retrieve the architecture plan once processing completes.`,pollLabel:n=>`Architecture generation for ${n}`},fsd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-fsd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/fsd`,saveSubdir:"fsd",filename:n=>`${n}-fsd-plan.md`,requestErrorPrefix:"Failed to request FSD generation: ",confirmationText:n=>`FSD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,pollLabel:n=>`FSD generation for ${n}`},prd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-prd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/prd`,saveSubdir:"prd",filename:n=>`${n}-prd-plan.md`,requestErrorPrefix:"Failed to request PRD generation: ",confirmationText:n=>`PRD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "prd" to retrieve the PRD once processing completes.`,pollLabel:n=>`PRD generation for ${n}`},clarifying_questions:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/clarifying-questions`,saveSubdir:"clarifying-questions",filename:n=>`${n}-clarifying-questions.md`,requestErrorPrefix:"Failed to request clarifying questions: ",confirmationText:n=>`Clarifying questions requested for ${n}. Processing typically takes 1-5 minutes. Use get_clarifying_questions with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Clarifying questions for ${n}`},ticket_critique:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-critique`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-critique`,saveSubdir:"ticket-critiques",filename:n=>`${n}-ticket-quality-critique.md`,requestErrorPrefix:"Failed to request ticket critique: ",confirmationText:n=>`Ticket critique requested for ${n}. Processing typically takes 1-5 minutes. Use get_ticket_critique with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Ticket critique for ${n}`},reimplement_context:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/request-reimplement-context`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/reimplement-context`,saveSubdir:"reimplementations",filename:n=>`${n}-context.md`,requestErrorPrefix:"Failed to request reimplement context: ",confirmationText:n=>`Reimplement context processing requested for ${n}. Processing typically takes 1-2 minutes. Use get_reimplement_context with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Reimplement context for ${n}`},ticket_review:{kind:"review",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-review`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-review`,requestErrorPrefix:"Failed to request ticket review: ",confirmationText:n=>`Combined ticket review requested for ${n}. Processing typically takes 2-6 minutes. Use get_clarifying_questions and get_ticket_critique with ticket_number "${n}" to retrieve the two documents once processing completes.`,pollLabel:n=>`Ticket review for ${n}`,clarifySaveSubdir:"clarifying-questions",critiqueSaveSubdir:"ticket-critiques"}},REVIEW_GENERATION_BASELINES=new ReviewGenerationRegistry;function reviewArtifactUrls(ticketNumber){return{critique:buildGetUrl(TICKET_ARTIFACTS.ticket_critique.getEndpoint(ticketNumber),{repo_name:REPO_NAME}),clarify:buildGetUrl(TICKET_ARTIFACTS.clarifying_questions.getEndpoint(ticketNumber),{repo_name:REPO_NAME})}}async function readDeliveredGeneration(url){try{let resp=await fetch(url,{headers:await getGetHeaders()});return resp.status!==200?null:parseReviewGeneration(await resp.text())}catch{return null}}async function recordReviewGenerationBaseline(ticketNumber){let urls=reviewArtifactUrls(ticketNumber),[critique,clarify]=await Promise.all([readDeliveredGeneration(urls.critique),readDeliveredGeneration(urls.clarify)]);REVIEW_GENERATION_BASELINES.record(REPO_NAME,ticketNumber,{critique,clarify}),invalidateCompletedReviewResult(REPO_NAME,ticketNumber)}function formatBoundedWaitNonReady(result,label,guidance){if(result.state==="pending")return{content:[{type:"text",text:JSON.stringify({state:"pending",reason:result.reason,message:`${label} is still in progress (${result.reason}). This is a normal, resumable result \u2014 call this tool again to open another bounded wait window.`})}]};let status=result.reason==="INVALID_ARGUMENT"?400:502;return{content:[{type:"text",text:JSON.stringify({error:"WAIT_FAILED",status,state:"error",reason:result.reason,message:`${label} could not be waited on (${result.reason}). ${guidance}`})}]}}var BUDGET_MS_SCHEMA=z17.number().int().min(1).max(MAX_BUDGET_MS).optional().describe(`Bounded wait window in ms (1..${MAX_BUDGET_MS}; default ${MAX_BUDGET_MS}). A pending result is normal \u2014 call again to continue waiting.`);function buildTicketArtifactRequestBody(args){let body={repo_name:REPO_NAME},trimmedSecondOpinion=args.second_opinion?.trim();trimmedSecondOpinion&&(body.provider_override=trimmedSecondOpinion);let trimmedProvider=args.provider?.trim();trimmedProvider&&!trimmedSecondOpinion&&(body.provider=trimmedProvider);let normalizedRounds=normalizeReviewRounds(args.rounds);return normalizedRounds!==void 0&&(body.rounds=normalizedRounds),(args.scope==="questions"||args.scope==="critique"||args.scope==="both")&&(body.scope=args.scope),args.force&&(body.force=!0),body}async function getTicketArtifactDocsPath(subdir){switch(subdir){case"plans":return getDocsPath("plans");case"architecture":return getDocsPath("architecture");case"fsd":return getDocsPath("fsd");case"prd":return getDocsPath("prd");case"clarifying-questions":return getDocsPath("clarifying-questions");case"ticket-critiques":return getDocsPath("ticket-critiques");case"reimplementations":return getDocsPath("reimplementations")}}function resolveDesignDocArtifactType(docType){return docType==="fsd"?"fsd":docType==="prd"?"prd":"architecture"}async function requestTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(args.wait_for_result){let retrievalToolName=type==="fsd"?"get_doc":`get_${type}`,result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl,retrievalToolName},config.pollSchedule);if(!result.ok)return{content:[{type:"text",text:result.text}]};let text3=result.text;if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text3);text3+=note}return{content:[{type:"text",text:appendBackendWarningsToText(text3,backendWarnings)}]}}return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]}}async function getTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],resp=await fetch(buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(resp.status===202)return{content:[{type:"text",text:await resp.text()}]};if(type==="reimplement_context"){if(resp.status===404)return{content:[{type:"text",text:JSON.stringify({error:"NOT_FOUND",message:`Reimplement context for ${args.ticket_number} is not yet available. Processing may still be in progress. Try again in a moment, or call request_reimplement_context to trigger processing.`})}]};let text4=await handleResponse(resp);if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text4);return{content:[{type:"text",text:text4+note}]}}return{content:[{type:"text",text:text4}]}}let ok=resp.ok,text3=await handleResponse(resp);if(ok&&args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text3);text3+=note}return{content:[{type:"text",text:text3}]}}async function requestTicketReview(args){let config=TICKET_ARTIFACTS.ticket_review,getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME});await recordReviewGenerationBaseline(args.ticket_number);let resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return REVIEW_GENERATION_BASELINES.clear(REPO_NAME,args.ticket_number),{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){REVIEW_GENERATION_BASELINES.clear(REPO_NAME,args.ticket_number);let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let triggerBody=null;try{triggerBody=await resp.json()}catch{}let backendWarnings=extractBackendWarnings(triggerBody),expectedLegCount=triggerBody!==null&&typeof triggerBody=="object"?triggerBody.expected_leg_count:void 0;if(typeof expectedLegCount=="number"&&REVIEW_GENERATION_BASELINES.attachExpectedLegCount(REPO_NAME,args.ticket_number,expectedLegCount),!args.wait_for_result)return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]};let result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl});if(!result.ok)return{content:[{type:"text",text:result.text}]};let envelope2;try{envelope2=JSON.parse(result.text)}catch(parseErr){return{content:[{type:"text",text:`Failed to parse ticket review response: ${parseErr}
8104
+ Last observed progress: ${JSON.stringify({status:latestProgress.status??"in_progress",last_progress_at:latestProgress.last_progress_at??null,stages_in_flight:latestProgress.stages_in_flight??[],failed_stages:latestProgress.failed_stages??[],completed_stage_count:latestProgress.completed_stage_count??0})}`:"";for(;;){await new Promise(resolve2=>setTimeout(resolve2,waitMs));let elapsed=Math.round((Date.now()-startTime)/1e3);if(Date.now()-startTime>=timeoutMs){let situation=`${label} timed out after ${Math.round(timeoutMs/1e3)} seconds. The task may still be processing on the server.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}console.error(`${label} in progress... (elapsed: ${elapsed}s)`);let resp;try{resp=await fetch(getUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`${label} poll connection failure ${consecutiveFetchFailures}/${3} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=3){let situation=`${label} stopped polling after ${3} consecutive connection failures.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}schedule?waitMs=schedule.intervalMs:waitMs=defaultPollDelayMs(Date.now()-startTime);continue}if(consecutiveFetchFailures=0,resp.status===404||resp.status===202){let body=await resp.text();if(resp.status===202){let progress=parseAutomationProgress(body);if(progress){if(latestProgress=progress,progress.terminal===!0)return{ok:!1,text:`${label} ended without a result: the server reported a terminal "${progress.status??"failed"}" status.${progressSuffix()}`};emitAutomationProgressNotice(label,elapsed,progress)}}}else{let isOk=resp.ok,text3=await handleResponse(resp);return{ok:isOk,text:text3}}schedule?waitMs=schedule.intervalMs:waitMs=defaultPollDelayMs(Date.now()-startTime)}}function normalizeReviewRounds(rounds){if(rounds===1)return 1;if(rounds===2)return 2}var TICKET_ARTIFACTS={plan:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-plan`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/plan`,saveSubdir:"plans",filename:n=>`${n}-plan.md`,requestErrorPrefix:"Failed to request plan generation: ",confirmationText:n=>`Plan generation requested for ${n}. A fresh existing plan is reused instantly; otherwise processing typically takes 5-12 minutes. Use get_plan with ticket_number "${n}" to retrieve the plan once processing completes.`,pollLabel:n=>`Plan generation for ${n}`,pollSchedule:{initialDelayMs:18e4,intervalMs:6e4}},architecture:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-architecture`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/architecture-plan`,saveSubdir:"architecture",filename:n=>`${n}-architecture-plan.md`,requestErrorPrefix:"Failed to request architecture generation: ",confirmationText:n=>`Architecture generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "tdd" to retrieve the architecture plan once processing completes.`,pollLabel:n=>`Architecture generation for ${n}`},fsd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-fsd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/fsd`,saveSubdir:"fsd",filename:n=>`${n}-fsd-plan.md`,requestErrorPrefix:"Failed to request FSD generation: ",confirmationText:n=>`FSD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,pollLabel:n=>`FSD generation for ${n}`},prd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-prd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/prd`,saveSubdir:"prd",filename:n=>`${n}-prd-plan.md`,requestErrorPrefix:"Failed to request PRD generation: ",confirmationText:n=>`PRD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "prd" to retrieve the PRD once processing completes.`,pollLabel:n=>`PRD generation for ${n}`},clarifying_questions:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/clarifying-questions`,saveSubdir:"clarifying-questions",filename:n=>`${n}-clarifying-questions.md`,requestErrorPrefix:"Failed to request clarifying questions: ",confirmationText:n=>`Clarifying questions requested for ${n}. Processing typically takes 1-5 minutes. Use get_clarifying_questions with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Clarifying questions for ${n}`},ticket_critique:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-critique`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-critique`,saveSubdir:"ticket-critiques",filename:n=>`${n}-ticket-quality-critique.md`,requestErrorPrefix:"Failed to request ticket critique: ",confirmationText:n=>`Ticket critique requested for ${n}. Processing typically takes 1-5 minutes. Use get_ticket_critique with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Ticket critique for ${n}`},reimplement_context:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/request-reimplement-context`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/reimplement-context`,saveSubdir:"reimplementations",filename:n=>`${n}-context.md`,requestErrorPrefix:"Failed to request reimplement context: ",confirmationText:n=>`Reimplement context processing requested for ${n}. Processing typically takes 1-2 minutes. Use get_reimplement_context with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Reimplement context for ${n}`},ticket_review:{kind:"review",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-review`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-review`,requestErrorPrefix:"Failed to request ticket review: ",confirmationText:n=>`Combined ticket review requested for ${n}. Processing typically takes 2-6 minutes. Use get_clarifying_questions and get_ticket_critique with ticket_number "${n}" to retrieve the two documents once processing completes.`,pollLabel:n=>`Ticket review for ${n}`,clarifySaveSubdir:"clarifying-questions",critiqueSaveSubdir:"ticket-critiques"}},REVIEW_GENERATION_BASELINES=new ReviewGenerationRegistry;function reviewArtifactUrls(ticketNumber){return{critique:buildGetUrl(TICKET_ARTIFACTS.ticket_critique.getEndpoint(ticketNumber),{repo_name:REPO_NAME}),clarify:buildGetUrl(TICKET_ARTIFACTS.clarifying_questions.getEndpoint(ticketNumber),{repo_name:REPO_NAME})}}async function readDeliveredGeneration(url){try{let resp=await fetch(url,{headers:await getGetHeaders()});return resp.status!==200?null:parseReviewGeneration(await resp.text())}catch{return null}}async function recordReviewGenerationBaseline(ticketNumber){let urls=reviewArtifactUrls(ticketNumber),[critique,clarify]=await Promise.all([readDeliveredGeneration(urls.critique),readDeliveredGeneration(urls.clarify)]);REVIEW_GENERATION_BASELINES.record(REPO_NAME,ticketNumber,{critique,clarify}),invalidateCompletedReviewResult(REPO_NAME,ticketNumber)}function formatBoundedWaitNonReady(result,label,guidance){if(result.state==="pending")return{content:[{type:"text",text:JSON.stringify({state:"pending",reason:result.reason,message:`${label} is still in progress (${result.reason}). This is a normal, resumable result \u2014 call this tool again to open another bounded wait window.`})}]};let status=result.reason==="INVALID_ARGUMENT"?400:502;return{content:[{type:"text",text:JSON.stringify({error:"WAIT_FAILED",status,state:"error",reason:result.reason,message:`${label} could not be waited on (${result.reason}). ${guidance}`})}]}}var DEFAULT_REVIEW_BUDGET_MS=2e4,BUDGET_MS_SCHEMA=z17.number().int().min(1).max(MAX_BUDGET_MS).optional().describe(`Wait window in ms (1..${MAX_BUDGET_MS}). Omit for tool default; repeat on pending.`);function buildTicketArtifactRequestBody(args){let body={repo_name:REPO_NAME},trimmedSecondOpinion=args.second_opinion?.trim();trimmedSecondOpinion&&(body.provider_override=trimmedSecondOpinion);let trimmedProvider=args.provider?.trim();trimmedProvider&&!trimmedSecondOpinion&&(body.provider=trimmedProvider);let normalizedRounds=normalizeReviewRounds(args.rounds);return normalizedRounds!==void 0&&(body.rounds=normalizedRounds),(args.scope==="questions"||args.scope==="critique"||args.scope==="both")&&(body.scope=args.scope),args.force&&(body.force=!0),body}async function getTicketArtifactDocsPath(subdir){switch(subdir){case"plans":return getDocsPath("plans");case"architecture":return getDocsPath("architecture");case"fsd":return getDocsPath("fsd");case"prd":return getDocsPath("prd");case"clarifying-questions":return getDocsPath("clarifying-questions");case"ticket-critiques":return getDocsPath("ticket-critiques");case"reimplementations":return getDocsPath("reimplementations")}}function resolveDesignDocArtifactType(docType){return docType==="fsd"?"fsd":docType==="prd"?"prd":"architecture"}async function requestTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(args.wait_for_result){let retrievalToolName=type==="fsd"?"get_doc":`get_${type}`,result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl,retrievalToolName},config.pollSchedule);if(!result.ok)return{content:[{type:"text",text:result.text}]};let text3=result.text;if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text3);text3+=note}return{content:[{type:"text",text:appendBackendWarningsToText(text3,backendWarnings)}]}}return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]}}async function getTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],resp=await fetch(buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(resp.status===202)return{content:[{type:"text",text:await resp.text()}]};if(type==="reimplement_context"){if(resp.status===404)return{content:[{type:"text",text:JSON.stringify({error:"NOT_FOUND",message:`Reimplement context for ${args.ticket_number} is not yet available. Processing may still be in progress. Try again in a moment, or call request_reimplement_context to trigger processing.`})}]};let text4=await handleResponse(resp);if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text4);return{content:[{type:"text",text:text4+note}]}}return{content:[{type:"text",text:text4}]}}let ok=resp.ok,text3=await handleResponse(resp);if(ok&&args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text3);text3+=note}return{content:[{type:"text",text:text3}]}}async function requestTicketReview(args){let config=TICKET_ARTIFACTS.ticket_review,getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME});await recordReviewGenerationBaseline(args.ticket_number);let resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return REVIEW_GENERATION_BASELINES.clear(REPO_NAME,args.ticket_number),{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){REVIEW_GENERATION_BASELINES.clear(REPO_NAME,args.ticket_number);let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let triggerBody=null;try{triggerBody=await resp.json()}catch{}let backendWarnings=extractBackendWarnings(triggerBody),expectedLegCount=triggerBody!==null&&typeof triggerBody=="object"?triggerBody.expected_leg_count:void 0;if(typeof expectedLegCount=="number"&&REVIEW_GENERATION_BASELINES.attachExpectedLegCount(REPO_NAME,args.ticket_number,expectedLegCount),!args.wait_for_result)return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]};let result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl});if(!result.ok)return{content:[{type:"text",text:result.text}]};let envelope2;try{envelope2=JSON.parse(result.text)}catch(parseErr){return{content:[{type:"text",text:`Failed to parse ticket review response: ${parseErr}
8105
8105
  Raw body: ${result.text}`}]}}let clarifyContent=envelope2.clarify?.status==="success"&&typeof envelope2.clarify.content=="string"?envelope2.clarify.content:"",critiqueContent=envelope2.critique?.status==="success"&&typeof envelope2.critique.content=="string"?envelope2.critique.content:"",submissionContext=REVIEW_GENERATION_BASELINES.getContext(REPO_NAME,args.ticket_number),canonicalPaths=await reviewCanonicalPaths(args.ticket_number),persistenceEnabled=!!args.save_locally,gateResult=await classifyAndPersistReviewEnvelope({repoName:REPO_NAME,ticketNumber:args.ticket_number,envelope:{clarify:{content:clarifyContent},critique:{content:critiqueContent}},baseline:submissionContext?.baseline??null,expectedLegCount:submissionContext?.expectedLegCount,canonicalPaths,persistenceEnabled});if(!gateResult.ok){let landed=gateResult.saved_legs.length>0?gateResult.saved_legs.join(", "):"none",failureText=`**${gateResult.error}** \u2014 the ticket review completed, but its artifacts could not be persisted.
8106
8106
 
8107
8107
  ${gateResult.message}
@@ -8120,7 +8120,7 @@ ${gateResult.message}`;return persistenceEnabled&&gateResult.saved_legs.length>0
8120
8120
  `),1;if(argv[0]==="implement-epic")return runImplementEpicCli(argv.slice(1));if(argv[0]==="drive-epic")return runDriveEpicCli(argv.slice(1));if(argv[0]==="plane")return runPlaneCli(argv.slice(1));if(argv[0]==="emit-conductor-bundle")return process.stderr.write(`'emit-conductor-bundle' was renamed to 'emit-epic-implementer-bundle'. Run 'emit-epic-implementer-bundle ...' instead.
8121
8121
  `),1;if(argv[0]==="emit-epic-implementer-bundle")return runEpicImplementerBundleCli(argv.slice(1));if(argv[0]==="regression-check")return runRegressionCheckCli(argv.slice(1));if(argv[0]==="credentials")return runCredentialsCli(argv.slice(1));if(argv[0]==="conductor")return process.stderr.write(`'conductor' was renamed to 'epic-implementer'. Run 'epic-implementer ...' instead.
8122
8122
  `),1;if(argv[0]==="epic-implementer")return runEpicImplementerCli(argv.slice(1));if(argv[0]==="install-bridge"||argv[0]==="install")try{return await runInstallBridgeWithLatestCli(argv.slice(1))}catch(error){let lines=buildInstallBridgeFailureLines(error,{step:INSTALL_BRIDGE_DISPATCH_STEP_LABEL,debugEnabled:!!process.env.BAPI_INSTALL_DEBUG});for(let line of lines)console.error(line);return 1}return argv[0]==="connect-github"?runConnectGithubCli(argv.slice(1)):argv[0]==="connect-bitbucket"?runConnectBitbucketCli(argv.slice(1)):argv[0]==="upgrade"?runUpgradeCli(argv.slice(1)):(argv[0]==="serve"&&(argv=argv.slice(1)),argv.includes("--version")?(console.log(`${VERSION}
8123
- commit: ${BUILD_COMMIT}`),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null)}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2)).catch(()=>{console.error("Bridge API CLI failed: an unexpected internal error occurred while running the requested subcommand."),process.exit(1)});cliExitCode!==null&&process.exit(cliExitCode);try{INDEX_SCOPE=validateOptionalIndexScope(process.env[INDEX_SCOPE_ENV_VAR])}catch{console.error(INDEX_SCOPE_CONFIGURATION_ERROR),process.exit(1)}var MCP_SERVER_INSTRUCTIONS="Bridge API tools group into six families: tickets, planning/docs, review/council, VCS/CI, pipeline, and SFCC. Most `request_*`/`create_*` operations start background work and return an identifier; poll the paired `get_*` tool for the result. A paired `get_*` returning 404 generally means the artifact is not ready yet, not that polling itself failed. Exception: `get_plan` returns a 202 progress body while generation is active, and 404 means no active successful artifact \u2014 generation was never started or failed. Prefer `wait_for_result=false` and polling over blocking past the MCP client deadline. Council, ticket-review, and deep-research operations are expensive, billable, multi-model runs \u2014 never fire one redundantly.",server=new McpServer({name:MCP_SERVER_NAME,version:"1.0.0"},{instructions:MCP_SERVER_INSTRUCTIONS}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme",MCP_README_URI,{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z17.string(),repo_name:z17.string().optional(),save_locally:z17.boolean().optional().default(!0),wait_for_result:z17.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z17.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z17.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&&registerConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc"),includeWriteTools:ACTIVE_GROUPS.has("sfcc-write")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The same JSON carries docs_dir (BAPI_DOCS_DIR, default docs/tmp), role ('admin'|'member'|null) and customer_type ('b2b'|'b2c'|null) \u2014 read them here, not from a separate tool. Both identity fields are best-effort: null means 'not determined' (e.g. a legacy shared key) and never fails the ping. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),payload={...body,mcp_commit:BUILD_COMMIT,docs_dir:await getDocsDir(),role:body.role??null,customer_type:body.customer_type??null},content=[{type:"text",text:JSON.stringify(payload,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=formatUpdateAdvice(body.mcp_version??"",body.latest_mcp_version??"");content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider synchronously. This does NOT create or retrieve a Bridge artifact \u2014 for multi-provider depth, use request_council instead.",inputSchema:{prompt:z17.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z17.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z17.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("clarify_task",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Clarify a task without Jira or repository setup. Returns questions immediately; connecting a repo adds code-aware context.",inputSchema:{task_description:z17.string().trim().min(1).describe("The task to generate clarifying questions for.")}},async({task_description})=>{let resp=await fetch(buildApiUrl("/clarify-task"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,task_description})});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z17.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z17.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z17.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z17.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir16(imagesDir,{recursive:!0}),await writeFile15(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z17.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z17.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z17.object({width:z17.number().int().positive(),height:z17.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z17.array(z17.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z17.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile21(p),stat:p=>stat12(p),mkdir:(p,opts)=>mkdir16(p,opts),writeFile:(p,data)=>writeFile15(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search and list tickets from the configured ticket backend, most recently updated first. Filters are backend-dependent: one the backend cannot apply returns an explicit error, not an empty list. Use get_ticket for full details.",inputSchema:{query:z17.string().optional().describe(`Free-text search. Backend-dependent: Jira searches summary/description/comments via JQL text ~ '...'; local searches summary and description. Example: "payment timeout"`),status:z17.string().optional().describe("Filter by status name. Jira accepts the project's workflow statuses; local has four: 'To Do', 'In Progress', 'In Review', 'Done'"),labels:z17.string().optional().describe('Comma-separated labels. Requires Jira backend: local returns a terminal 409 UNSUPPORTED_IN_LOCAL_MODE rather than ignoring the filter \u2014 remove the filter, or use Jira mode. Jira filters via JQL labels in (...); no spaces allowed. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z17.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z17.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z17.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text:text3}]}});async function fetchTicketModelTier(ticket_number){let fallback={difficulty:null,tier:null,source:"fallback"};try{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return fallback;let body=await resp.json();return body===null||typeof body!="object"?fallback:{difficulty:typeof body.difficulty=="number"?body.difficulty:null,tier:body.tier==="cheap"||body.tier==="basic"||body.tier==="premium"?body.tier:null,source:typeof body.source=="string"&&body.source.length>0?body.source:"fallback"}}catch{return fallback}}registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for one ticket by key from the configured ticket backend: summary, status, type, assignee, reporter, description, timestamps. Use get_tickets to search/list; get_comments for comments (requires Jira backend). Set include_model_tier to add { difficulty, tier, source }.",inputSchema:{ticket_number:commonFields.ticket_number,include_model_tier:z17.boolean().optional().describe(`Opt in to an EXTRA backend lookup that merges the ticket's coarse implementation-model tier ({ difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source }) into the response. Omitted or false (the default) issues exactly one request and returns the plain ticket. A null tier with source "fallback" means the tier could not be resolved and you should use your default model; a failed lookup degrades to that rather than failing the call.`)}},async({ticket_number,include_model_tier})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){if(include_model_tier===!0){let tierFields=await fetchTicketModelTier(ticket_number);try{let ticketBody=JSON.parse(text3);text3=JSON.stringify({...ticketBody,...tierFields},null,2)}catch{}}let filename=`${safeTicketFileSegment(ticket_number)}.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Retrieve all comments on a Jira ticket, oldest-first, as {id, author, body, created, updated}; bodies are Markdown converted from wiki markup. Use add_comment to post one.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("comments"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a ticket in the configured ticket backend, immediately \u2014 confirm with the user first. Requires description or file_path (file_path wins). Returns {ticket_key, url}: the url is supplied by the backend and used as returned (https:// in Jira, local://tickets/... in local) \u2014 never construct one. priority/parent_key persist in BOTH backends (Jira: parent_key only for Epic children; local: trimmed opaque text, no allowlist). labels still requires Jira: local REJECTS non-empty with 409 UNSUPPORTED_IN_LOCAL_MODE, creating NO ticket \u2014 never silently dropped.",inputSchema:{summary:z17.string().describe("Ticket title \u2014 keep under 100 characters"),description:z17.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z17.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z17.string().optional().describe("Jira: 'Highest'|'High'|'Medium'|'Low'|'Lowest' (omit for default). Local: free-form text, trimmed; no allowlist/default. Omit to leave unset."),labels:z17.array(z17.string()).optional().describe("Requires Jira backend. Jira labels to apply (e.g. ['frontend', 'tech-debt']); local rejects a non-empty list"),assignee:z17.string().optional().describe("Assignee identifier (a Jira username or account ID in Jira mode). Omit to leave unassigned"),parent_key:z17.string().optional().describe("Jira: Epic key \u2014 pass ONLY for a child Epic. Local: opaque trimmed text, no parent/issue-type validation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE-only: reads a plan, never triggers generation. Active generation returns a 202 progress body; 404 means no active successful artifact \u2014 never started or failed. request_plan_generation force is for deliberate regeneration, not absence recovery. Returns plan markdown verbatim; see also get_clarifying_questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("plan",args));registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_ticket_review` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Parse/index the repository for Bridge API's AI agents, or read the current job's status. action "start" (default) only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. Run it after major codebase changes so plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. There is no completion callback \u2014 poll action "status" to observe when it reaches terminal success or terminal failure. action "status" never runs a parse: it returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment. For large repositories a parse may take several minutes. Confirm with the user before triggering one.`,inputSchema:{action:z17.enum(["start","status"]).default("start").describe(`"start" (default) enqueues a parse; "status" reads the current job's status without starting one.`),directory_path:z17.string().optional().describe(`action "start" only. Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository`)}},async({action,directory_path})=>{if(action==="status"){let url=buildGetUrl("/parse-status",{repo_name:INDEX_SCOPE??REPO_NAME}),resp2=await fetch(url,{headers:await getGetHeaders({scopeAddressed:!0})});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let payload={repo_name:INDEX_SCOPE??REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders({scopeAddressed:!0}),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var regenerateDirectoryMapTool=registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Post a markdown comment on a Jira ticket; it appears immediately. For long comments (over ~2000 characters) set attach_as_file to true, attaching the comment as a .md file instead of posting inline.
8123
+ commit: ${BUILD_COMMIT}`),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null)}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2)).catch(()=>{console.error("Bridge API CLI failed: an unexpected internal error occurred while running the requested subcommand."),process.exit(1)});cliExitCode!==null&&process.exit(cliExitCode);try{INDEX_SCOPE=validateOptionalIndexScope(process.env[INDEX_SCOPE_ENV_VAR])}catch{console.error(INDEX_SCOPE_CONFIGURATION_ERROR),process.exit(1)}var MCP_SERVER_INSTRUCTIONS="Bridge API tools group into six families: tickets, planning/docs, review/council, VCS/CI, pipeline, and SFCC. Most `request_*`/`create_*` operations start background work and return an identifier; poll the paired `get_*` tool for the result. A paired `get_*` returning 404 generally means the artifact is not ready yet, not that polling itself failed. Exception: `get_plan` returns a 202 progress body while generation is active, and 404 means no active successful artifact \u2014 generation was never started or failed. Prefer `wait_for_result=false` and polling over blocking past the MCP client deadline. Council, ticket-review, and deep-research operations are expensive, billable, multi-model runs \u2014 never fire one redundantly.",server=new McpServer({name:MCP_SERVER_NAME,version:"1.0.0"},{instructions:MCP_SERVER_INSTRUCTIONS}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme",MCP_README_URI,{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z17.string(),repo_name:z17.string().optional(),save_locally:z17.boolean().optional().default(!0),wait_for_result:z17.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z17.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z17.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&&registerConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc"),includeWriteTools:ACTIVE_GROUPS.has("sfcc-write")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The same JSON carries docs_dir (BAPI_DOCS_DIR, default docs/tmp), role ('admin'|'member'|null) and customer_type ('b2b'|'b2c'|null) \u2014 read them here, not from a separate tool. Both identity fields are best-effort: null means 'not determined' (e.g. a legacy shared key) and never fails the ping. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),payload={...body,mcp_commit:BUILD_COMMIT,docs_dir:await getDocsDir(),role:body.role??null,customer_type:body.customer_type??null},content=[{type:"text",text:JSON.stringify(payload,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=formatUpdateAdvice(body.mcp_version??"",body.latest_mcp_version??"");content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider synchronously. This does NOT create or retrieve a Bridge artifact \u2014 for multi-provider depth, use request_council instead.",inputSchema:{prompt:z17.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z17.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z17.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("clarify_task",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Clarify a task without Jira or repository setup. Returns questions immediately; connecting a repo adds code-aware context.",inputSchema:{task_description:z17.string().trim().min(1).describe("The task to generate clarifying questions for.")}},async({task_description})=>{let resp=await fetch(buildApiUrl("/clarify-task"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,task_description})});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z17.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z17.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z17.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z17.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir16(imagesDir,{recursive:!0}),await writeFile15(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z17.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z17.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z17.object({width:z17.number().int().positive(),height:z17.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z17.array(z17.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z17.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile21(p),stat:p=>stat12(p),mkdir:(p,opts)=>mkdir16(p,opts),writeFile:(p,data)=>writeFile15(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search and list tickets from the configured ticket backend, most recently updated first. Filters are backend-dependent: one the backend cannot apply returns an explicit error, not an empty list. Use get_ticket for full details.",inputSchema:{query:z17.string().optional().describe(`Free-text search. Backend-dependent: Jira searches summary/description/comments via JQL text ~ '...'; local searches summary and description. Example: "payment timeout"`),status:z17.string().optional().describe("Filter by status name. Jira accepts the project's workflow statuses; local has four: 'To Do', 'In Progress', 'In Review', 'Done'"),labels:z17.string().optional().describe('Comma-separated labels. Requires Jira backend: local returns a terminal 409 UNSUPPORTED_IN_LOCAL_MODE rather than ignoring the filter \u2014 remove the filter, or use Jira mode. Jira filters via JQL labels in (...); no spaces allowed. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z17.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z17.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z17.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text:text3}]}});async function fetchTicketModelTier(ticket_number){let fallback={difficulty:null,tier:null,source:"fallback"};try{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return fallback;let body=await resp.json();return body===null||typeof body!="object"?fallback:{difficulty:typeof body.difficulty=="number"?body.difficulty:null,tier:body.tier==="cheap"||body.tier==="basic"||body.tier==="premium"?body.tier:null,source:typeof body.source=="string"&&body.source.length>0?body.source:"fallback"}}catch{return fallback}}registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for one ticket by key from the configured ticket backend: summary, status, type, assignee, reporter, description, timestamps. Use get_tickets to search/list; get_comments for comments (requires Jira backend). Set include_model_tier to add { difficulty, tier, source }.",inputSchema:{ticket_number:commonFields.ticket_number,include_model_tier:z17.boolean().optional().describe(`Opt in to an EXTRA backend lookup that merges the ticket's coarse implementation-model tier ({ difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source }) into the response. Omitted or false (the default) issues exactly one request and returns the plain ticket. A null tier with source "fallback" means the tier could not be resolved and you should use your default model; a failed lookup degrades to that rather than failing the call.`)}},async({ticket_number,include_model_tier})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){if(include_model_tier===!0){let tierFields=await fetchTicketModelTier(ticket_number);try{let ticketBody=JSON.parse(text3);text3=JSON.stringify({...ticketBody,...tierFields},null,2)}catch{}}let filename=`${safeTicketFileSegment(ticket_number)}.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Retrieve all comments on a Jira ticket, oldest-first, as {id, author, body, created, updated}; bodies are Markdown converted from wiki markup. Use add_comment to post one.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("comments"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a ticket in the configured ticket backend, immediately \u2014 confirm with the user first. Requires description or file_path (file_path wins). Returns {ticket_key, url}: the url is supplied by the backend and used as returned (https:// in Jira, local://tickets/... in local) \u2014 never construct one. priority/parent_key persist in BOTH backends (Jira: parent_key only for Epic children; local: trimmed opaque text, no allowlist). labels still requires Jira: local REJECTS non-empty with 409 UNSUPPORTED_IN_LOCAL_MODE, creating NO ticket \u2014 never silently dropped.",inputSchema:{summary:z17.string().describe("Ticket title \u2014 keep under 100 characters"),description:z17.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z17.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z17.string().optional().describe("Jira: 'Highest'|'High'|'Medium'|'Low'|'Lowest' (omit for default). Local: free-form text, trimmed; no allowlist/default. Omit to leave unset."),labels:z17.array(z17.string()).optional().describe("Requires Jira backend. Jira labels to apply (e.g. ['frontend', 'tech-debt']); local rejects a non-empty list"),assignee:z17.string().optional().describe("Assignee identifier (a Jira username or account ID in Jira mode). Omit to leave unassigned"),parent_key:z17.string().optional().describe("Jira: Epic key \u2014 pass ONLY for a child Epic. Local: opaque trimmed text, no parent/issue-type validation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read a plan; never triggers generation. Without budget_ms, return markdown or active 202 progress. With budget_ms, wait that many ms and return markdown or {state:'pending'|'error'}; repeat pending. 404 means no active successful artifact. request_plan_generation force is for deliberate regeneration, not absence recovery.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally,budget_ms:BUDGET_MS_SCHEMA}},async({ticket_number,save_locally,budget_ms})=>{if(budget_ms===void 0)return getTicketArtifact("plan",{ticket_number,save_locally});let url=buildGetUrl(TICKET_ARTIFACTS.plan.getEndpoint(ticket_number),{repo_name:REPO_NAME}),result=await boundedWait({budgetMs:budget_ms,probe:async signal=>{let resp=await fetch(url,{headers:await getGetHeaders(),signal});if(resp.status===202){let progress=parseAutomationProgress(await resp.text());return progress&&(progress.status==="failed"||progress.status==="expired")?{kind:"terminal"}:{kind:"observation",ready:!1}}if(!resp.ok)return{kind:"terminal"};let plan2=await resp.text();return plan2.trim()?{kind:"observation",ready:!0,value:plan2}:{kind:"malformed"}}});if(result.state!=="ready")return formatBoundedWaitNonReady(result,`Plan generation for ${ticket_number}`,"Do not re-submit automatically. Inspect the failure before retrying generation.");let plan=result.result;return save_locally&&(plan+=await saveLocally(await getTicketArtifactDocsPath(TICKET_ARTIFACTS.plan.saveSubdir),TICKET_ARTIFACTS.plan.filename(ticket_number),plan)),{content:[{type:"text",text:plan}]}});registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_ticket_review` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Parse/index the repository for Bridge API's AI agents, or read the current job's status. action "start" (default) only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. Run it after major codebase changes so plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. There is no completion callback \u2014 poll action "status" to observe when it reaches terminal success or terminal failure. action "status" never runs a parse: it returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment. For large repositories a parse may take several minutes. Confirm with the user before triggering one.`,inputSchema:{action:z17.enum(["start","status"]).default("start").describe(`"start" (default) enqueues a parse; "status" reads the current job's status without starting one.`),directory_path:z17.string().optional().describe(`action "start" only. Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository`)}},async({action,directory_path})=>{if(action==="status"){let url=buildGetUrl("/parse-status",{repo_name:INDEX_SCOPE??REPO_NAME}),resp2=await fetch(url,{headers:await getGetHeaders({scopeAddressed:!0})});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let payload={repo_name:INDEX_SCOPE??REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders({scopeAddressed:!0}),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var regenerateDirectoryMapTool=registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Post a markdown comment on a Jira ticket; it appears immediately. For long comments (over ~2000 characters) set attach_as_file to true, attaching the comment as a .md file instead of posting inline.
8124
8124
 
8125
8125
  Tip: To generate plans, clarifying questions, or ticket critiques, use the dedicated request_plan_generation or request_ticket_review tools.`,inputSchema:{ticket_number:commonFields.ticket_number,comment:z17.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the comment. If both file_path and comment are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),attach_as_file:z17.boolean().optional().default(!1).describe("Set to true to attach the comment as a .md file instead of posting inline. Recommended for comments over 2000 characters"),file_name:z17.string().optional().describe("Custom filename for the attached .md file (only used when attach_as_file is true). Defaults to {ticket_number}-comment.md if not provided. Example: 'PROJ-123-clarifying-questions.md'")}},async({ticket_number,comment,file_path,attach_as_file,file_name})=>{let resolved=await resolveTextOrFile(comment,file_path,"comment");if(!resolved.ok)return resolved.errorResponse;let payload={repo_name:REPO_NAME,comment:resolved.text,attach_as_file};file_name&&(payload.file_name=file_name);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/comment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});registerTool("update_ticket_description",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Update an existing ticket's description in the configured ticket backend. A direct, synchronous overwrite of the existing description. Supply markdown: Jira mode converts it to wiki markup, local stores it as written. This does NOT create a new ticket. Use create_ticket for that. Returns a success message with the ticket number, or an error if the update fails.",inputSchema:{ticket_number:commonFields.ticket_number,description:z17.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the new description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),force:z17.boolean().optional().describe("Apply the update even when the server would otherwise hold it for review because it changes most of the description. Set this ONLY on explicit human approval \u2014 never automatically \u2014 and only after seeing the hold response that reported the change percentage. Every forced apply is audit-logged server-side.")}},async({ticket_number,description,file_path,force})=>{let resolved=await resolveTextOrFile(description,file_path,"description");if(!resolved.ok)return resolved.errorResponse;let body={repo_name:REPO_NAME,description:resolved.text};force!==void 0&&(body.force=force);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/description`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(body)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});var ATTACHMENT_OPERATION_FIELDS={upload:["file_path","content","file_name","link_type","replace_existing"],download:["attachment_id","filename","file_path"],list:["include_ai_generated"],delete:["file_name"]},ATTACHMENT_OPTIONAL_FIELDS=["file_path","content","file_name","link_type","replace_existing","attachment_id","filename","include_ai_generated"];function validateAttachmentOperationFields(args){let operation=String(args.operation),allowed=new Set(ATTACHMENT_OPERATION_FIELDS[operation]??[]),offenders=ATTACHMENT_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));return offenders.length===0?null:{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`attachment operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none beyond ticket_number)"}.`})}]}}registerTool("attachment",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Flat object: operation plus only that operation's fields. upload takes text or image/png|jpeg|webp|gif (10 MB max). download saves binary/image attachments as raw bytes to file_path; UTF-8 text is inline. delete removes by exact filename (no-op if absent).",inputSchema:{operation:z17.enum(["upload","download","list","delete"]).describe("Which attachment operation to perform."),ticket_number:commonFields.ticket_number,file_path:z17.string().optional().describe("upload: path to a local file to upload. Binary uploads are restricted to the allowlisted image types image/png, image/jpeg, image/webp, image/gif, up to `10 MB`; other binaries such as PDFs and ZIPs are rejected as unsupported attachment types. Text uploads are up to `1 MB`. If both file_path and content are provided, file_path takes precedence. download: override the default save location (must stay within the project root/worktree). Pass a file_path when you need to open an image/design comp locally. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}."),content:z17.string().max(1048576).optional().describe("upload only: inline text content to upload (max `1 MB`). Optional if file_path is provided."),file_name:z17.string().optional().describe("upload: filename for the attachment in Jira. Defaults to the basename of file_path if provided, or {ticket_number}-attachment.md otherwise. delete (required): exact filename of the attachment to delete. Deleting an absent filename succeeds as a no-op (reported result has deleted=false) rather than erroring."),link_type:z17.string().optional().describe("upload only: when provided, also syncs the content to Bridge API's tickets_links table. Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. Cannot be used with binary file uploads."),replace_existing:z17.boolean().optional().describe("upload only: when true (the default when omitted), deletes any existing attachment with the same filename before uploading."),attachment_id:z17.string().optional().describe("download only: Jira attachment ID. Mutually exclusive with filename. For design/UI tickets, pass the attachment_id from the plan's DESIGN COMP CANDIDATES section to fetch the design comp."),filename:z17.string().optional().describe("download only: attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),include_ai_generated:z17.boolean().optional().describe("list only: include AI-generated attachments in the list (default: false)")}},async args=>{let combinationError=validateAttachmentOperationFields(args);if(combinationError)return combinationError;switch(args.operation){case"upload":{let{ticket_number,file_path,content,file_name,link_type}=args,replace_existing=args.replace_existing??!0,derivedFileName=file_name||(file_path?path58.basename(file_path):`${ticket_number}-attachment.md`),resolved=await resolveUploadAttachment(content,file_path,"content",derivedFileName);if(!resolved.ok)return resolved.errorResponse;if(link_type&&resolved.encoding==="base64")return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`attachment operation "upload" cannot combine link_type with a binary upload (${derivedFileName}). Omit link_type for image uploads.`})}]};let payload={repo_name:REPO_NAME,content:resolved.text,file_name:derivedFileName,replace_existing};resolved.encoding&&(payload.encoding=resolved.encoding),resolved.contentType&&(payload.content_type=resolved.contentType),link_type&&(payload.link_type=link_type);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}}case"download":{let{ticket_number,attachment_id,filename,file_path}=args;if(!attachment_id&&!filename)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:"Provide either attachment_id or filename (at least one is required)."})}]};if(attachment_id&&filename)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'attachment operation "download" accepts attachment_id or filename, not both. Supply exactly one.'})}]};let params={repo_name:REPO_NAME};attachment_id&&(params.attachment_id=attachment_id),filename&&(params.filename=filename);let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments/download`,params),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let body=await resp.json(),serverFilename=body.filename,content=body.content,isText2=body.is_text,mimeType=body.mime_type,size=body.size,safeFileName=path58.basename(serverFilename),safeTicket=path58.basename(ticket_number),savePath=file_path||path58.join(await getDocsDir(),"attachments",safeTicket,safeFileName),resolvedSave=path58.resolve(savePath),resolvedRoot=path58.resolve(await getProjectRoot());if(!resolvedSave.startsWith(resolvedRoot+path58.sep)&&resolvedSave!==resolvedRoot)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:`Save path "${savePath}" is outside the project root. Refusing to write.`})}]};await mkdir16(path58.dirname(resolvedSave),{recursive:!0}),isText2?await writeFile15(resolvedSave,content,"utf-8"):await writeFile15(resolvedSave,Buffer.from(content,"base64"));let resultText=`File saved to: ${resolvedSave}
8126
8126
  Filename: ${safeFileName}
@@ -8132,9 +8132,9 @@ ${content.slice(0,MAX_INLINE_TEXT_LENGTH)}
8132
8132
 
8133
8133
  [Content truncated. Full content saved to ${resolvedSave}]`:resultText+=`
8134
8134
 
8135
- ${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text:text3}]}}case"delete":{let{ticket_number,file_name}=args;if(typeof file_name!="string"||file_name.length===0)return{content:[{type:"text",text:JSON.stringify({deleted:!1,message:'No file_name supplied for attachment operation "delete" \u2014 nothing to delete.'})}]};let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a plan. A non-forced request against a fresh existing plan is reused \u2014 202, reused: true, no new work. Otherwise starts an async job (~5-12 min); retrieve via get_plan. 202 if accepted, 404 if the ticket does not exist, 403 if unauthorized. force regenerates deliberately \u2014 never for polling or timeout recovery.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,force:z17.boolean().optional().describe("Deliberately regenerate the plan even when a fresh one is stored. Omit for normal recovery-safe requests. Default: false.")}},async args=>requestTicketArtifact("plan",args));var mergePullRequestDeps={repoName:REPO_NAME,buildApiUrl,buildUrl,buildGetUrl,getHeaders:getGetHeaders,getPostHeaders,handleResponse};ACTIVE_GROUPS.has("estimation")&&registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Estimate an Epic or ticket-key group (exactly one of epic_key/ticket_keys). allow_partial permits a partial result on child failure (default: fail-closed). Repo config may post the breakdown as a comment and set story-points if empty; not overridable via MCP.",inputSchema:{epic_key:z17.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z17.array(z17.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z17.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("assess_epic_conductor_eligibility",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Planning-only: predicts which frozen epic children likely modify workflow files (human merge required) and, of those, which touch claude-review.yml (hand review, no automated review). A ticket-text lower bound, not a fact. Fails open to 'unavailable' on bad input; never spawns or dispatches work.",inputSchema:{children:z17.array(z17.object({id:z17.string().trim().min(1),title:z17.string().trim().min(1),scope:z17.string().optional(),description:z17.string().optional(),requirements:z17.string().optional()})).min(1).describe("Complete ordered child set \u2014 never a partial subset.")}},async args=>{let result=await assessEpicConductorEligibility(args.children,{client:createBridgeEpicConductorEligibilityClient({repoName:REPO_NAME,buildUrl,getPostHeaders})});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start async generation of a design doc (tdd/architecture, fsd, or prd) for a ticket (~2-4 min). architecture and tdd are the same artifact. Retrieve via get_doc \u2014 a 404 there means not ready yet, not a failure \u2014 or set wait_for_result to block and return it directly.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'architecture' (an alias for 'tdd' \u2014 the same artifact, not a fourth document), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a ticket, routed by doc_type. Use doc_type 'tdd' (or its alias 'architecture' \u2014 the same artifact) for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document) or its alias 'architecture' (the same artifact), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_review` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));async function reviewCanonicalPaths(ticketNumber){let config=TICKET_ARTIFACTS.ticket_review,[clarifyDir,critiqueDir]=await Promise.all([getTicketArtifactDocsPath(config.clarifySaveSubdir),getTicketArtifactDocsPath(config.critiqueSaveSubdir)]);return{clarify:path58.join(clarifyDir,TICKET_ARTIFACTS.clarifying_questions.filename(ticketNumber)),critique:path58.join(critiqueDir,TICKET_ARTIFACTS.ticket_critique.filename(ticketNumber))}}registerTool("wait_for_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Wait, in one bounded window, for a submitted ticket review to finish. Starts no generation, but a ready result WRITES accepted canonical artifact(s) and may REMOVE a withheld leg's stale local file. Returns {state:'ready'|'pending'|'error'}; pending is normal. See saved_legs/withheld_legs/absent_legs/freshness_basis for the authoritative set.",inputSchema:{ticket_number:commonFields.ticket_number,budget_ms:BUDGET_MS_SCHEMA}},async({ticket_number,budget_ms})=>{let combinedUrl=buildGetUrl(TICKET_ARTIFACTS.ticket_review.getEndpoint(ticket_number),{repo_name:REPO_NAME}),baseline=REVIEW_GENERATION_BASELINES.get(REPO_NAME,ticket_number),baselineAvailable=baseline!==null,classifyLeg=(leg,legBaseline)=>{let content=typeof leg?.content=="string"?leg.content:"";if(content.trim().length===0)return{ready:!1,generation:null,delivered:!1};let generation=parseReviewGeneration(content);return baselineAvailable?generation===null?{ready:!1,generation:null,delivered:!0}:{ready:generationAdvanced(legBaseline,generation),generation,delivered:!0}:{ready:!0,generation,delivered:!0}},result=await boundedWait({budgetMs:budget_ms??MAX_BUDGET_MS,probe:async signal=>{let notYet=()=>({kind:"observation",ready:!0,value:{critique:{ready:!1,generation:null,delivered:!1},clarify:{ready:!1,generation:null,delivered:!1},generation_baseline_available:baselineAvailable,envelope:{}}}),resp=await fetch(combinedUrl,{headers:await getGetHeaders(),signal});if(resp.status===404)return notYet();if(resp.status===202){let progress=parseAutomationProgress(await resp.text());return progress&&(progress.status==="failed"||progress.status==="expired")?{kind:"terminal"}:notYet()}if(!resp.ok)return{kind:"terminal"};let body=await resp.text();if(body.trim().length===0)return{kind:"malformed"};let envelope2;try{envelope2=JSON.parse(body)}catch{return{kind:"malformed"}}return envelope2===null||typeof envelope2!="object"?{kind:"malformed"}:{kind:"observation",ready:!0,value:{critique:classifyLeg(envelope2.critique,baseline?.critique??null),clarify:classifyLeg(envelope2.clarify,baseline?.clarify??null),generation_baseline_available:baselineAvailable,envelope:envelope2}}},isReady:facts=>[facts.critique,facts.clarify].some(leg=>leg.ready)});if(result.state!=="ready")return formatBoundedWaitNonReady(result,`Ticket review for ${ticket_number}`,"Call this tool again if the review may still be running, or re-submit with request_ticket_review if it never started or needs to be retried.");let submissionContext=REVIEW_GENERATION_BASELINES.getContext(REPO_NAME,ticket_number),canonicalPaths=await reviewCanonicalPaths(ticket_number),gateResult=await classifyAndPersistReviewEnvelope({repoName:REPO_NAME,ticketNumber:ticket_number,envelope:result.result.envelope,baseline:submissionContext?.baseline??baseline,expectedLegCount:submissionContext?.expectedLegCount,canonicalPaths,persistenceEnabled:!0});if(!gateResult.ok)return{content:[{type:"text",text:JSON.stringify({error:gateResult.error,status:gateResult.status,state:"error",reason:gateResult.error,message:gateResult.message,saved_legs:gateResult.saved_legs})}]};gateResult.saved_legs.length>0&&REVIEW_GENERATION_BASELINES.clear(REPO_NAME,ticket_number);let critiqueLeg=gateResult.legs.find(leg=>leg.leg==="critique"),clarifyLeg=gateResult.legs.find(leg=>leg.leg==="clarify");return{content:[{type:"text",text:JSON.stringify({state:"ready",ticket_number,delivered_legs:gateResult.delivered_legs,critique_delivered:critiqueLeg?.delivered??!1,clarify_delivered:clarifyLeg?.delivered??!1,critique_generation:critiqueLeg?.generation??null,clarify_generation:clarifyLeg?.generation??null,generation_baseline_available:gateResult.generation_baseline_available,freshness_basis:gateResult.freshness_basis,advanced_legs:gateResult.advanced_legs,accepted_legs:gateResult.accepted_legs,saved_legs:gateResult.saved_legs,withheld_legs:gateResult.withheld_legs,absent_legs:gateResult.absent_legs,replay:gateResult.replay??!1,message:gateResult.message})}]}});registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Combined ticket review: clarifying questions (or debugging guidance for bugs) AND a quality critique, in parallel (~2-6 min). Retrieve via get_clarifying_questions and get_ticket_critique, or wait_for_result to block for only the legs proven current; save_locally WRITES those and may REMOVE a withheld stale file. 404/403 if absent.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z17.literal([1,2]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing."),scope:z17.enum(["questions","critique","both"]).default("both").describe('Which review artifacts to generate: "questions" for clarifying questions (or debugging guidance on a bug ticket) only, "critique" for the ticket quality critique only, or "both" (default) for the combined run. Retrieve either artifact with get_clarifying_questions / get_ticket_critique whichever scope produced it.')}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented ticket (~1-2 min). Retrieve via get_reimplement_context, or set wait_for_result to block and return it directly.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit the ticket itself in the configured ticket backend: not the summary, description, comments, attachments, or status. Works for a ticket from either backend \u2014 it needs only the ticket key. Already tracked is a safe no-op: it upserts the description and repo_name without error. After create_ticket this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. To mutate the ticket use `update_ticket_description`, `add_comment` (requires Jira backend), or `update_jira_status`. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z17.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Records Bridge DB workflow timestamps on a tracked ticket \u2014 does NOT change ticket status (use update_jira_status). Sets each field to current UTC time. Valid: critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called. Needs track_ticket first (else 404); 400 on invalid field.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z17.array(z17.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state_tree",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the repository's live ticket dependency tree: what each ticket is blocked on, what it blocks, and how far each work attempt got. Omit ticket_key for the whole repo, or supply one for its subtree. Read-only: it cannot assert state or declare a dependency.",inputSchema:{ticket_key:commonFields.ticket_number.optional().describe("Optional subtree selector; omit for the whole repository tree.")}},async({ticket_key})=>{let params={repo_name:REPO_NAME};ticket_key&&(params.ticket_key=ticket_key);let url=buildGetUrl("/ticket-state-tree",params),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:`Transition a ticket to a target status in the configured ticket backend. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). Local accepts only 'To Do', 'In Progress', 'In Review', 'Done'. transition_id takes precedence over target_status. Pass target_status as "auto" for server-side LLM resolution of the correct post-PR status; if auto-resolve finds no match it returns status: skipped (not an error). Returns the from/to status, or an error listing available transitions if no match is found.`,inputSchema:{ticket_number:commonFields.ticket_number,target_status:z17.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z17.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","disable_sfcc_mcp_awareness","enable_regression_checks","ai_automation_level","stack_detection_mode","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", "),TICKET_BACKEND_MODE_HINT='ticket_backend_mode is repository config ("jira" | "local"; absent/null = jira), never a ticket-tool argument.',CONFIG_FIELD_OPERATION_FIELDS={get:["field_name"],update:["field_name","value","file_path","only_if_null"],list:[]},CONFIG_FIELD_OPTIONAL_FIELDS=["field_name","value","file_path","only_if_null"],CONFIG_FIELD_NAME_REQUIRED_OPERATIONS=["get","update"];function configFieldValidationError(message){return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message})}]}}function validateConfigFieldOperationFields(args){let operation=String(args.operation),allowed=new Set(CONFIG_FIELD_OPERATION_FIELDS[operation]??[]),offenders=CONFIG_FIELD_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));if(offenders.length>0)return configFieldValidationError(`config_field operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none)"}.`);if(CONFIG_FIELD_NAME_REQUIRED_OPERATIONS.includes(operation)){let fieldName=args.field_name;if(typeof fieldName!="string"||fieldName.length===0)return configFieldValidationError(`config_field operation "${operation}" requires field_name. Valid options: ${VALID_CONFIG_FIELDS}`)}return operation==="update"&&args.value!==void 0&&args.file_path!==void 0?configFieldValidationError('config_field operation "update" accepts value or file_path, not both. Supply exactly one.'):null}registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list. One flat object: field_name is required for get and update and meaningless for list; value, file_path, and only_if_null belong to update only.",inputSchema:{operation:z17.enum(["get","update","list"]).describe("Which configuration operation to perform."),field_name:z17.string().optional().describe(`Required for get and update; omit for list \u2014 list returns every configuration field with its current value. get reads the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads, and apply_install_manifest over many individual updates. ${TICKET_BACKEND_MODE_HINT} Always call with operation: "get" first to read the current value before an update. Returns 400 if the field name is invalid, 404 if no configuration row exists. Valid options: ${VALID_CONFIG_FIELDS}`),value:z17.union([z17.string(),z17.boolean(),z17.array(z17.string()),z17.array(z17.record(z17.string(),z17.unknown())),z17.record(z17.string(),z17.union([z17.string(),z17.null()]))]).optional().describe(`update only. The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by the implement-ticket recipe's PR-creation step and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z17.string().optional().describe("update only. Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops, or for JSON array/object fields."),only_if_null:z17.boolean().optional().describe("update only. Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL or still holds an unmodified Bridge-seeded default (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}},async args=>{let combinationError=validateConfigFieldOperationFields(args);if(combinationError)return combinationError;switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});var inviteMemberTool=registerTool("invite_member",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:'Invite a teammate to THIS configured project by minting a scoped user_access key. Admin-only (a member key or a key scoped to another repo is rejected server-side). repo_name is resolved from the session \u2014 do NOT pass it. Args: email (required), name?, role ("member"|"admin", default member). Returns {id, api_key}; the plaintext api_key is shown EXACTLY ONCE \u2014 relay it verbatim so the admin can distribute it out-of-band.',inputSchema:{email:z17.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z17.string().optional().describe("Optional display name for the teammate."),role:z17.enum(["member","admin"]).optional().default("member").describe('Role for the minted key. Defaults to "member"; pass "admin" only to deliberately opt this invite up to admin.')}},async({email,name,role})=>{try{let resp=await fetch(buildApiUrl("/setup/keys/mint"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,email,name,role:role??"member"})});return{content:[{type:"text",text:await handleResponse(resp)}]}}catch{return{content:[{type:"text",text:JSON.stringify({error:"SERVICE_UNAVAILABLE",status:503,message:"The connection to Bridge API failed before the invite could be submitted. Retry inviting the teammate."})}]}}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, how_to_use, retrieval_of, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools; a non-empty retrieval_of marks a retrieval half, never rendered on its own. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. workflows lists curated non-MCP slash commands. concise_tool_capabilities projects both into two sections (available_now, needs_setup) of mixed tool/workflow items; render as given, never recompute. human_setup is an additive remaining-setup list; render plainly, no stage/contract vocabulary. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok&&isSuccessfulInstallManifestResponse(text3)&&enableLearnToolsOnce("get_install_manifest"),ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text3);text3=text3+note}return{content:[{type:"text",text:text3}]}});var applyInstallManifestTool=registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z17.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z17.record(z17.string(),z17.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text3=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud")try{let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text3+=`
8135
+ ${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text:text3}]}}case"delete":{let{ticket_number,file_name}=args;if(typeof file_name!="string"||file_name.length===0)return{content:[{type:"text",text:JSON.stringify({deleted:!1,message:'No file_name supplied for attachment operation "delete" \u2014 nothing to delete.'})}]};let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a plan. A non-forced request against a fresh existing plan is reused \u2014 202, reused: true, no new work. Otherwise starts an async job (~5-12 min); retrieve via get_plan. 202 if accepted, 404 if the ticket does not exist, 403 if unauthorized. force regenerates deliberately \u2014 never for polling or timeout recovery.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,force:z17.boolean().optional().describe("Deliberately regenerate the plan even when a fresh one is stored. Omit for normal recovery-safe requests. Default: false.")}},async args=>requestTicketArtifact("plan",args));var mergePullRequestDeps={repoName:REPO_NAME,buildApiUrl,buildUrl,buildGetUrl,getHeaders:getGetHeaders,getPostHeaders,handleResponse};ACTIVE_GROUPS.has("estimation")&&registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Estimate an Epic or ticket-key group (exactly one of epic_key/ticket_keys). allow_partial permits a partial result on child failure (default: fail-closed). Repo config may post the breakdown as a comment and set story-points if empty; not overridable via MCP.",inputSchema:{epic_key:z17.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z17.array(z17.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z17.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("assess_epic_conductor_eligibility",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Planning-only: predicts which frozen epic children likely modify workflow files (human merge required) and, of those, which touch claude-review.yml (hand review, no automated review). A ticket-text lower bound, not a fact. Fails open to 'unavailable' on bad input; never spawns or dispatches work.",inputSchema:{children:z17.array(z17.object({id:z17.string().trim().min(1),title:z17.string().trim().min(1),scope:z17.string().optional(),description:z17.string().optional(),requirements:z17.string().optional()})).min(1).describe("Complete ordered child set \u2014 never a partial subset.")}},async args=>{let result=await assessEpicConductorEligibility(args.children,{client:createBridgeEpicConductorEligibilityClient({repoName:REPO_NAME,buildUrl,getPostHeaders})});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start async generation of a design doc (tdd/architecture, fsd, or prd) for a ticket (~2-4 min). architecture and tdd are the same artifact. Retrieve via get_doc \u2014 a 404 there means not ready yet, not a failure \u2014 or set wait_for_result to block and return it directly.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'architecture' (an alias for 'tdd' \u2014 the same artifact, not a fourth document), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a ticket, routed by doc_type. Use doc_type 'tdd' (or its alias 'architecture' \u2014 the same artifact) for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document) or its alias 'architecture' (the same artifact), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_review` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));async function reviewCanonicalPaths(ticketNumber){let config=TICKET_ARTIFACTS.ticket_review,[clarifyDir,critiqueDir]=await Promise.all([getTicketArtifactDocsPath(config.clarifySaveSubdir),getTicketArtifactDocsPath(config.critiqueSaveSubdir)]);return{clarify:path58.join(clarifyDir,TICKET_ARTIFACTS.clarifying_questions.filename(ticketNumber)),critique:path58.join(critiqueDir,TICKET_ARTIFACTS.ticket_critique.filename(ticketNumber))}}registerTool("wait_for_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Wait for a submitted ticket review to finish. Starts no generation, but a ready result WRITES accepted canonical artifact(s) and may REMOVE a withheld leg's stale local file. Returns {state:'ready'|'pending'|'error'}; pending is normal. See saved_legs/withheld_legs/absent_legs/freshness_basis for the authoritative set. Default 20s window.",inputSchema:{ticket_number:commonFields.ticket_number,budget_ms:BUDGET_MS_SCHEMA}},async({ticket_number,budget_ms})=>{let combinedUrl=buildGetUrl(TICKET_ARTIFACTS.ticket_review.getEndpoint(ticket_number),{repo_name:REPO_NAME}),baseline=REVIEW_GENERATION_BASELINES.get(REPO_NAME,ticket_number),baselineAvailable=baseline!==null,classifyLeg=(leg,legBaseline)=>{let content=typeof leg?.content=="string"?leg.content:"";if(content.trim().length===0)return{ready:!1,generation:null,delivered:!1};let generation=parseReviewGeneration(content);return baselineAvailable?generation===null?{ready:!1,generation:null,delivered:!0}:{ready:generationAdvanced(legBaseline,generation),generation,delivered:!0}:{ready:!0,generation,delivered:!0}},result=await boundedWait({budgetMs:budget_ms??DEFAULT_REVIEW_BUDGET_MS,probe:async signal=>{let notYet=()=>({kind:"observation",ready:!0,value:{critique:{ready:!1,generation:null,delivered:!1},clarify:{ready:!1,generation:null,delivered:!1},generation_baseline_available:baselineAvailable,envelope:{}}}),resp=await fetch(combinedUrl,{headers:await getGetHeaders(),signal});if(resp.status===404)return notYet();if(resp.status===202){let progress=parseAutomationProgress(await resp.text());return progress&&(progress.status==="failed"||progress.status==="expired")?{kind:"terminal"}:notYet()}if(!resp.ok)return{kind:"terminal"};let body=await resp.text();if(body.trim().length===0)return{kind:"malformed"};let envelope2;try{envelope2=JSON.parse(body)}catch{return{kind:"malformed"}}return envelope2===null||typeof envelope2!="object"?{kind:"malformed"}:{kind:"observation",ready:!0,value:{critique:classifyLeg(envelope2.critique,baseline?.critique??null),clarify:classifyLeg(envelope2.clarify,baseline?.clarify??null),generation_baseline_available:baselineAvailable,envelope:envelope2}}},isReady:facts=>[facts.critique,facts.clarify].some(leg=>leg.ready)});if(result.state!=="ready")return formatBoundedWaitNonReady(result,`Ticket review for ${ticket_number}`,"Call this tool again if the review may still be running, or re-submit with request_ticket_review if it never started or needs to be retried.");let submissionContext=REVIEW_GENERATION_BASELINES.getContext(REPO_NAME,ticket_number),canonicalPaths=await reviewCanonicalPaths(ticket_number),gateResult=await classifyAndPersistReviewEnvelope({repoName:REPO_NAME,ticketNumber:ticket_number,envelope:result.result.envelope,baseline:submissionContext?.baseline??baseline,expectedLegCount:submissionContext?.expectedLegCount,canonicalPaths,persistenceEnabled:!0});if(!gateResult.ok)return{content:[{type:"text",text:JSON.stringify({error:gateResult.error,status:gateResult.status,state:"error",reason:gateResult.error,message:gateResult.message,saved_legs:gateResult.saved_legs})}]};gateResult.saved_legs.length>0&&REVIEW_GENERATION_BASELINES.clear(REPO_NAME,ticket_number);let critiqueLeg=gateResult.legs.find(leg=>leg.leg==="critique"),clarifyLeg=gateResult.legs.find(leg=>leg.leg==="clarify");return{content:[{type:"text",text:JSON.stringify({state:"ready",ticket_number,delivered_legs:gateResult.delivered_legs,critique_delivered:critiqueLeg?.delivered??!1,clarify_delivered:clarifyLeg?.delivered??!1,critique_generation:critiqueLeg?.generation??null,clarify_generation:clarifyLeg?.generation??null,generation_baseline_available:gateResult.generation_baseline_available,freshness_basis:gateResult.freshness_basis,advanced_legs:gateResult.advanced_legs,accepted_legs:gateResult.accepted_legs,saved_legs:gateResult.saved_legs,withheld_legs:gateResult.withheld_legs,absent_legs:gateResult.absent_legs,replay:gateResult.replay??!1,message:gateResult.message})}]}});registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Combined ticket review: clarifying questions (or debugging guidance for bugs) AND a quality critique, in parallel (~2-6 min). Retrieve via get_clarifying_questions and get_ticket_critique, or wait_for_result to block for only the legs proven current; save_locally WRITES those and may REMOVE a withheld stale file. 404/403 if absent.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z17.literal([1,2]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing."),scope:z17.enum(["questions","critique","both"]).default("both").describe('Which review artifacts to generate: "questions" for clarifying questions (or debugging guidance on a bug ticket) only, "critique" for the ticket quality critique only, or "both" (default) for the combined run. Retrieve either artifact with get_clarifying_questions / get_ticket_critique whichever scope produced it.')}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented ticket (~1-2 min). Retrieve via get_reimplement_context, or set wait_for_result to block and return it directly.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit the ticket itself in the configured ticket backend: not the summary, description, comments, attachments, or status. Works for a ticket from either backend \u2014 it needs only the ticket key. Already tracked is a safe no-op: it upserts the description and repo_name without error. After create_ticket this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. To mutate the ticket use `update_ticket_description`, `add_comment` (requires Jira backend), or `update_jira_status`. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z17.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Records Bridge DB workflow timestamps on a tracked ticket \u2014 does NOT change ticket status (use update_jira_status). Sets each field to current UTC time. Valid: critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called. Needs track_ticket first (else 404); 400 on invalid field.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z17.array(z17.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state_tree",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the repository's live ticket dependency tree: what each ticket is blocked on, what it blocks, and how far each work attempt got. Omit ticket_key for the whole repo, or supply one for its subtree. Read-only: it cannot assert state or declare a dependency.",inputSchema:{ticket_key:commonFields.ticket_number.optional().describe("Optional subtree selector; omit for the whole repository tree.")}},async({ticket_key})=>{let params={repo_name:REPO_NAME};ticket_key&&(params.ticket_key=ticket_key);let url=buildGetUrl("/ticket-state-tree",params),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:`Transition a ticket to a target status in the configured ticket backend. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). Local accepts only 'To Do', 'In Progress', 'In Review', 'Done'. transition_id takes precedence over target_status. Pass target_status as "auto" for server-side LLM resolution of the correct post-PR status; if auto-resolve finds no match it returns status: skipped (not an error). Returns the from/to status, or an error listing available transitions if no match is found.`,inputSchema:{ticket_number:commonFields.ticket_number,target_status:z17.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z17.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","disable_sfcc_mcp_awareness","enable_regression_checks","ai_automation_level","stack_detection_mode","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", "),TICKET_BACKEND_MODE_HINT='ticket_backend_mode is repository config ("jira" | "local"; absent/null = jira), never a ticket-tool argument.',CONFIG_FIELD_OPERATION_FIELDS={get:["field_name"],update:["field_name","value","file_path","only_if_null"],list:[]},CONFIG_FIELD_OPTIONAL_FIELDS=["field_name","value","file_path","only_if_null"],CONFIG_FIELD_NAME_REQUIRED_OPERATIONS=["get","update"];function configFieldValidationError(message){return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message})}]}}function validateConfigFieldOperationFields(args){let operation=String(args.operation),allowed=new Set(CONFIG_FIELD_OPERATION_FIELDS[operation]??[]),offenders=CONFIG_FIELD_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));if(offenders.length>0)return configFieldValidationError(`config_field operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none)"}.`);if(CONFIG_FIELD_NAME_REQUIRED_OPERATIONS.includes(operation)){let fieldName=args.field_name;if(typeof fieldName!="string"||fieldName.length===0)return configFieldValidationError(`config_field operation "${operation}" requires field_name. Valid options: ${VALID_CONFIG_FIELDS}`)}return operation==="update"&&args.value!==void 0&&args.file_path!==void 0?configFieldValidationError('config_field operation "update" accepts value or file_path, not both. Supply exactly one.'):null}registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list. One flat object: field_name is required for get and update and meaningless for list; value, file_path, and only_if_null belong to update only.",inputSchema:{operation:z17.enum(["get","update","list"]).describe("Which configuration operation to perform."),field_name:z17.string().optional().describe(`Required for get and update; omit for list \u2014 list returns every configuration field with its current value. get reads the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads, and apply_install_manifest over many individual updates. ${TICKET_BACKEND_MODE_HINT} Always call with operation: "get" first to read the current value before an update. Returns 400 if the field name is invalid, 404 if no configuration row exists. Valid options: ${VALID_CONFIG_FIELDS}`),value:z17.union([z17.string(),z17.boolean(),z17.array(z17.string()),z17.array(z17.record(z17.string(),z17.unknown())),z17.record(z17.string(),z17.union([z17.string(),z17.null()]))]).optional().describe(`update only. The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by the implement-ticket recipe's PR-creation step and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z17.string().optional().describe("update only. Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops, or for JSON array/object fields."),only_if_null:z17.boolean().optional().describe("update only. Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL or still holds an unmodified Bridge-seeded default (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}},async args=>{let combinationError=validateConfigFieldOperationFields(args);if(combinationError)return combinationError;switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});var inviteMemberTool=registerTool("invite_member",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:'Invite a teammate to THIS configured project by minting a scoped user_access key. Admin-only (a member key or a key scoped to another repo is rejected server-side). repo_name is resolved from the session \u2014 do NOT pass it. Args: email (required), name?, role ("member"|"admin", default member). Returns {id, api_key}; the plaintext api_key is shown EXACTLY ONCE \u2014 relay it verbatim so the admin can distribute it out-of-band.',inputSchema:{email:z17.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z17.string().optional().describe("Optional display name for the teammate."),role:z17.enum(["member","admin"]).optional().default("member").describe('Role for the minted key. Defaults to "member"; pass "admin" only to deliberately opt this invite up to admin.')}},async({email,name,role})=>{try{let resp=await fetch(buildApiUrl("/setup/keys/mint"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,email,name,role:role??"member"})});return{content:[{type:"text",text:await handleResponse(resp)}]}}catch{return{content:[{type:"text",text:JSON.stringify({error:"SERVICE_UNAVAILABLE",status:503,message:"The connection to Bridge API failed before the invite could be submitted. Retry inviting the teammate."})}]}}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, how_to_use, retrieval_of, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools; a non-empty retrieval_of marks a retrieval half, never rendered on its own. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. workflows lists curated non-MCP slash commands. concise_tool_capabilities projects both into two sections (available_now, needs_setup) of mixed tool/workflow items; render as given, never recompute. human_setup is an additive remaining-setup list; render plainly, no stage/contract vocabulary. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok&&isSuccessfulInstallManifestResponse(text3)&&enableLearnToolsOnce("get_install_manifest"),ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text3);text3=text3+note}return{content:[{type:"text",text:text3}]}});var applyInstallManifestTool=registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z17.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z17.record(z17.string(),z17.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text3=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud")try{let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text3+=`
8136
8136
 
8137
8137
  \u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`)}catch(profileError){profileError instanceof DuplicateRegistrationError&&(text3+=`
8138
8138
 
8139
8139
  \u26A0\uFE0F SFCC profile NOT updated: ${profileError.relPaths.join(", ")} carries two Bridge MCP registrations, so it was left untouched. Remove the unintended entry, then re-run this apply to activate the SFCC tools.`)}}catch{}return{content:[{type:"text",text:text3}]}}),learnToolGate=createLearnToolGate({handles:{apply_install_manifest:applyInstallManifestTool,invite_member:inviteMemberTool,regenerate_directory_map:regenerateDirectoryMapTool},getClientName:()=>server.server.getClientVersion()?.name});function enableLearnToolsOnce(trigger){learnToolGate.enableOnce(trigger)}learnToolGate.disableAll();registerTool("persist_routing_credential",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",inputSchema:{repo_name:z17.string().describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process.")}},async({repo_name})=>{let repoName=typeof repo_name=="string"?repo_name.trim():"",deps=buildCredentialStoreWriteDeps(),storePath=getPrimaryCredentialStorePath(deps);if(repoName.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,message:"Cannot persist routing credential: repo_name is required. Pass the repo name this install is configuring.",path:storePath})}]};let target=`bapi:${repoName}`,apiKey=await getResolvedApiKeyForRepo(repoName);if(apiKey.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,target,path:storePath,message:`No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`})}]};let result=await upsertBapiCredential(repoName,apiKey,deps);return result.ok?{content:[{type:"text",text:JSON.stringify({ok:!0,action:result.action,target:result.target,path:result.path,migratedFallback:result.migratedFallback,message:`Stored routing credential for ${result.target} at ${result.path}.`})}]}:{content:[{type:"text",text:JSON.stringify({ok:!1,target:result.target,path:result.path,kind:result.kind,message:`Failed to persist routing credential for ${result.target}: ${result.error} You can rerun /install-bridge or migrate manually.`})}]}});function formatDeepResearchProviderReason(meta){if(!meta)return"";let parts=[],reason=meta.incomplete_details?.reason;reason&&parts.push(`provider reason: ${reason}`);let errMsg=meta.error?.message,errCode=meta.error?.code;return(errMsg||errCode)&&(errCode&&errMsg?parts.push(`provider error: ${errCode}: ${errMsg}`):errMsg?parts.push(`provider error: ${errMsg}`):errCode&&parts.push(`provider error: ${errCode}`)),parts.length?` (${parts.join("; ")})`:""}function _safeIsoMs(value){if(!value)return null;let ms=new Date(value).getTime();return Number.isNaN(ms)?null:ms}function formatDeepResearchElapsed(createdAt,lastPollAt){let createdMs=_safeIsoMs(createdAt);if(createdMs===null)return"";let now=Date.now(),startedMs=Math.max(0,now-createdMs),startedMin=Math.floor(startedMs/6e4),lastPollMs=_safeIsoMs(lastPollAt),pollSuffix="";return lastPollMs!==null&&(pollSuffix=`, last poll ${Math.max(0,Math.floor((now-lastPollMs)/1e3))}s ago`),` (running ${startedMin}m${pollSuffix})`}function formatDeepResearchFailure(body){let kind=body.error_kind||body.error_message||"Unknown error",reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`}function formatDeepResearchStatus(body,taskId){let elapsed=formatDeepResearchElapsed(body.created_at,body.last_poll_at),reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`}registerTool("request_deep_research",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start async deep research on a technical topic using AI-powered web search (~2-10 min). Returns a task_id (or the full report if wait_for_result is true). Retrieve via get_deep_research.",inputSchema:{query:z17.string().describe("The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"),context:z17.string().optional().describe("Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"),ticket_number:commonFields.ticket_number.optional(),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally}},async({query,context,ticket_number,wait_for_result,save_locally})=>{let submitPayload={repo_name:REPO_NAME,query};context&&(submitPayload.context=context),ticket_number&&(submitPayload.ticket_number=ticket_number);let submitResp=await fetch(buildUrl("/deep-research"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let taskId=(await submitResp.json()).task_id;if(!wait_for_result)return{content:[{type:"text",text:`Deep research submitted (task_id: ${taskId}). Processing typically takes 2-10 minutes. Use get_deep_research with task_id ${taskId} to retrieve the result once processing completes.`}]};let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,lastStatus="queued",latestStatusBody=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3);console.error(`Deep research in progress... (elapsed: ${elapsed}s, status: ${lastStatus})`);let statusUrl=buildGetUrl(`/deep-research/${taskId}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok){let errorText=await handleResponse(statusResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error polling deep research status: ${errorText}`})}]}}let statusBody=await statusResp.json();if(lastStatus=statusBody.status,latestStatusBody=statusBody,lastStatus==="completed")break;if(lastStatus==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}if(lastStatus!=="completed"){let statusSuffix=latestStatusBody?` ${formatDeepResearchStatus(latestStatusBody,taskId)}`:"";return{content:[{type:"text",text:`Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`}]}}let resultUrl=buildGetUrl(`/deep-research/${taskId}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok){let errorText=await handleResponse(resultResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error retrieving deep research result: ${errorText}`})}]}}let resultText=await resultResp.text();if(save_locally){let slug=slugify(query),note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${taskId}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});registerTool("get_deep_research",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",inputSchema:{task_id:z17.number().describe("The task ID returned by request_deep_research."),query_slug:z17.string().optional().describe("Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."),save_locally:commonFields.save_locally}},async({task_id,query_slug,save_locally})=>{let statusUrl=buildGetUrl(`/deep-research/${task_id}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(statusBody.status==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};if(statusBody.status!=="completed")return{content:[{type:"text",text:formatDeepResearchStatus(statusBody,task_id)}]};let resultUrl=buildGetUrl(`/deep-research/${task_id}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let resultText=await resultResp.text();if(save_locally){let slug=query_slug||"research",note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${task_id}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});var COUNCIL_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isCouncilTerminalStatus(status){return COUNCIL_TERMINAL_STATUSES.has(status)}async function pollCouncilUntilTerminal(councilId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"council_id",handleValue:councilId,recoveryGetUrl:buildGetUrl(`/councils/${councilId}/result`,{repo_name:repoName}),retrievalToolName:"get_council"};for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3),statusUrl=buildGetUrl(`/councils/${councilId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${councilId} status poll connection failure ${consecutiveFetchFailures}/${3} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=3){let situation2=`Council ${councilId} stopped polling after ${3} consecutive connection failures.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation2,recovery)}}Date.now()-startTime>6e4&&(pollIntervalMs=3e4);continue}if(consecutiveFetchFailures=0,!statusResp.ok)return{kind:"status",envelope:latest};if(latest=await statusResp.json(),latest.rows.every(row2=>isCouncilTerminalStatus(row2.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${councilId} timed out after ${Math.round(MAX_TIMEOUT_MS/1e3)} seconds. The task may still be processing on the server.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation,recovery)}}async function saveCouncilResultsLocally(envelope2,subject){let dir=await getDocsPath("council");return saveCouncilResultsToDir(envelope2,dir,subject)}function formatCouncilToolResponse(envelope2,savedPaths){let lines=[];lines.push(`# Council ${envelope2.council_id}`),lines.push(`Repo: ${envelope2.repo_name}`),lines.push("");for(let row2 of envelope2.results)lines.push(`## ${row2.provider} \u2014 status: ${row2.status}`),lines.push(`error_kind: ${row2.error_kind??"null"}`),row2.error_message&&lines.push(`error_message: ${row2.error_message}`),row2.markdown&&(lines.push(""),lines.push(row2.markdown)),lines.push("");if(savedPaths.length>0){lines.push("---"),lines.push("Saved files:");for(let p of savedPaths)lines.push(`- ${p}`)}return lines.join(`
8140
- `)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start a billable, multi-provider council (~15 min). Prefer wait_for_result=false; returns a council_id to poll via get_council (or the full result envelope if wait_for_result is true).",inputSchema:{task_description:z17.string().describe("Free-form description of the task for the council to weigh in on. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z17.array(z17.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z17.string().optional().describe("Optional caller-supplied concerns to surface to the council agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_council_id:z17.string().optional().describe("Optional council_id (the id field returned by an earlier council) to refine. When provided, the prior council's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z17.enum(["technical","design","discovery","general"]).optional().describe("Council-mode selector. 'technical' (default) is the implementation/architecture council, grounded in indexed repository code. 'general' convenes the council from the supplied brief alone, with no indexed repository context required. 'design' and 'discovery' are accepted compatibility values. Takes precedence over the legacy boolean design field."),design:z17.boolean().optional().describe('Legacy compatibility flag: true selects design mode, taking precedence over prior-run inheritance and the technical default but not over an explicit mode. New callers should use mode: "design". Absent is treated as false.'),lenses:z17.array(z17.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z17.boolean().optional().describe("Opt-in second cross-examination round (default off; ignored in discovery mode). Each provider critiques the others' round-1 output, appended under '## Cross-examination'. Costs an extra round and measured no better than the default \u2014 prefer omitting it."),request_id:z17.string().uuid().optional().describe("Advanced recovery input. Normally omit this \u2014 one is generated automatically. Only pass it when repeating a submission after an ambiguous-outcome error, using the exact request_id from that error, so the retry cannot start a second run.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_council_id,mode,design,lenses,debate,request_id})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_council_id&&(submitPayload.prior_council_request_id=prior_council_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let councilRequestId=request_id??randomUUID5(),councilPostHeaders={...await getPostHeaders(),"X-Bridge-Council-Request-ID":councilRequestId},submitResp;try{submitResp=await fetch(buildUrl("/councils"),{method:"POST",headers:councilPostHeaders,body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure(`the request_council tool, passing request_id: "${councilRequestId}" so the retry cannot start a second run`,{handleName:"request_id",handleValue:councilRequestId,recoveryGetUrl:buildGetUrl(`/councils/${councilRequestId}/result`,{repo_name:effectiveRepo})})}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (council_id: ${submitBody.council_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with council_id ${submitBody.council_id} to retrieve results.`}]};let pollOutcome=await pollCouncilUntilTerminal(submitBody.council_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (council_id: ${submitBody.council_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/councils/${submitBody.council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2,task_description)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve the result envelope for a submitted council by council_id (read-only; does not start one \u2014 use request_council first). Returns opinion-provider rows with error_kind per row; some rows failed or skipped is normal partial success \u2014 never re-request solely for that. Not-found means still processing.",inputSchema:{council_id:z17.string().describe("The UUID returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({council_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/councils/${council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z17.string().describe("The source branch name for the pull request"),base_branch:z17.string().describe("The target/destination branch name for the pull request"),title:z17.string().describe("The title of the pull request"),body:z17.string().optional().describe("The description/body of the pull request. Omitting this creates an EMPTY body \u2014 GitHub's REST API does not auto-apply the repo's PR template.")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("merge_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Ask the server to merge a pull request; the SERVER decides the outcome. Only merged:true is success \u2014 dry_run, pending_approval, and lease_held are non-progress outcomes where nothing merged. Returns one JSON envelope: merged, outcome, reason, retry_hint, evaluated_head_sha, pr_number. repo_name is injected from the environment.",inputSchema:{pr_number:z17.number().int().positive().describe("Pull request number to merge."),expected_head_sha:z17.string().regex(/^[0-9a-fA-F]{40}$/).describe("Full 40-character head commit SHA the merge is bound to.")}},async args=>mergePullRequestHandler(mergePullRequestDeps,args));var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to discover checks for"),force_rerun:z17.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text3=await handleResponse(resp);try{JSON.parse(text3).available===!0&&(pollCiChecksTool.enable(),waitForCiChecksTool.enable())}catch{}return{content:[{type:"text",text:text3}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text3=await handleResponse(resp);try{let parsed=JSON.parse(text3);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text:text3}]}}),waitForCiChecksTool=registerTool("wait_for_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Wait, in one bounded window, until every CI check for a commit is TERMINAL (not necessarily passing). Returns {state:'ready'|'pending'|'error'} plus each check's name, required, status, conclusion and complete. Pending is normal \u2014 call again to keep waiting. Decide pass/fail yourself from the required checks.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to wait for CI checks on"),budget_ms:BUDGET_MS_SCHEMA}},async({commit_ref,budget_ms})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),result=await boundedWait({budgetMs:budget_ms??MAX_BUDGET_MS,probe:async signal=>{let resp=await fetch(url,{headers:await getGetHeaders(),signal});if(!resp.ok)return{kind:"terminal"};let envelope2;try{envelope2=JSON.parse(await resp.text())}catch{return{kind:"malformed"}}if(envelope2===null||typeof envelope2!="object")return{kind:"malformed"};if(envelope2.available!==!0)return{kind:"observation",ready:!0,value:{ready:!1,unavailable:{reason:envelope2.reason,action:envelope2.action},checks:[]}};let detail=envelope2.detail;if(detail===null||typeof detail!="object")return{kind:"malformed"};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{kind:"malformed"};let allComplete=detail.all_complete,checks=rawChecks.map(entry=>{let row2=entry??{};return{name:row2.name,required:row2.required,status:row2.status,conclusion:row2.conclusion,complete:row2.complete}});return{kind:"observation",ready:!0,value:{ready:checks.length>0&&allComplete===!0&&checks.every(c=>c.complete===!0),checks}}},isReady:facts=>facts.ready||facts.unavailable!==void 0});return result.state!=="ready"?formatBoundedWaitNonReady(result,`CI checks for ${commit_ref}`,"Confirm the commit is pushed and reachable, then call resolve_ci_checks for this SHA before waiting again."):result.result.unavailable!==void 0?{content:[{type:"text",text:JSON.stringify({state:"unavailable",commit_ref,reason:result.result.unavailable.reason,action:result.result.unavailable.action,message:"CI check configuration is not available for this repository, so there is nothing to wait on. Call resolve_ci_checks for this commit, or escalate through the recipe's unavailable-CI path."})}]}:{content:[{type:"text",text:JSON.stringify({state:"ready",commit_ref,all_complete:!0,checks:result.result.checks,message:"Every reported check is terminal. Terminal is NOT the same as passing \u2014 decide the outcome from the required checks' conclusions."})}]}});async function checkCiConfigAndDisablePoll(){let disableCiPollingTools=reason=>{pollCiChecksTool.disable(),waitForCiChecksTool.disable(),console.error(`poll_ci_checks and wait_for_ci_checks disabled: ${reason}`)};try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&disableCiPollingTools("ci_check_config is null")}else disableCiPollingTools("could not read ci_check_config")}catch(err){disableCiPollingTools(String(err))}}await checkCiConfigAndDisablePoll();registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). You execute the returned steps yourself (execution_mode: inline); no server-side orchestrator runs them. Bundled pipeline names include review-ticket, implement-ticket, and idea-to-ticket. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z17.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z17.record(z17.string(),z17.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z17.array(z17.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z17.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z17.union([z17.literal(1),z17.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,selectPhase1Instructions(INSTRUCTIONS2),mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED,executionMode:"inline"});if(rounds===1||rounds===2)for(let step of recipe.steps)step.type==="mcp_call"&&step.tool==="request_ticket_review"&&(step.params={...step.params??{},rounds});return pipelineName===LEARN_REPOSITORY_PIPELINE_NAME&&enableLearnToolsOnce("get_pipeline_recipe:learn-repository"),{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});async function fetchPlanMetadata(ticketKey){let resp=await fetch(buildGetUrl(`/ticket/${encodeURIComponent(ticketKey)}/plan`,{repo_name:REPO_NAME,include_metadata:"true"}),{headers:await getGetHeaders()});if(resp.status===202)throw new Error(`Plan generation for ${ticketKey} is still running; routing metadata is not available yet.`);if(!resp.ok)throw new Error(`Could not read plan routing metadata for ${ticketKey} (HTTP ${resp.status}).`);let body;try{body=await resp.json()}catch{throw new Error(`Plan routing metadata for ${ticketKey} was not valid JSON.`)}let envelope2=body??{},status=envelope2.plan_metadata_status;if(envelope2.plan_metadata===null||envelope2.plan_metadata===void 0)throw new Error(`Plan for ${ticketKey} carries no routing metadata (plan_metadata_status: ${String(status??"absent")}). Ownership must not be inferred from plan prose \u2014 regenerate the plan or execute every step.`);return envelope2.plan_metadata}function safeArtifactMessage(err){let raw=err instanceof Error?err.message:String(err);return redactSecrets(raw).replace(/(?:\/[^\s/:"']+){2,}/g,match=>`\u2026/${match.slice(match.lastIndexOf("/")+1)}`)}function artifactErrorEnvelope(err,status){let message=safeArtifactMessage(err);return console.error(`[phase-artifacts] ${message}`),{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[status]??"BAD_REQUEST",status,message})}]}}function artifactOkEnvelope(payload){return{content:[{type:"text",text:JSON.stringify(payload,null,2)}]}}registerTool("get_phase_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Inline pipeline phases only: return the step IDs this plan phase owns plus the durable ledger of what earlier phases already settled. Ownership is derived from the plan's provenance metadata, never from plan prose. Reading creates nothing.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase:z17.enum(PLAN_PHASES).describe("Which plan phase's routing to return.")}},async({ticket_key,phase})=>{try{let context=await loadInlinePhaseContext({docsDir:await getDocsDir(),ticketKey:ticket_key,phase,planMetadata:await fetchPlanMetadata(ticket_key)});return artifactOkEnvelope({status:"ok",...context})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_phase_result",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Inline pipeline phases only: validates a phase-result envelope against owned steps, persists it to the ledger. Dispositions: executed, adapted (needs adaptation.kind + evidence), escalated (needs escalationReason), unrun-advisory (needs advisoryExplanation). Rejects unsettled/unowned/backslid steps. Call as the phase final action, then continue.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase_result:z17.record(z17.string(),z17.unknown()).describe("The bapi-phase-result envelope: {version, phase, records:[{stepId, provenanceClass, disposition, evidence}]}.")}},async({ticket_key,phase_result})=>{try{let result=await writeInlinePhaseResult({docsDir:await getDocsDir(),ticketKey:ticket_key,planMetadata:await fetchPlanMetadata(ticket_key),phaseResult:phase_result});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_checkpoint",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Inline pipeline phases only: record the branch and SHA the produced work was pushed at. Accepted only when the push is proven durable on origin (pushed and remote_matches_head both true).",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),branch:z17.string().describe("The branch that was pushed."),sha:z17.string().describe("The pushed HEAD SHA."),pushed:z17.boolean().describe("True only when the push succeeded."),remote_matches_head:z17.boolean().describe("True only when the remote tip equals the local HEAD.")}},async({ticket_key,branch,sha,pushed,remote_matches_head})=>{try{let result=await writeInlineCheckpoint({docsDir:await getDocsDir(),ticketKey:ticket_key,checkpoint:{branch,sha,pushed,remoteMatchesHead:remote_matches_head}});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("capture_review_decisions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Authoritative durable write for settled ticket-review decisions. One resolution per stable review-item id, checked against the pending generation's manifest. Prose markers alone are NOT durable. Surface refusals; never work around them.",inputSchema:{ticket_number:z17.string().describe("Ticket key, e.g. BAPI-123."),generation:z17.number().int().positive().describe("Required; read from delivered-document identity metadata."),schema_version:z17.number().int().positive().optional(),source_bundle_sha256:z17.string().regex(/^[0-9a-f]{64}$/).describe("Required; read from the same metadata. Mismatch is refused."),resolutions:z17.array(z17.object({id:z17.string().describe("Stable review-item id, e.g. crit-001."),adjudication:z17.enum(["accepted","rejected"]),effect:z17.enum(["apply","dismiss"]),binding_instruction:z17.string().nullable().optional().describe("Non-empty for apply; null for dismiss.")})).describe("Exactly one entry per manifest id."),critique_document:z17.string().optional().describe("Rewritten critique markdown (transient)."),clarify_document:z17.string().optional().describe("Rewritten clarifying markdown (transient)."),capture_workflow_version:z17.string().optional(),capture_trace_id:z17.string().optional(),request_id:z17.string().optional().describe("Reuse across retries of one capture.")}},async({ticket_number,generation,schema_version,source_bundle_sha256,resolutions,critique_document,clarify_document,capture_workflow_version,capture_trace_id,request_id})=>{let resp=await fetch(buildUrl("/review-decisions/capture"),{method:"POST",headers:{...await getPostHeaders(),"X-Bridge-Review-Capture-Request-ID":request_id??randomUUID5()},body:JSON.stringify({repo_name:REPO_NAME,ticket_number,generation,schema_version,source_bundle_sha256,resolutions,critique_document,clarify_document,capture_workflow_version,capture_trace_id})});return{content:[{type:"text",text:await handleResponse(resp)}]}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os26.tmpdir(),entries;try{entries=await readdir7(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path58.join(tmpDir,entry);try{let info=await stat12(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm7(fullPath,{recursive:!0,force:!0})}catch{}}}async function materializeFreshBase(base_branch,base_sha,no_refresh_base){let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp4(path58.join(os26.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path58.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm7(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink7(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm7(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}}async function cleanupFreshBase(fresh_base_root){let allowedPrefix=path58.join(os26.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path58.resolve(fresh_base_root),resolvedTmpDir=path58.resolve(os26.tmpdir()),isDirectChildOfTmpDir=path58.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path58.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm7(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})}registerTool("fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:'Materialize or remove the pinned review base tree. action "materialize": git archive origin/<base_branch> into a temp dir; returns { base_sha, base_branch, fresh_base_root }. base_branch precedence: param > config > "main"; no_refresh_base "true" returns base_sha "local-stale" and the project root. action "cleanup": deletes fresh_base_root, refusing paths outside <tmpdir>/bridge-review-*.',inputSchema:{action:z17.enum(["materialize","cleanup"]).describe('"materialize" creates a pinned base tree; "cleanup" removes one created earlier.'),base_branch:z17.string().optional().describe(`materialize only. Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z17.string().optional().describe("materialize only. Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z17.string().optional().describe('materialize only. Pass "true" to skip fetch/materialization and fall back to the local project root as-is.'),fresh_base_root:z17.string().optional().describe("cleanup only, and required for it. The fresh_base_root path returned by a prior materialize call.")}},async({action,base_branch,base_sha,no_refresh_base,fresh_base_root})=>action==="cleanup"?typeof fresh_base_root!="string"||fresh_base_root.trim().length===0?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'fresh_base_root is required for action "cleanup".'})}]}:await cleanupFreshBase(fresh_base_root):await materializeFreshBase(base_branch,base_sha,no_refresh_base));function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path58.posix.isAbsolute(value)||path58.win32.isAbsolute(value)||path58.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path58.resolve(await getDocsDir()),resolvedTarget=path58.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path58.sep)?{ok:!0,docsPath:path58.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}var DECISION_PAGE_CONTENT_CONTRACT="Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options).",DECISION_PAGE_CONTENT_EXAMPLE='{"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}';function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);if(input.content===void 0)return validationError2(`No \`content\` supplied. All decision data must be nested under \`content\` \u2014 root-level actionable_items / system_goals / clear_improvements / implementation_order are dropped by the lean input schema. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z17.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path58.resolve(path58.dirname(fileURLToPath4(import.meta.url)),"../"),assetsDir;try{await stat12(path58.join(projectRootForAssets,"design-assets")),assetsDir=path58.join(projectRootForAssets,"design-assets")}catch{assetsDir=path58.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile21(path58.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile21(path58.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir16(docsPath,{recursive:!0}),await writeFile15(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var updateStatusManager=createUpdateStatusManager({warn:message=>console.error(message),onLateStale:()=>{try{server.server.sendToolListChanged()}catch{}},enabled:UPDATE_CHECK_ENABLED});UPDATE_CHECK_ENABLED&&updateStatusManager.start();var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,createUpdateAdvisoryListHandler(gate.handleList,()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed())),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");if(!toolSurfaceGate)try{let protocolServer=server.server,capturedOriginal=null,handler=createUpdateAdvisoryListHandler((request,extra)=>capturedOriginal?capturedOriginal(request,extra):Promise.resolve({tools:[]}),()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed());capturedOriginal=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,handler)}catch{}var previousOnInitialized=server.server.oninitialized;server.server.oninitialized=()=>{try{previousOnInitialized?.()}catch{}try{let clientName=server.server.getClientVersion()?.name;isKnownListChangedClient(clientName)||enableLearnToolsOnce("client-fallback")}catch{enableLearnToolsOnce("client-fallback")}};var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} (commit ${BUILD_COMMIT}) starting on stdio, waiting for an MCP client. To set up a project, run: npx -y ${MCP_PACKAGE_NAME} install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
8140
+ `)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start a billable, multi-provider council (~15 min). Prefer wait_for_result=false; returns a council_id to poll via get_council (or the full result envelope if wait_for_result is true).",inputSchema:{task_description:z17.string().describe("Free-form description of the task for the council to weigh in on. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z17.array(z17.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z17.string().optional().describe("Optional caller-supplied concerns to surface to the council agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_council_id:z17.string().optional().describe("Optional council_id (the id field returned by an earlier council) to refine. When provided, the prior council's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z17.enum(["technical","design","discovery","general"]).optional().describe("Council-mode selector. 'technical' (default) is the implementation/architecture council, grounded in indexed repository code. 'general' convenes the council from the supplied brief alone, with no indexed repository context required. 'design' and 'discovery' are accepted compatibility values. Takes precedence over the legacy boolean design field."),design:z17.boolean().optional().describe('Legacy compatibility flag: true selects design mode, taking precedence over prior-run inheritance and the technical default but not over an explicit mode. New callers should use mode: "design". Absent is treated as false.'),lenses:z17.array(z17.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z17.boolean().optional().describe("Opt-in second cross-examination round (default off; ignored in discovery mode). Each provider critiques the others' round-1 output, appended under '## Cross-examination'. Costs an extra round and measured no better than the default \u2014 prefer omitting it."),request_id:z17.string().uuid().optional().describe("Advanced recovery input. Normally omit this \u2014 one is generated automatically. Only pass it when repeating a submission after an ambiguous-outcome error, using the exact request_id from that error, so the retry cannot start a second run.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_council_id,mode,design,lenses,debate,request_id})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_council_id&&(submitPayload.prior_council_request_id=prior_council_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let councilRequestId=request_id??randomUUID5(),councilPostHeaders={...await getPostHeaders(),"X-Bridge-Council-Request-ID":councilRequestId},submitResp;try{submitResp=await fetch(buildUrl("/councils"),{method:"POST",headers:councilPostHeaders,body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure(`the request_council tool, passing request_id: "${councilRequestId}" so the retry cannot start a second run`,{handleName:"request_id",handleValue:councilRequestId,recoveryGetUrl:buildGetUrl(`/councils/${councilRequestId}/result`,{repo_name:effectiveRepo})})}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (council_id: ${submitBody.council_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with council_id ${submitBody.council_id} to retrieve results.`}]};let pollOutcome=await pollCouncilUntilTerminal(submitBody.council_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (council_id: ${submitBody.council_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/councils/${submitBody.council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2,task_description)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve the result envelope for a submitted council by council_id (read-only; does not start one \u2014 use request_council first). Returns opinion-provider rows with error_kind per row; some rows failed or skipped is normal partial success \u2014 never re-request solely for that. Not-found means still processing.",inputSchema:{council_id:z17.string().describe("The UUID returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({council_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/councils/${council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z17.string().describe("The source branch name for the pull request"),base_branch:z17.string().describe("The target/destination branch name for the pull request"),title:z17.string().describe("The title of the pull request"),body:z17.string().optional().describe("The description/body of the pull request. Omitting this creates an EMPTY body \u2014 GitHub's REST API does not auto-apply the repo's PR template.")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("merge_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Ask the server to merge a pull request; the SERVER decides the outcome. Only merged:true is success \u2014 dry_run, pending_approval, and lease_held are non-progress outcomes where nothing merged. Returns one JSON envelope: merged, outcome, reason, retry_hint, evaluated_head_sha, pr_number. repo_name is injected from the environment.",inputSchema:{pr_number:z17.number().int().positive().describe("Pull request number to merge."),expected_head_sha:z17.string().regex(/^[0-9a-fA-F]{40}$/).describe("Full 40-character head commit SHA the merge is bound to.")}},async args=>mergePullRequestHandler(mergePullRequestDeps,args));var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to discover checks for"),force_rerun:z17.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text3=await handleResponse(resp);try{JSON.parse(text3).available===!0&&(pollCiChecksTool.enable(),waitForCiChecksTool.enable())}catch{}return{content:[{type:"text",text:text3}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text3=await handleResponse(resp);try{let parsed=JSON.parse(text3);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text:text3}]}}),waitForCiChecksTool=registerTool("wait_for_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Wait, in one bounded window, until every CI check for a commit is TERMINAL (not necessarily passing). Returns {state:'ready'|'pending'|'error'} plus each check's name, required, status, conclusion and complete. Pending is normal \u2014 call again to keep waiting. Default window: 240000 ms. Decide pass/fail yourself from the required checks.",inputSchema:{commit_ref:z17.string().describe("Git commit SHA to wait for CI checks on"),budget_ms:BUDGET_MS_SCHEMA}},async({commit_ref,budget_ms})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),result=await boundedWait({budgetMs:budget_ms??MAX_BUDGET_MS,probe:async signal=>{let resp=await fetch(url,{headers:await getGetHeaders(),signal});if(!resp.ok)return{kind:"terminal"};let envelope2;try{envelope2=JSON.parse(await resp.text())}catch{return{kind:"malformed"}}if(envelope2===null||typeof envelope2!="object")return{kind:"malformed"};if(envelope2.available!==!0)return{kind:"observation",ready:!0,value:{ready:!1,unavailable:{reason:envelope2.reason,action:envelope2.action},checks:[]}};let detail=envelope2.detail;if(detail===null||typeof detail!="object")return{kind:"malformed"};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{kind:"malformed"};let allComplete=detail.all_complete,checks=rawChecks.map(entry=>{let row2=entry??{};return{name:row2.name,required:row2.required,status:row2.status,conclusion:row2.conclusion,complete:row2.complete}});return{kind:"observation",ready:!0,value:{ready:checks.length>0&&allComplete===!0&&checks.every(c=>c.complete===!0),checks}}},isReady:facts=>facts.ready||facts.unavailable!==void 0});return result.state!=="ready"?formatBoundedWaitNonReady(result,`CI checks for ${commit_ref}`,"Confirm the commit is pushed and reachable, then call resolve_ci_checks for this SHA before waiting again."):result.result.unavailable!==void 0?{content:[{type:"text",text:JSON.stringify({state:"unavailable",commit_ref,reason:result.result.unavailable.reason,action:result.result.unavailable.action,message:"CI check configuration is not available for this repository, so there is nothing to wait on. Call resolve_ci_checks for this commit, or escalate through the recipe's unavailable-CI path."})}]}:{content:[{type:"text",text:JSON.stringify({state:"ready",commit_ref,all_complete:!0,checks:result.result.checks,message:"Every reported check is terminal. Terminal is NOT the same as passing \u2014 decide the outcome from the required checks' conclusions."})}]}});async function checkCiConfigAndDisablePoll(){let disableCiPollingTools=reason=>{pollCiChecksTool.disable(),waitForCiChecksTool.disable(),console.error(`poll_ci_checks and wait_for_ci_checks disabled: ${reason}`)};try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&disableCiPollingTools("ci_check_config is null")}else disableCiPollingTools("could not read ci_check_config")}catch(err){disableCiPollingTools(String(err))}}await checkCiConfigAndDisablePoll();registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). You execute the returned steps yourself (execution_mode: inline); no server-side orchestrator runs them. Bundled pipeline names include review-ticket, implement-ticket, and idea-to-ticket. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z17.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z17.record(z17.string(),z17.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z17.array(z17.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z17.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z17.union([z17.literal(1),z17.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,selectPhase1Instructions(INSTRUCTIONS2),mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED,executionMode:"inline"});if(rounds===1||rounds===2)for(let step of recipe.steps)step.type==="mcp_call"&&step.tool==="request_ticket_review"&&(step.params={...step.params??{},rounds});return pipelineName===LEARN_REPOSITORY_PIPELINE_NAME&&enableLearnToolsOnce("get_pipeline_recipe:learn-repository"),{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});async function fetchPlanMetadata(ticketKey){let resp=await fetch(buildGetUrl(`/ticket/${encodeURIComponent(ticketKey)}/plan`,{repo_name:REPO_NAME,include_metadata:"true"}),{headers:await getGetHeaders()});if(resp.status===202)throw new Error(`Plan generation for ${ticketKey} is still running; routing metadata is not available yet.`);if(!resp.ok)throw new Error(`Could not read plan routing metadata for ${ticketKey} (HTTP ${resp.status}).`);let body;try{body=await resp.json()}catch{throw new Error(`Plan routing metadata for ${ticketKey} was not valid JSON.`)}let envelope2=body??{},status=envelope2.plan_metadata_status;if(envelope2.plan_metadata===null||envelope2.plan_metadata===void 0)throw new Error(`Plan for ${ticketKey} carries no routing metadata (plan_metadata_status: ${String(status??"absent")}). Ownership must not be inferred from plan prose \u2014 regenerate the plan or execute every step.`);return envelope2.plan_metadata}function safeArtifactMessage(err){let raw=err instanceof Error?err.message:String(err);return redactSecrets(raw).replace(/(?:\/[^\s/:"']+){2,}/g,match=>`\u2026/${match.slice(match.lastIndexOf("/")+1)}`)}function artifactErrorEnvelope(err,status){let message=safeArtifactMessage(err);return console.error(`[phase-artifacts] ${message}`),{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[status]??"BAD_REQUEST",status,message})}]}}function artifactOkEnvelope(payload){return{content:[{type:"text",text:JSON.stringify(payload,null,2)}]}}registerTool("get_phase_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Inline pipeline phases only: return the step IDs this plan phase owns plus the durable ledger of what earlier phases already settled. Ownership is derived from the plan's provenance metadata, never from plan prose. Reading creates nothing.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase:z17.enum(PLAN_PHASES).describe("Which plan phase's routing to return.")}},async({ticket_key,phase})=>{try{let context=await loadInlinePhaseContext({docsDir:await getDocsDir(),ticketKey:ticket_key,phase,planMetadata:await fetchPlanMetadata(ticket_key)});return artifactOkEnvelope({status:"ok",...context})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_phase_result",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Inline pipeline phases only: validates a phase-result envelope against owned steps, persists it to the ledger. Dispositions: executed, adapted (needs adaptation.kind + evidence), escalated (needs escalationReason), unrun-advisory (needs advisoryExplanation). Rejects unsettled/unowned/backslid steps. Call as the phase final action, then continue.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase_result:z17.record(z17.string(),z17.unknown()).describe("The bapi-phase-result envelope: {version, phase, records:[{stepId, provenanceClass, disposition, evidence}]}.")}},async({ticket_key,phase_result})=>{try{let result=await writeInlinePhaseResult({docsDir:await getDocsDir(),ticketKey:ticket_key,planMetadata:await fetchPlanMetadata(ticket_key),phaseResult:phase_result});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_checkpoint",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Inline pipeline phases only: record the branch and SHA the produced work was pushed at. Accepted only when the push is proven durable on origin (pushed and remote_matches_head both true).",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),branch:z17.string().describe("The branch that was pushed."),sha:z17.string().describe("The pushed HEAD SHA."),pushed:z17.boolean().describe("True only when the push succeeded."),remote_matches_head:z17.boolean().describe("True only when the remote tip equals the local HEAD.")}},async({ticket_key,branch,sha,pushed,remote_matches_head})=>{try{let result=await writeInlineCheckpoint({docsDir:await getDocsDir(),ticketKey:ticket_key,checkpoint:{branch,sha,pushed,remoteMatchesHead:remote_matches_head}});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("capture_review_decisions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Authoritative durable write for settled ticket-review decisions. One resolution per stable review-item id, checked against the pending generation's manifest. Prose markers alone are NOT durable. Surface refusals; never work around them.",inputSchema:{ticket_number:z17.string().describe("Ticket key, e.g. BAPI-123."),generation:z17.number().int().positive().describe("Required; read from delivered-document identity metadata."),schema_version:z17.number().int().positive().optional(),source_bundle_sha256:z17.string().regex(/^[0-9a-f]{64}$/).describe("Required; read from the same metadata. Mismatch is refused."),resolutions:z17.array(z17.object({id:z17.string().describe("Stable review-item id, e.g. crit-001."),adjudication:z17.enum(["accepted","rejected"]),effect:z17.enum(["apply","dismiss"]),binding_instruction:z17.string().nullable().optional().describe("Non-empty for apply; null for dismiss.")})).describe("Exactly one entry per manifest id."),critique_document:z17.string().optional().describe("Rewritten critique markdown (transient)."),clarify_document:z17.string().optional().describe("Rewritten clarifying markdown (transient)."),capture_workflow_version:z17.string().optional(),capture_trace_id:z17.string().optional(),request_id:z17.string().optional().describe("Reuse across retries of one capture.")}},async({ticket_number,generation,schema_version,source_bundle_sha256,resolutions,critique_document,clarify_document,capture_workflow_version,capture_trace_id,request_id})=>{let resp=await fetch(buildUrl("/review-decisions/capture"),{method:"POST",headers:{...await getPostHeaders(),"X-Bridge-Review-Capture-Request-ID":request_id??randomUUID5()},body:JSON.stringify({repo_name:REPO_NAME,ticket_number,generation,schema_version,source_bundle_sha256,resolutions,critique_document,clarify_document,capture_workflow_version,capture_trace_id})});return{content:[{type:"text",text:await handleResponse(resp)}]}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os26.tmpdir(),entries;try{entries=await readdir7(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path58.join(tmpDir,entry);try{let info=await stat12(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm7(fullPath,{recursive:!0,force:!0})}catch{}}}async function materializeFreshBase(base_branch,base_sha,no_refresh_base){let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp4(path58.join(os26.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path58.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm7(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink7(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm7(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}}async function cleanupFreshBase(fresh_base_root){let allowedPrefix=path58.join(os26.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path58.resolve(fresh_base_root),resolvedTmpDir=path58.resolve(os26.tmpdir()),isDirectChildOfTmpDir=path58.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path58.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm7(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})}registerTool("fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:'Materialize or remove the pinned review base tree. action "materialize": git archive origin/<base_branch> into a temp dir; returns { base_sha, base_branch, fresh_base_root }. base_branch precedence: param > config > "main"; no_refresh_base "true" returns base_sha "local-stale" and the project root. action "cleanup": deletes fresh_base_root, refusing paths outside <tmpdir>/bridge-review-*.',inputSchema:{action:z17.enum(["materialize","cleanup"]).describe('"materialize" creates a pinned base tree; "cleanup" removes one created earlier.'),base_branch:z17.string().optional().describe(`materialize only. Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z17.string().optional().describe("materialize only. Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z17.string().optional().describe('materialize only. Pass "true" to skip fetch/materialization and fall back to the local project root as-is.'),fresh_base_root:z17.string().optional().describe("cleanup only, and required for it. The fresh_base_root path returned by a prior materialize call.")}},async({action,base_branch,base_sha,no_refresh_base,fresh_base_root})=>action==="cleanup"?typeof fresh_base_root!="string"||fresh_base_root.trim().length===0?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'fresh_base_root is required for action "cleanup".'})}]}:await cleanupFreshBase(fresh_base_root):await materializeFreshBase(base_branch,base_sha,no_refresh_base));function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path58.posix.isAbsolute(value)||path58.win32.isAbsolute(value)||path58.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path58.resolve(await getDocsDir()),resolvedTarget=path58.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path58.sep)?{ok:!0,docsPath:path58.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}var DECISION_PAGE_CONTENT_CONTRACT="Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options).",DECISION_PAGE_CONTENT_EXAMPLE='{"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}';function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);if(input.content===void 0)return validationError2(`No \`content\` supplied. All decision data must be nested under \`content\` \u2014 root-level actionable_items / system_goals / clear_improvements / implementation_order are dropped by the lean input schema. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z17.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path58.resolve(path58.dirname(fileURLToPath5(import.meta.url)),"../"),assetsDir;try{await stat12(path58.join(projectRootForAssets,"design-assets")),assetsDir=path58.join(projectRootForAssets,"design-assets")}catch{assetsDir=path58.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile21(path58.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile21(path58.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir16(docsPath,{recursive:!0}),await writeFile15(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var updateStatusManager=createUpdateStatusManager({warn:message=>console.error(message),onLateStale:()=>{try{server.server.sendToolListChanged()}catch{}},enabled:UPDATE_CHECK_ENABLED});UPDATE_CHECK_ENABLED&&updateStatusManager.start();var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,createUpdateAdvisoryListHandler(gate.handleList,()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed())),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");if(!toolSurfaceGate)try{let protocolServer=server.server,capturedOriginal=null,handler=createUpdateAdvisoryListHandler((request,extra)=>capturedOriginal?capturedOriginal(request,extra):Promise.resolve({tools:[]}),()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed());capturedOriginal=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,handler)}catch{}var previousOnInitialized=server.server.oninitialized;server.server.oninitialized=()=>{try{previousOnInitialized?.()}catch{}try{let clientName=server.server.getClientVersion()?.name;isKnownListChangedClient(clientName)||enableLearnToolsOnce("client-fallback")}catch{enableLearnToolsOnce("client-fallback")}};var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} (commit ${BUILD_COMMIT}) starting on stdio, waiting for an MCP client. To set up a project, run: npx -y ${MCP_PACKAGE_NAME} install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};