@bridge_gpt/mcp-server 0.2.44 → 0.2.45

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,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.44",BUILD_COMMIT="01d0612aa886"}});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,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.45",BUILD_COMMIT="4cb362377d01"}});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
 
@@ -385,7 +385,7 @@ This stage is **non-critical** \u2014 log a warning on failure but do not stop t
385
385
  On success, display the structured report from Stage 4 confirming that the commit and push are complete, including the branch name, commit hash, file list, and any warnings from earlier stages.
386
386
 
387
387
  On failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.
388
- `,"conduct-epic.md":"---\nschedulable: true\narguments: {\"positionals\":[{\"name\":\"epicKey\",\"type\":\"string\",\"required\":true}],\"flags\":[{\"name\":\"tickets\",\"flag\":\"--tickets\",\"type\":\"string\"},{\"name\":\"baseBranch\",\"flag\":\"--base-branch\",\"type\":\"string\"},{\"name\":\"checkpointPath\",\"flag\":\"--checkpoint-path\",\"type\":\"string\"}]}\n---\n\n# Conduct Epic: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command drives one multi-ticket epic from an approved ticket list to a finished `epic/<EPIC>` branch, one ticket at a time. It is the LLM half of the LLM-as-conductor pivot: there is no server-side reconciler here, no background worker, and no durable run row \u2014 the checkpoint file written by the packaged `conduct-epic` CLI plus the live state of GitHub *is* the entire memory of the loop.\n\nCadence is not an argument of this command. `/loop` owns the interval, this command owns exactly one reconcile-then-act step, and the two compose without either one holding state for the other.\n\nIt composes work that already exists rather than reimplementing it: `/review-and-start --auto --base-branch 'epic/<EPIC>' <KEY>` spawns each ticket's worker, the `merge_pull_request` MCP tool merges a green and approved pull request, `parse_repository` / `get_parse_status` re-index the repository after each merge so the next ticket's plan sees its predecessor's code, and the packaged `conduct-epic` CLI (`init`, `status`, `checkpoint set`, `finish`, `spawn`) owns every durable file operation.\n\n---\n\n# Instructions\n\nYou are executing a 5-stage tick. Run the stages in order, take **exactly one** action from the Stage 3 detection table, write **exactly one** checkpoint in Stage 4, then stop. Do not loop internally, do not take a second action because the first one looked cheap, and do not carry assumptions from a previous tick \u2014 every tick reconciles from scratch.\n\nThe \"exactly one checkpoint\" rule has **three explicitly documented exemptions** and no others: the two print-only parks, `init_failed` (Stage 1) and `foreign_lock` (Stage 2), which stop before Stage 3; and the `all_done` tick (Row 1), which has no in-flight ticket to name in a `checkpoint set` command. Stage 4 states each one.\n\n## Stage 0 \u2014 Arguments and Ping\n\n1. **Parse `$ARGUMENTS`** into exactly one epic positional and the three optional flags. Accept no other input shape.\n\n - **`<EPIC>`**: exactly one positional token, which must match `[A-Z]+-[0-9]+` (e.g. `BAPI-798`). Zero epic positionals, more than one positional, or a positional that does not match the pattern is malformed input. Extra positionals are rejected rather than ignored.\n - **`--tickets <K1,K2,\u2026>`** (and the equals form `--tickets=<K1,K2,\u2026>`): a non-empty, comma-separated, **ordered** list of ticket keys. Preserve the caller's order exactly \u2014 it is the execution order of the epic. Every entry must match `[A-Z]+-[0-9]+` after trimming surrounding whitespace; reject a malformed key, an empty entry, and a duplicate key. This flag is required **only on the first tick** (see Stage 1); later ticks read the order from the checkpoint.\n - **`--base-branch <branch>`** (and the equals form `--base-branch=<branch>`): validated with the same rules as `/start-tickets` Stage 0 \u2014 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`). It is the branch `epic/<EPIC>` is cut from at `init` time; it is not the pull-request base of a ticket, which is always `epic/<EPIC>`.\n - **`--checkpoint-path <path>`** (and the equals form `--checkpoint-path=<path>`): must be a non-empty string after trimming, checked **before** it is used as a path or interpolated into a CLI invocation. When omitted, the CLI's own default (`~/.config/bridge/conduct/<repo>/<EPIC>.json`) applies and `status` prints the resolved path.\n\n Reject malformed input before any side effect: an unsupported flag, a flag given without its value, a `--tickets` list that fails the rules above, a `--base-branch` value that fails validation, an empty `--checkpoint-path`, a missing epic, or an extra positional. On any of these, stop immediately and display:\n\n ```\n Invalid arguments.\n Usage: /conduct-epic [flags] <EPIC>\n <EPIC> required, matches [A-Z]+-[0-9]+ (e.g. BAPI-798)\n --tickets K1,K2,\u2026 ordered ticket keys; required only on the first tick\n --base-branch <branch> branch epic/<EPIC> is cut from (default: the repo base)\n --checkpoint-path <path> override the checkpoint file location\n ```\n\n2. **Connectivity check**: call the `ping` MCP tool with **no parameters**. If the call fails, or does not return `\"status\": \"ok\"`, stop immediately \u2014 before Stage 1 initialization, before any CLI invocation, and before any state is written \u2014 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\n3. **Execution model.** This command is one tick; run it with `/loop 5m /conduct-epic <EPIC>`; each tick re-reads the checkpoint and GitHub, takes at most one action, and exits. `/loop` is the external driver that re-invokes this command \u2014 it is not an internal loop this command runs, and cadence is never an argument of this command.\n\n4. **Bash interpolation rule (global; applies to every Bash invocation in every stage).** Before interpolating any dynamic value \u2014 the epic key, a ticket key, a branch name, a checkpoint path, a prompt-file path, a JSON blob, a journal line \u2014 replace every `'` in the value with `'\\''`, then wrap the complete value in single quotes. Never expand a dynamic value unquoted, and never build a command by concatenating an unquoted variable. Credentials must never appear in a command argument, in printed output, in a journal line, or in a prompt file: the CLI and the MCP tools resolve their own credentials from the environment and the user-scoped credential store.\n\n5. **Packaged CLI launcher (`BAPI_MCP_CLI`); global, applies to every packaged-CLI invocation in every stage.** Resolve the launcher **once**, here in Stage 0, and reuse that one resolved value for the rest of the tick. 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\n When the override is set, apply item 4's single-quote escaping rule to `<launcher>` before interpolating it into a Bash command string, keep every dynamic argument independently quoted rather than concatenated into the launcher value, and never put a credential or a credential-bearing environment assignment into it. 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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Initialize If No Checkpoint\n\nRun the first status probe through the **Bash tool**, forwarding `--checkpoint-path '<path>'` only when the user supplied it:\n\n```\n<launcher> conduct-epic status '<EPIC>' --json\n```\n\nA zero-exit response whose `checkpoint_exists` is `false` is the **only** condition under which initialization is allowed.\n\n- **`checkpoint_exists` is `false`** \u2014 this is the first tick. `--tickets` is required here, and **only** here: if it was not supplied, halt with the Stage 0 usage message and initialize nothing. On every later tick `--tickets` is optional and ignored, because the ordered list already lives in the checkpoint. Otherwise run, forwarding `--base-branch '<b>'` and `--checkpoint-path '<p>'` only when supplied:\n\n ```\n <launcher> conduct-epic init '<EPIC>' --tickets '<K1,K2,\u2026>'\n ```\n\n Print the initialization preflight output **verbatim** \u2014 do not summarize it, do not suppress its announcements, and do not reorder it. `init` runs one preflight that lists every failure at once, and that listing is the operator's only diagnostic when it refuses.\n\n On a **non-zero** exit, `init_failed` is a **print-only park**: emit `NEEDS_HUMAN:init_failed` with the complete secret-free output as evidence, print exactly one bounded, secret-free stdout journal line describing this invocation, and stop the tick. Do **not** call `checkpoint set` and do not otherwise mutate durable state. There is nothing to write to: when initialization failed, no writable checkpoint may exist at all, and any checkpoint that does exist may be the unreadable one that caused the failure. Do not attempt a second initialization in the same tick and do not fall through to Stage 2.\n\n- **`checkpoint_exists` is `true`** \u2014 an epic that already has a checkpoint must **never** trigger `init`. The CLI deliberately refuses reinitialization (`already initialized`), so a retry is not a recovery path; it is a bug in the caller. Skip straight to Stage 2.\n\n- **The status command exits non-zero** (a corrupt or wrong-version checkpoint, for example) \u2014 treat it exactly like a failed init, including the print-only rule: preserve the secret-free stderr as evidence, emit `NEEDS_HUMAN:init_failed`, print one journal line, call no `checkpoint set`, and stop the tick. `status` never rewrites a checkpoint it could not read, so nothing has been damaged.\n\n## Stage 2 \u2014 Reconcile From Status JSON\n\nRun the status probe **again**, with the same conditional `--checkpoint-path '<path>'` forwarding:\n\n```\n<launcher> conduct-epic status '<EPIC>' --json\n```\n\nThis second response is the action snapshot. **This JSON object is the only evidence the tick acts on.** Worker claims are never trusted \u2014 a session that says \"CI passed\", \"review approved\", or \"PR merged\" has told you nothing this tick may use. Every one of those facts is re-derived here from GitHub and the server through `status`, and only from there.\n\nThe top-level contract is exactly: `ok`, `epic_key`, `epic_branch`, `checkpoint_path`, `checkpoint_exists`, `all_done`, `ticket`, `worktree_path`, `worktree_exists`, `branch_head`, `worker_commits_since_spawn`, `last_seen_head`, `last_state_change_at`, `stale_for_seconds`, `pr`, `merged_externally`, `ci`, `review`, `parse`, `deadlines`, `scope`, `lock`, `needs_human`, and `probe_errors`.\n\nThe nested objects the detection table reads are:\n\n- `ticket` \u2014 the in-flight ticket (the first entry that is not `done`, or `null` when `all_done`): `key`, `status` (`pending`, `in_progress`, `merged`, `done`, `needs_human`), `branch`, `pr_number`, `spawned_at`, `parse_requested_at`, `parse_requested_for_sha`, `respawns`, `conflict_attempts`, `counters.sessions_spawned`, `counters.plan_generations_observed`, `counters.merge_attempts`, and `journal`.\n - `parse_requested_at` and `parse_requested_for_sha` are **each a string or `null`**. They are RETAINED for the audit trail of epics driven before the scope-status contract existed, and **no row reads them any more**: Row 5 asks the `scope` sub-object directly instead of reconstructing causality from a request timestamp. Do not write them and do not decide on them.\n - `journal` is the ticket's journal lines, **oldest-first, newest last**, exactly as stored. It is a human-readable audit trail and is **never** the source of a decision: it is capped at 50 lines and evicts oldest-first, so a marker searched for in it would silently vanish after roughly fifty wait ticks and the loop would re-request a parse it had already requested.\n- `pr` \u2014 `number`, `state` (`OPEN`, `MERGED`, `CLOSED`), `head_sha`, `base`, `mergeable`, `merge_state`, `updated_at`.\n- `ci` \u2014 `required`, `complete`, `stable_across_two_polls`, `head_sha`, and `checks` entries of `name`, `status`, `conclusion`, `required`.\n- `review` \u2014 `opted_in`, `source`, `available`, `verdict` (`approved`, `changes_requested`, `unknown`), `head_sha`.\n- `parse` \u2014 `status` (`idle`, `queued`, `in_progress`, `succeeded`, `failed`), `terminal`, `started_at`, and `finished_at`. The last two are each **a string or `null`** and are the ISO-8601 times of the current or last parse run. A `null` on either is unavailable evidence and **never** permits advancement \u2014 in particular, missing timestamps can never satisfy Row 5's causal check. There is no repository-wide index-branch override field: BAPI-847 retired that control plane, and an epic now gets its own index scope instead of taking the repository's index away.\n- `deadlines` \u2014 `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\n- `scope` \u2014 the epic's index scope, read directly from the server: `scope_id`, `lifecycle_state`, `freshness_status`, `blocked_reason`, `required_commit_sha`, `indexed_commit_sha`, and `last_error`. It is `null` **only** when this epic declares no scope at all; that is not a probe failure and carries no `probe_errors` entry.\n - `freshness_status` is one of `fresh`, `pending`, `blocked`, `failed`, `unavailable`. **`fresh` is the only value that means the index covers this epic's merged code.** `pending` is a refresh still running. `blocked` is an epic advance the server REFUSED to index and will never resolve by waiting \u2014 `blocked_reason` names which refusal. `failed` is the scope's own generation failing. `unavailable` means the scope could not be read this tick, and is reported alongside a `{probe: \"scope\"}` entry in `probe_errors`.\n - `required_commit_sha` is the commit the scope must cover; `indexed_commit_sha` is the commit it actually has. **They are separate fields because they mean different things** \u2014 the required SHA moves the moment a merge is accepted, long before anything is indexed, so a required SHA equal to your merge commit is not evidence that your merge was indexed.\n- `lock` \u2014 `held_by_me`, `owner_pid`, `host`, `alive`.\n- `needs_human` \u2014 `null`, or `reason`, `evidence`, `at`.\n- `probe_errors` \u2014 entries of `probe` and `reason`.\n\nA failed probe leaves its sub-object `null` and is listed in `probe_errors`; it never fails the command. **A `null` sub-object is unavailable evidence, not a negative result.** Never infer a merge, an approval, a CI success, or a parse success from a `null` value, from a missing field, or from narrative output of any kind \u2014 an unavailable probe means \"wait for the next tick\", never \"proceed\".\n\n**`pr` is the one sub-object whose `null` has two distinct meanings, and `probe_errors` is what tells them apart:**\n\n- **`pr` is `null` and there is no `{probe: \"pr\"}` entry** \u2014 confirmed absence. `gh` was asked and answered that this branch has no pull request. This is the **normal** state of every tick between the first spawn and the moment the worker opens its pull request, it is a negative result the rows may act on, and Rows 6 and 7 exist precisely for it.\n- **`pr` is `null` and there IS a `{probe: \"pr\"}` entry** \u2014 unavailable evidence. `gh` could not answer: unauthenticated, rate-limited, offline, or output that did not parse. Treat it as \"wait for the next tick\" and never as absence; a pull request that exists but cannot be seen must not be reasoned about as one that does not exist.\n\nDo not collapse these two into \"no PR\". Reading an outage as absence is how the loop would respawn into, or abandon, a pull request that was there all along.\n\nTwo states stop the tick before any action is selected:\n\n- **Already parked.** If `needs_human` is not `null`, print the stable phrase `already parked`, followed by the persisted `reason`, the persisted string `evidence`, and the persisted `at` timestamp \u2014 then stop. Take no action this tick and write no checkpoint. A parked epic is a human's to unpark by editing the checkpoint (`needs_human` back to `null`, the ticket `status` back to `pending`/`in_progress`, counters adjusted if a budget is re-granted). Do not select a new recovery action on top of an existing one.\n- **Foreign lock.** If `lock.held_by_me` is `false` and `lock.alive` is `true`, another live process owns this epic. `foreign_lock` is a **print-only park**: emit `NEEDS_HUMAN:foreign_lock` carrying `lock.owner_pid` and `lock.host` as evidence, print one bounded, secret-free stdout journal line for this invocation, and stop. Do **not** call `checkpoint set`, spawn a session, merge a pull request, or start a parse while that lock is alive. The checkpoint belongs to the other live process; writing to it \u2014 even to record a park \u2014 is the two-authorities corruption the lock exists to prevent, and `checkpoint set` refuses a live foreign lock anyway.\n\n## Stage 3 \u2014 Detect and Take Exactly One Action\n\nEvaluate the rows below **strictly in written order, from top to bottom**. Evaluation stops at the first row whose condition matches; that row's action is the only action this tick performs, and control then proceeds directly to Stage 4. A later row is never \"also\" run because it happens to apply.\n\nOne row states a **forward-looking guard** in its own condition: Row 3 (`stalled`) matches only when no later action or fail-closed row would be selectable for this snapshot. That guard is part of Row 3's condition, not a departure from written order \u2014 the ordering rule still holds, and Row 3 simply does not match while a real action is available.\n\nEach row is marked **fail-open** (an uncertain or transient condition waits for the next tick) or **fail-closed** (the tick refuses to act and parks rather than guessing).\n\n### Row 1 \u2014 `all_done`: finish the epic and open its pull request\n\nWhen `all_done` is `true`, run `<launcher> conduct-epic finish '<EPIC>'` (forwarding `--checkpoint-path '<p>'` when supplied), then call the `create_pull_request` MCP tool with `head_branch` set to `epic/<EPIC>` and `base_branch` set to `main`. Assemble the `body` from the finish summary: the merged ticket pull requests and any skipped tickets. **Open the pull request; never merge it** \u2014 a human reviews and merges the epic into `main`. Then stop.\n\n**This tick writes no checkpoint and does not increment `counters.iterations`.** It is the third documented exemption from Stage 4's one-checkpoint-per-tick rule, and unlike the two print-only parks it reaches Stage 3. The reason is mechanical: `all_done` is `true` exactly when `ticket` is `null`, `checkpoint set` requires `--ticket <KEY>`, and there is no in-flight ticket to name. `finish` is this tick's durable act, and it is the last one the epic needs \u2014 so do not invent a ticket key to satisfy the rule, and do not write a checkpoint before or after `finish`.\n\n### Row 2 \u2014 Wrong base: do not touch a pull request that is not on the epic branch\n\nWhen `pr.base` is present and is not `epic/<EPIC>`, **do not touch the pull request** \u2014 no merge, no comment, no respawn. Select `NEEDS_HUMAN:wrong_base`, carrying the observed `pr.base`, `pr.number`, and the expected `epic/<EPIC>`. **Fail-closed**: only pull requests based on `epic/<EPIC>` are ever acted upon, and this row is evaluated before every work and recovery row precisely so a mis-based pull request cannot be merged, respawned into, or advanced by a later row.\n\n### Row 3 \u2014 Hard liveness: a stalled epic parks before it waits\n\nWhen `stale_for_seconds >= deadlines.hard_seconds` (default three hours, `10800`) **and no other row below is selectable this tick**, select `NEEDS_HUMAN:stalled`, carrying the observed `stale_for_seconds` and the `deadlines.hard_seconds` it exceeded. **Fail-closed**.\n\n**This row outranks wait rows only.** Before selecting it, check whether any of the following would otherwise be selectable for this snapshot; if any one of them would, take that row instead and do not park:\n\n- pending work (Row 4's first spawn),\n- Row 5's **action** branches only \u2014 branch 1's parse request, branch 3's completion, and branch 5's causal `parse_failed` park,\n- a targeted respawn (Rows 7, 9, and 11),\n- CI-red handling (Row 9) and review-remediation handling (Row 11),\n- conflict handling (Row 12),\n- ready-to-merge handling (Row 13),\n- a closed, unmerged pull request (Row 13a).\n\n`stale_for_seconds` counts from the last observed head or status change, not from the last useful event \u2014 so an old but green and approved pull request accumulates staleness while being perfectly actionable. Parking that is the exact defect this guard removes. The row remains ahead of every wait row, because without it a wait would match forever and the epic would sit silent instead of asking for a human.\n\n**Row 5's wait branches are deliberately NOT in that list.** Branches 2, 4, and 6 \u2014 a parse that is queued or in progress, a non-causal `succeeded` or `failed`, an inconsistent request record \u2014 are waits, and exempting them would mean a merged ticket whose parse never starts waits forever with no human ever asked. They accumulate staleness like any other wait and park as `stalled` once `deadlines.hard_seconds` is exceeded.\n\n### Row 4 \u2014 Pending ticket: spawn the first worker\n\nWhen `ticket.status` is `pending`, spawn the ticket's session:\n\n```\n/review-and-start --auto --base-branch 'epic/<EPIC>' <KEY>\n```\n\nThen prepare the Stage 4 checkpoint values `spawned_at` (now, ISO-8601), `status=in_progress`, and `counters.sessions_spawned` = the Stage 2 value plus one.\n\n**Fail-closed**: refuse this spawn if the lock is foreign (Stage 2 has already parked in that case). The pull-request base of the spawned worker comes from BAPI-801's `BAPI_BASE_BRANCH` export \u2014 `/review-and-start --base-branch` forwards it into the spawned worker shell, and the worker's create-PR step resolves the base from it. That export is what makes the first pull request land on `epic/<EPIC>`; this loop never relies on it alone, because Row 2 independently re-checks the observed `pr.base` on every later tick.\n\n### Row 5 \u2014 Merged ticket: refresh the scope index, then mark done\n\nWhen `pr.state` is `MERGED`, or `merged_externally` is `true`, or `ticket.status` is `merged`, the ticket's code is on the epic branch. An **external merge is successful reconciliation, not an error** \u2014 a human who merged the pull request by hand did the loop's work for it, and `merged_externally` records exactly that.\n\n**The evidence this row acts on is `scope`, and only `scope`.** The epic's index scope is refreshed by the server the moment it observes the merge: it advances its own `required_commit_sha` to the merge commit and re-parses incrementally. So the question \"has this merge been indexed?\" is a question the scope can answer directly, and this row asks it instead of reconstructing an answer.\n\nThat is a deliberate replacement of the older mechanism. This row used to record the time it called `parse_repository` and the head SHA it called it for, then compare that timestamp against a repository-wide parse run's `started_at` / `finished_at` \u2014 because `parse.status` is repository-level and stays `succeeded` from any earlier parse of any earlier ticket, so \"succeeded\" alone proved nothing. Timestamp ordering was the only causality available. It is no longer needed, and inference is strictly worse than an answer: **do not call `parse_repository` from this row, and do not read `parse`, `ticket.parse_requested_at`, or `ticket.parse_requested_for_sha` as freshness evidence.** The server owns the refresh; this loop observes it.\n\nThis row is an **ordered state machine**, evaluated top to bottom, and the first matching branch is the tick's action:\n\n1. **`scope` is `null`** \u2014 this epic declares no index scope, so there is nothing to refresh and no freshness to establish. Call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. Journal that the ticket completed with no declared scope. **Fail-open.** An epic that never had a scope must not be blocked by one.\n\n2. **`scope.freshness_status` is `fresh`, and `scope.indexed_commit_sha` equals `scope.required_commit_sha`, both non-null** \u2014 the scope's index provably covers the commit the server is holding it to. Only then call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. Journal both observed watermarks.\n\n **Compare the scope's two watermarks against each other \u2014 never against `pr.head_sha` or `branch_head`.** Both of those are the *worker's* pre-merge branch tip: `pr.head_sha` is `headRefOid`, and `branch_head` is `git ls-remote` of the ticket's own branch. What lands on `epic/<EPIC>` is the merge commit GitHub creates, and that differs from the worker's tip under every merge strategy \u2014 merge, squash, and rebase alike. Comparing an indexed watermark against either one is therefore false essentially always, and a branch that waits on an always-false condition never marks anything done. For the same reason, do not invent a merge-commit field: the `scope` object carries exactly the seven fields named above, and none of them is one.\n\n The identity that IS causal runs between the scope's own two watermarks, and it is what replaces the old timestamp ordering. The server advances `required_commit_sha` the moment it observes this merge, and **only the parse** writes `indexed_commit_sha`; the two fields are owned by different writers precisely so their agreement means something. So `indexed == required` is the server's own statement that it has finished indexing everything it was asked to cover. A scope that finished refreshing for a **previous** ticket reads `fresh` too \u2014 but it reads it at that previous required commit, and the moment this merge is observed `required` moves ahead of `indexed` and `freshness_status` drops to `pending` until the re-parse lands. If either watermark is `null` the comparison cannot be made, so this branch does not match and the tick falls to branch 6 and waits.\n\n **The one gap this cannot see through** is the interval between the merge and the server observing it: in that window the scope still reads `fresh` at the previous ticket's watermark, and no field in the contract tells it apart from this ticket's. It is narrow in practice \u2014 the same merge event that makes `pr.state` read `MERGED` is the one that notifies the server, so a tick that reaches this row has almost always been preceded by that notification \u2014 and it closes on its own. It is not zero: a merge the server never observed at all would leave the watermarks agreeing at the previous commit, and this branch would mark the ticket done against an index that does not contain it. Treat a `done` whose journaled watermarks match the *previous* ticket's as that failure, not as a fresh index.\n\n3. **`scope.freshness_status` is `pending`, `unavailable`, or missing** \u2014 the refresh is still in flight, or the scope could not be read. Wait. Journal the observed `scope.lifecycle_state`, `scope.required_commit_sha`, and `scope.indexed_commit_sha`. Do not spawn anything and do not advance the next ticket. **An unread scope is never a fresh one.**\n\n4. **`scope.freshness_status` is `blocked`** \u2014 the server REFUSED to index this advance, and waiting will never change that. Select `NEEDS_HUMAN:shadow_stale_deadline`, with `scope.blocked_reason` as bounded string evidence, and state plainly in the evidence that **no epic advance was indexed**. **Fail-closed.**\n\n The controlled reasons and what each one means to a human:\n\n - `advance_blocked_base_merge` \u2014 the base branch was merged forward into the epic branch. The epic branch is pinned at its cut point; a base merge would move that pin.\n - `advance_blocked_unexpected_parent` \u2014 the merge commit does not descend directly from the branch head the scope pinned. Something other than a worker pull request landed on the branch.\n - `advance_blocked_history_changed` \u2014 the pinned head is gone from the branch's history. A force-push or rewrite.\n - `advance_blocked_unverifiable` \u2014 the advance could not be verified at all. Doubt blocks; it never indexes.\n\n **This park is immediate, and that is deliberate** \u2014 it is the one place the pilot escalates faster than v2. The v2 reconciler routes a blocked advance through the same `shadow.stale_deadline_seconds` clock it uses for an ordinary refresh hold, because its hold is anchored on a single durable episode timestamp that every hold reason shares. The pilot has no such episode and no typed `RunPolicy` deadline, and none of the four reasons above resolves by waiting, so waiting out a deadline would only delay a human by up to that deadline and change nothing else. Both conductors emit the **same** `shadow_stale_deadline` reason so one grep finds a refused advance either way; only the latency to the park differs. An operator comparing the two should expect the pilot to ask sooner, not to have asked for a different thing.\n\n5. **`scope.freshness_status` is `failed`** \u2014 the scope's own generation failed, which is a different problem from a refused advance. Select `NEEDS_HUMAN:parse_failed`, with `scope.lifecycle_state` and `scope.last_error` as bounded string evidence. **Fail-closed.**\n\n6. **None of branches 1\u20135 matched** \u2014 including a `fresh` scope whose indexed commit still trails its required commit, and a tick where either watermark is missing so no comparison can be made. Wait, and journal the observed scope fields. Neither advance nor park: hard liveness (Row 3) is what eventually escalates a wait that never resolves.\n\n**No next ticket is spawned until this one reaches `done`.** A merged ticket stays in flight until its scope is fresh for its own merge commit, so `ticket` still points at it and Row 4 cannot match for its successor \u2014 which is the whole point: the next ticket's review and plan must see this ticket's merged code.\n\n### Row 6 \u2014 Worktree working: wait\n\nWhen a worktree exists (`worktree_exists` is `true`), the pull request is **confirmed absent** (`pr` is `null` **and** `probe_errors` carries no `{probe: \"pr\"}` entry), and `worker_commits_since_spawn > 0`, the worker is making observable progress. Wait, and journal the observed `branch_head` and commit count. **Fail-open.**\n\nA `pr: null` accompanied by a PR probe error is unavailable evidence, not absence, and does not match this row \u2014 it falls through to Row 15 and waits.\n\n### Row 7 \u2014 Soft deadline with no progress: one targeted continuation\n\nWhen the pull request is **confirmed absent** (`pr` is `null` **and** no `{probe: \"pr\"}` entry), `worker_commits_since_spawn` is `0`, and `deadlines.elapsed_since_spawn_seconds >= deadlines.soft_seconds` (default one hour, `3600`), spend the single targeted respawn on kind `continue`, with the prompt:\n\n```\nBranch <b> for <KEY>: continue the existing plan; do not regenerate it; push when done\n```\n\nPrepare `respawns` = the Stage 2 value plus one. `respawns` is **one shared per-ticket budget**, not one allowance per row: Rows 7, 9, and 11 all spend the same single counter, so spending it here leaves nothing for a later CI fix or review fix on this ticket. The attempt **counts only if it pushed** \u2014 a later tick observing a non-null `branch_head` is the proof. A respawn that produces no push is a no-op, and a no-op respawn stops the loop rather than spinning: once the one targeted respawn is spent and the ticket still shows no pushed head, select `NEEDS_HUMAN:stalled`. **Fail-closed after one attempt**, which is what keeps a dead worker from being respawned without bound.\n\n### Row 8 \u2014 Pull request open, CI not settled: wait\n\nWhen a pull request is open and `ci.complete` is `false` **and no required check in `ci.checks` has already reached a terminal unsuccessful conclusion**, wait; or when `ci.complete` is `true` and green but `ci.stable_across_two_polls` is `false`, wait. **Fail-open.**\n\nThe boolean alone is not the condition. `ci.complete` is `false` both while checks are still running and once a required check has definitively failed, and those are opposite situations: the first is worth waiting on and the second never becomes green on its own. This row therefore covers pending and not-yet-stable checks **only** \u2014 a required check with a terminal unsuccessful conclusion is **not** consumed here and falls through to Row 9.\n\n### Row 9 \u2014 Pull request open, CI red: one targeted fix\n\nWhen a pull request is open, one or more required checks in `ci.checks` have a terminal unsuccessful conclusion, and there has been no new commit for over 60 minutes (`stale_for_seconds > 3600` is the authoritative no-new-commit duration), spend the single targeted respawn on kind `ci_fix`. Take the failing check names from `ci.checks` \u2014 the entries whose `required` is `true` \u2014 and use the prompt:\n\n```\nPR #N is red on <checks>: read the check annotations, fix, push; do not regenerate the plan\n```\n\nPrepare `respawns` = the Stage 2 value plus one; the attempt counts only if it pushed. A bare `/implement-ticket --auto` is **prohibited** here: it regenerates the plan, costs a full plan generation, and discards the failure detail the annotations already carry.\n\n`respawns` is **one shared per-ticket budget** across Rows 7, 9, and 11. A continuation respawn spent earlier on this ticket therefore leaves **no** CI-fix attempt: with the counter already at its limit, persistent red CI parks immediately as `NEEDS_HUMAN:ci_red` rather than getting a fix session of its own. Once the shared respawn is spent and CI is still red, select `NEEDS_HUMAN:ci_red` with the failing check names as bounded string evidence. **Fail-closed after one attempt.**\n\n### Row 10 \u2014 Review opted in but unavailable: wait one tick\n\nWhen `pr.state` is `OPEN`, `review.opted_in` is `true`, and `review.available` is `false`, the review source could not be read. Wait one tick and journal the condition. **Fail-open** for that tick \u2014 but the clock keeps running, so continued unavailability is caught by Row 3's hard-liveness park rather than waiting forever.\n\nThe `pr.state` is `OPEN` guard is load-bearing: without it a `CLOSED` pull request whose review source happens to be unreadable matches here, ahead of Row 13a, and the loop waits tick after tick on abandoned work instead of parking it.\n\n### Row 11 \u2014 Changes requested for the current head: one targeted review fix\n\nWhen `pr.state` is `OPEN`, `review.verdict` is `changes_requested`, **and** `review.head_sha` equals `pr.head_sha`, spend the single targeted respawn on kind `review_fix`. The `OPEN` guard is what stops a `changes_requested` verdict left on a **closed** pull request's head from spending this ticket's one respawn on work nobody will merge \u2014 that snapshot belongs to Row 13a. The prompt carries the authoritative Stage 2 review evidence: the ticket key, the pull-request number, the reviewed head SHA, and the requested changes. A stale `review.head_sha` (one that does not equal `pr.head_sha`) is a verdict about code that no longer exists and never triggers this row. Prepare `respawns` = the Stage 2 value plus one.\n\n`respawns` is **one shared per-ticket budget** across Rows 7, 9, and 11. Any earlier continuation or CI-fix respawn on this ticket therefore leaves **no** review-fix attempt: with the counter already at its limit, requested changes on the current head park immediately. Once the shared respawn is spent and the verdict still stands for the current head, select `NEEDS_HUMAN:review_changes_requested`. **Fail-closed after one attempt.**\n\n### Row 12 \u2014 Conflicting pull request: at most two conflict sessions\n\nWhen `pr.state` is `OPEN` **and** either `pr.mergeable` is `CONFLICTING` or `pr.merge_state` is `DIRTY`, spawn a session of kind `conflict` with the prompt:\n\n```\nrebase onto origin/epic/<EPIC>, resolve, run tests, push\n```\n\nPrepare `conflict_attempts` = the Stage 2 value plus one. The conflict budget is **two** sessions and is counted separately from the single targeted respawn of Rows 7, 9, and 11 \u2014 a rebase is a different failure mode from a stalled or red worker. After the second conflict session, if the pull request is still `CONFLICTING`/`DIRTY`, select `NEEDS_HUMAN:conflict`. **Fail-closed after two attempts.**\n\nA **closed** pull request is frequently left `CONFLICTING`/`DIRTY` by GitHub, so without the `pr.state` is `OPEN` guard this row would match ahead of Row 13a and spend a rebase session resolving conflicts on a branch nobody will merge.\n\n### Row 13 \u2014 Ready to merge\n\nMerge only when **all** of the following hold on the fresh Stage 2 snapshot: the pull request is open (`pr.state` is `OPEN`); `pr.base` is `epic/<EPIC>`; `ci.complete` is `true` and `ci.stable_across_two_polls` is `true` for `ci.head_sha` equal to `pr.head_sha`; the pull request is not conflicting; and review is either opted out (`review.opted_in` is `false`) or approved (`review.verdict` is `approved`) with `review.head_sha` equal to `pr.head_sha`.\n\nThen call the `merge_pull_request` MCP tool with exactly `pr_number` set to `pr.number` and `expected_head_sha` set to `pr.head_sha`. **The expected SHA is derived only from the fresh Stage 2 status** \u2014 never from the checkpoint, never from a worker's report, never from an earlier tick. The checkpoint deliberately stores no expected head; merge identity always comes from a freshly observed `pr.head_sha`. Prepare `counters.merge_attempts` = the Stage 2 value plus one for **every** invocation of the tool, successful or not.\n\nMap the returned envelope:\n\n- **`merged` is `true`** \u2014 the only success. It covers `outcome: merged` and `outcome: already_merged`, both of which carry that boolean. Prepare `status=merged` and top-level `counters.merges` = the Stage 2 value plus one.\n- **`outcome: refused` with `reason: head_sha_drift`** \u2014 the head moved under the merge. Journal the complete envelope (including `actual_head_sha`) and take a fresh status snapshot on the next tick. Never retry with the stale SHA.\n- **Outcome `lease_held`, `review_not_approved`, or `unknown`, or any envelope carrying `retry_hint: retry_later`** \u2014 journal it and wait for the next reconciliation tick.\n- **Outcome `dry_run`, `pending_approval`, `gate_unresolved`, `action_key_mismatch`, `review_unavailable`, `review_source_unsupported`, `error`, or any `refused` result carrying `retry_hint: needs_human`** \u2014 select `NEEDS_HUMAN:merge_blocked`. Preserve the **complete** envelope as the evidence, including `hint`, `actual_head_sha`, `ci_summary`, `paths`, and `http_status` whenever those are present; `hint` is usually the exact operator fix. **JSON-stringify that envelope into a bounded, secret-free string** \u2014 `evidence` is string data, never an object (see Stage 4).\n\n**Fail-closed**: only `merged: true` is success. A missing, `false`, or malformed `merged` value is never treated as a merge, no matter what `outcome` says alongside it.\n\n### Row 13a \u2014 Pull request closed without being merged\n\nWhen `pr.state` is `CLOSED` and the pull request was not merged, the ticket's work has been abandoned on GitHub and nothing this loop does can advance it. Select `NEEDS_HUMAN:merge_blocked`, with bounded string evidence that identifies `pr.state: CLOSED` along with `pr.number`. **Fail-closed** \u2014 a closed pull request is never respawned into, reopened, or merged by this loop.\n\n### Row 14 \u2014 Local-mode ticket operation refused\n\nWhen a ticket operation returns `409 UNSUPPORTED_IN_LOCAL_MODE`, tolerate it and journal it. The repository is running the local ticket backend, where that response is the documented terminal answer rather than a failure. It introduces **no** new parking reason. **Fail-open.**\n\n### Row 15 \u2014 No row matched: journal the snapshot and do nothing else\n\nWhen no row above matches, that is the tick's outcome, not a licence to improvise. Journal a concise summary of the Stage 2 snapshot, take **no** external action \u2014 no MCP tool call, no spawn, no merge, no parse \u2014 and change **no** row-specific checkpoint field. The single `checkpoint set` this tick writes therefore carries only the universal `counters.iterations` update and its one journal line.\n\nThis row exists because unmatched snapshots are real and reachable: a pull request that is open, complete, stable, and awaiting a review whose `verdict` is still `unknown`; a `stale_for_seconds` or `elapsed_since_spawn_seconds` that is `null` because nothing has been observed yet. Each of those is a legitimate \"wait for reality to move\" state, and a tick that improvised an action for it would be acting on evidence it does not have. **Fail-open.**\n\n### Shared mechanics for every targeted session\n\nRows 7, 9, 11, and 12 spawn a session the same way. The four kinds are exactly `continue`, `ci_fix`, `review_fix`, and `conflict`.\n\n**First, write the prompt file** with the Write tool, at:\n\n```\n~/.config/bridge/conduct/<repo>/<EPIC>/prompts/<KEY>-<kind>-<n>.md\n```\n\nwhere `<EPIC>` and `<KEY>` are the validated keys, `<kind>` is one of the four kinds above, and `<n>` is the applicable absolute attempt number. **`<repo>` is the repository component of the resolved `checkpoint_path` that Stage 2's `status` returned** \u2014 read it from there rather than re-deriving it from credentials, from `BAPI_REPO_NAME`, or from anything remembered in conversation. `status` resolves that path itself, including any `--checkpoint-path` override and any `XDG_CONFIG_HOME` redirection, so it is the only value guaranteed to match where the CLI actually keeps this epic's state.\n\n**End every prompt with this exact wording**, so the spawned worker releases its worktree cleanly instead of lingering:\n\n```\nAfter the final pipeline step completes, cleanly end your worker session (for example by issuing the `/exit` command) so the worktree is released and no idle process lingers \u2014 but **only when no follow-up remains that you still own**. Do **not** exit while any of the following is true:\n\n- there are unresolved CI failures you are still correcting (the post-PR CI-correction loop in the CI-monitoring step still owns work),\n- review changes were requested and you have not yet addressed them,\n- there is a merge conflict on your PR that you still own,\n- you have unpushed local commits.\n\nExit only after your final branch state is pushed, the done-gate / CI-monitoring workflow required by the recipe has completed, and no CI/review follow-up remains. A clean `SessionEnd` is both the correct terminal lifecycle signal and the point at which the worker should exit.\n```\n\n**Then spawn**, forwarding `--checkpoint-path '<p>'` only when supplied:\n\n```\n<launcher> conduct-epic spawn '<EPIC>' --ticket '<KEY>' --prompt-file '<path>'\n```\n\n`spawn` opens exactly **one** agent tab in the ticket's `worktree_path` running the prompt file's contents. It refuses when the worktree is missing, the prompt file is unreadable, or the lock is held by another live process.\n\n**The budgets are this command's job, not the CLI's.** `spawn` never checks them: it will happily open a fifth tab if asked. One targeted respawn **shared** across Rows 7, 9, and 11 \u2014 a single per-ticket `respawns` counter, not one allowance per row \u2014 and two conflict sessions in Row 12, are enforced here, by reading the Stage 2 `respawns` and `conflict_attempts` before choosing the row.\n\nAfter a **successful** spawn, prepare `counters.sessions_spawned` = the Stage 2 value plus one. If the spawn command itself fails, do **not** advance `respawns`, `conflict_attempts`, or `counters.sessions_spawned` \u2014 a session that never opened has consumed no budget.\n\nKeep credentials, raw environment values, and unrelated command output out of prompt files and out of the spawn command's arguments. The spawned agent resolves its own credentials.\n\n## Stage 4 \u2014 Checkpoint and Stop\n\nEvery tick that reaches this stage ends with **exactly one** checkpoint command and **exactly one** journal line. There are **three exemptions**, and they divide into two kinds:\n\n- **Two print-only parks, before Stage 3.** `init_failed` (Stage 1) and `foreign_lock` (Stage 2) stop the tick *before* Stage 3 and write nothing durable at all \u2014 they print their `NEEDS_HUMAN:` line and one stdout journal line and stop. Because they never reach Stage 3 they also never increment `counters.iterations`.\n- **The `all_done` tick, inside Stage 3.** Row 1 reaches Stage 3 but has **no in-flight ticket**: `all_done` is `true` exactly when `ticket` is `null`, and `checkpoint set` requires `--ticket <KEY>`. That tick runs `finish`, opens the epic pull request, writes **no** checkpoint, and \u2014 as the single stated exception to the rule below \u2014 does **not** increment `counters.iterations`.\n\nEvery other tick, including a Row 15 fallthrough, writes here. Run, forwarding `--checkpoint-path '<p>'` whenever the user supplied it:\n\n```\n<launcher> conduct-epic checkpoint set '<EPIC>' --ticket '<KEY>' --field <name> <absolute-value> \u2026 --journal '<line>'\n```\n\nRepeat `--field <name> <absolute-value>` once per changed field, and pass `--journal '<line>'` exactly once. Do not issue a second `checkpoint set` in the same tick, and do not split the fields across two invocations \u2014 one tick, one auditable write.\n\n**Every value is absolute, computed from the Stage 2 snapshot.** Relative or guessed increments are prohibited: the CLI stores what it is given, so a \"+1\" that was never resolved against a fresh read silently corrupts the count. Compute `n + 1` from the Stage 2 value for `counters.sessions_spawned`, `respawns`, `conflict_attempts`, `counters.merge_attempts`, `counters.iterations`, and `counters.merges`.\n\nInclude only the fields the selected row actually affected \u2014 typically some of `status`, `spawned_at`, `respawns`, `conflict_attempts`, `counters.sessions_spawned`, `counters.merge_attempts`, `counters.iterations`, and `counters.merges`.\n\n**`parse_requested_at` and `parse_requested_for_sha` are no longer written by any row.** The CLI still accepts them so an older checkpoint stays readable, but Row 5 now reads the `scope` sub-object \u2014 the server's own answer about whether this merge was indexed \u2014 rather than recording a request and timing it. Writing them would record evidence nothing reads.\n\n**`counters.iterations` increments exactly once for every tick that reaches Stage 3**, and it is written in that tick's single `checkpoint set` as the Stage 2 absolute value plus one. It is the one field every such tick updates, including a Row 15 fallthrough \u2014 which is why a fallthrough tick's checkpoint contains only `counters.iterations` and its journal line, with no status, retry, merge, or parking mutation. The two print-only parks never reach Stage 3 and so never increment it, and the `all_done` tick reaches Stage 3 but writes no checkpoint, so it does not increment it either.\n\n**Parking** adds two fields to the same single command:\n\n```\n--field status needs_human --field needs_human '{\"reason\":\"<reason>\",\"evidence\":\"<bounded secret-free JSON-stringified envelope or output>\",\"at\":\"<ISO-8601 timestamp>\"}'\n```\n\n**`evidence` is a JSON string, never an object.** The CLI's checkpoint schema accepts only `{reason: string, evidence: string, at: string}` and rejects anything else outright, so an object-valued `evidence` makes `checkpoint set` exit non-zero: the `NEEDS_HUMAN:` line prints, the park never persists, and the next tick repeats the failing action. When the evidence is structured \u2014 a merge envelope, a command's output \u2014 JSON-stringify it and escape every embedded quote and control character so the result is a single valid JSON string value. Keep it bounded and secret-free.\n\nThe `reason` is one of the closed list below and `at` is an ISO-8601 timestamp. Every `NEEDS_HUMAN:<reason>` line printed by a stage carries the **same** evidence that is persisted here \u2014 the printed line and the checkpoint never disagree.\n\nThe parking vocabulary is closed, and it has two partitions:\n\n- **Eight persisted reasons**, each written durably by the single `checkpoint set` above: `stalled`, `ci_red`, `review_changes_requested`, `merge_blocked`, `conflict`, `parse_failed`, `shadow_stale_deadline`, and `wrong_base`. A persisted park is what makes the *next* tick report `already parked` and stop.\n - `shadow_stale_deadline` is Row 5 branch 4's reason, and it is deliberately **the same token the v2 conductor parks under** for the same condition. Both conductors reaching for one string is what lets an operator grep for a refused epic advance without first working out which conductor drove the epic. It is distinct from `parse_failed`: `parse_failed` means the index generation broke, while `shadow_stale_deadline` means the index refused to accept the branch advance at all.\n- **Two print-only reasons**, which are printed and journaled to stdout for the current invocation only and write nothing durable: `init_failed` and `foreign_lock`. Neither may call `checkpoint set`. A print-only park leaves no durable record, so it does not produce an `already parked` tick \u2014 the next tick reconciles from scratch and reports the condition again if it persists.\n\nDo not invent a new reason; a genuinely new failure mode is a change to this command and to the BAPI-805 runbook together.\n\nThe journal line is one line containing the ISO-8601 time, the selected action, and concise evidence. Print it **last**, after the checkpoint command has succeeded, so the operator's final line of output is the tick's durable record.\n\nEvery dynamic value in this stage follows the Stage 0 single-quote rule \u2014 the epic key, the ticket key, the checkpoint path, the `needs_human` JSON, and the journal line are each escaped (`'` \u2192 `'\\''`) and wrapped in single quotes. Credentials never appear in a checkpoint argument or in journal evidence.\n\n## Operational Guarantees\n\n- **Spec freshness is `/review-and-start`'s job, not a separate check.** Each ticket's review phase runs in a worktree cut from the current `epic/<EPIC>` tip, so its review and its plan already see every predecessor's merged code. This command runs no separate spec-freshness check and needs none.\n- **The checkpoint plus GitHub are the resume point.** Nothing relies on conversation memory. A sleeping laptop merely misses ticks; the next invocation reconciles from scratch and continues where reality actually is.\n- **This command never creates an `epic_run`.** It must never be combined with `setup-epic` on the same epic \u2014 the v2 conductor stays active there, and two authorities transitioning one epic is exactly the failure this pivot removes.\n- **`/loop 5m /conduct-epic <EPIC>` is the driver.** The operator runbook is BAPI-805's, not this file's.\n- **Recovery is bounded**: one targeted respawn *shared* across Rows 7, 9, and 11, and two conflict sessions, then park. There is no third chance and no escalating retry.\n- **The first spawn relies on BAPI-801's `BAPI_BASE_BRANCH` contract**, while every tick still independently verifies the observed `pr.base` (Row 2). The export makes the right thing happen; the check catches it when it does not.\n","council.md":'Convene a multi-perspective council on a task via Bridge API and save the resulting report locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\nParse `$ARGUMENTS`. The supported invocation is exactly:\n\n```text\n/council <task description> [--mode technical|design|discovery|general] [--debate] [--lenses a,b] [--ticket PROJ-123]\n```\n\nParsing rules:\n\n- Keep every non-flag token in its original order; the joined result is the required `task_description`. Remove each recognized flag, and the value token that belongs to it, from that text.\n- `--mode <value>` accepts exactly `technical`, `design`, `discovery`, or `general`. When `--mode` is omitted, the selected mode is `technical`.\n- `--debate` is a valueless boolean flag. It takes no following token.\n- `--lenses <a,b>` takes one comma-separated value. Split it on commas and keep the non-empty entries as the `lenses` array.\n- `--ticket <KEY>` captures the immediately following token as the ticket key.\n- A missing value for `--mode`, `--lenses`, or `--ticket` \u2014 including a value position occupied by another recognized flag \u2014 is a validation failure. Never let the next flag become a flag\'s value.\n\nValidation must finish before any MCP tool call. Stop immediately, display the usage response below, and make no tool call when `$ARGUMENTS` is empty, when it contains only flags, when a flag that needs a value has none, or when `--mode` is given an unsupported value:\n\n```text\nUsage: /council <task description> [--mode technical|design|discovery|general] [--debate] [--lenses a,b] [--ticket PROJ-123]\nExample: /council "How should we add rate limiting to the LLM client?" --mode technical\n```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall `get_docs_dir` (no parameters). Store the returned path as `docs_dir`. This is context only \u2014 do not slugify it, predict a filename from it, or otherwise construct a report path yourself.\n\n## Step 3 \u2014 Convene the Council\n\nBefore calling the tool, tell the user calmly what to expect:\n\n```text\nConvening the council. This commonly takes around 15 minutes, and may continue in the background if the client deadline expires.\n```\n\nThen call `request_council` with:\n\n- `task_description`: the parsed task text\n- `mode`: the selected mode\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `debate`: `true` \u2014 include this parameter **only** when `--debate` was supplied\n- `lenses`: the parsed array \u2014 include this parameter **only** when `--lenses` was supplied\n- `ticket_number`: the captured key \u2014 include this parameter **only** when `--ticket` was supplied\n\nOmit an optional parameter entirely rather than sending a placeholder: never send `debate` with a false value, never send an empty `lenses` array, and never send an empty `ticket_number` string. Do not send any other parameter \u2014 no `providers`, no `concerns`, no prior `brainstorm_id` to refine, and no lens pair of your own. Omitted `lenses` already defaults server-side; do not re-implement that default here.\n\n## Step 4 \u2014 Report the Outcome\n\nKeep the report status-first and compact: status, then the next action, then supporting detail such as the saved path, `brainstorm_id`, or mode.\n\n**Completed.** The tool appends a `Saved files:` block listing one `- <path>` line per saved report. Collect those lines as `saved_paths`; each entry is a `saved_path` reported by the tool. Display them before any optional task, mode, or `docs_dir` context, and never invent or predict a filename:\n\n```text\nCouncil complete.\nSaved to: {saved_path}\n```\n\n**Backgrounded.** A response that exceeded the client deadline but carries a `brainstorm_id` is a successful submission, not a failure. Do not display "failed", an error banner, or unrecoverable-error wording for it. Display the exact returned id and the recovery action:\n\n```text\nCouncil submitted and still running in the background.\nRetrieve it with `get_council` using {"brainstorm_id": "<the exact id returned>", "save_locally": true}.\n```\n\n**Not indexed.** When a `technical` or `discovery` request reports that the repository is not indexed, say so and name the workaround \u2014 those two modes are codebase-grounded and need an indexed repository, while `general` needs no index:\n\n```text\nThis repository is not indexed, and {mode} mode needs an indexed repository.\nRerun the same task with `--mode general`.\n```\n\n**Failed.** A tool error that carries no `brainstorm_id` is a genuine failure. Surface the tool\'s own actionable message, stop, and do not invent a retrieval handle:\n\n```text\nCouncil failed: <error message from the tool>\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```text\n## Council Report\n\n- **Saved to**: {saved_path}\n- **Task**: <task_description>\n- **Mode**: <selected mode>\n- **Status**: Completed\n```\n\nFor a backgrounded council, replace the saved-path line with the returned `brainstorm_id` and the `get_council` recovery action, and set the status to `Submitted \u2014 running in the background`.\n',"create-doc.md":'Generate a design document (TDD, FSD, or PRD) for a Jira ticket, wait for the result, and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, a required `--doc-type` flag, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--doc-type` appears followed by one of `tdd`, `fsd`, or `prd`, capture that as `doc_type`.\n - If `--doc-type` is absent, or is followed by anything other than `tdd`/`fsd`/`prd` (or is the last token), stop immediately and report: "Usage error: --doc-type requires a document type (tdd, fsd, or prd)."\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate ticket key format**: Confirm the ticket key matches the Jira key pattern `[A-Za-z][A-Za-z0-9]+-\\d+`. If it does not match (or `ticket_key` is empty or missing), stop immediately and display:\n\n ```\n Usage: /create-doc <ticket_key> --doc-type <tdd|fsd|prd> [--second-opinion [provider]] [--provider <name>] (e.g., /create-doc BAPI-150 --doc-type fsd)\n ```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Design Document\n\nCall the `create_doc` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `doc_type`: the parsed `doc_type` (`tdd`, `fsd`, or `prd`)\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nThis step may take 2-4 minutes while the backend processes the document.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nDesign document generation failed: <error message from the tool>\n```\n\nIf generation did not finish, the document can be retrieved later with the `get_doc` MCP tool using the same `ticket_number` and `doc_type`.\n\n## Step 4 \u2014 Confirm Success\n\nResolve the local file path from `doc_type`:\n- `tdd` \u2192 `{docs_dir}/architecture/<ticket_key>-architecture-plan.md`\n- `fsd` \u2192 `{docs_dir}/fsd/<ticket_key>-fsd-plan.md`\n- `prd` \u2192 `{docs_dir}/prd/<ticket_key>-prd-plan.md`\n\nDisplay a confirmation message:\n\n```\nDesign document generated successfully for <ticket_key>\nSaved to: <local file path>\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Design Document Report\n\n- **Ticket**: <ticket_key>\n- **Doc Type**: <doc_type>\n- **Status**: Generated successfully\n- **Local File**: <local file path>\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n',"create-pr.md":'# Create PR: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), fetches the ticket summary, determines the base branch, and creates a pull request on the configured VCS provider. It is designed to run after `/commit-ticket` completes.\n\nIf any critical stage fails (Stage 0), stop immediately and report which stage failed and why. Non-critical stages (Stage 1 and Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 3-stage pipeline to create a pull request for a Jira ticket. Execute all stages in sequence.\n\n## Stage 0 \u2014 Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` \u2014 one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: \'<value>\'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /create-pr <ticket_key> (e.g., /create-pr BAPI-150)\n ```\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\n3. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `head_branch`. Verify that `head_branch` contains the `ticket_key` (case-insensitive comparison). If the branch does not contain the ticket key, stop immediately and display:\n\n ```\n Current branch \'<head_branch>\' does not contain ticket key <ticket_key>.\n Please switch to the correct feature branch before running /create-pr.\n ```\n\n4. **Resolve base branch**: Resolve the base through this ordered precedence and take the first tier that yields a usable value.\n\n 1. **`BAPI_BASE_BRANCH` from the environment, when set and non-empty.** Read it first, explicitly, with Bash \u2014 never infer the base from branch ancestry or the repository default branch:\n\n ```bash\n echo "${BAPI_BASE_BRANCH:-}"\n ```\n\n The `:-` form returns an empty line when the variable is unset, so the read never fails the stage. The packaged `start-tickets` exports this variable into a worker\'s shell for **every** resolved run base \u2014 the ordinary `main` case included, not only an epic branch \u2014 so under a packaged spawn this tier always wins over the repository-wide configured value.\n 2. **The repository\'s configured base branch** \u2014 only when the environment value is unset. Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch`.\n 3. **`main`** \u2014 the expected fallback default.\n\n Tiers 2 and 3 exist for a workflow where the environment contract is genuinely absent: `/create-pr` invoked by hand, or a legacy worker started outside packaged `start-tickets`. They are not the normal packaged-worker path \u2014 a packaged worker always arrives with `BAPI_BASE_BRANCH` set.\n\n Treat a null, empty, or whitespace-only value, an HTTP 400 Validation Error / Invalid field name, or any lookup error as not set, and fall back to `main` rather than failing the stage. Store the resolved value as `base_branch`.\n\n5. **Fetch ticket summary**: Call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key`. Extract the ticket summary from the response. If the tool returns an error, log a warning and use a generic summary based on the ticket key.\n\n6. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Create Pull Request\n\n1. **Compose PR title**: Format the title as `<ticket_key>: <ticket_summary>`. Truncate to 72 characters if needed.\n\n2. **Compose PR body**: Build a PR body that includes, in this order:\n - A brief description derived from the ticket summary\n - A plain text reference to the local implementation plan: `Implementation Plan available locally at {docs_dir}/plans/{ticket_key}-plan.md` (do not use markdown hyperlink syntax \u2014 the local path is sufficient for team members pulling the branch)\n - The checklist text of `.github/PULL_REQUEST_TEMPLATE.md`, read from the current worktree when that file exists and appended after the plan reference without rewriting its markdown structure. Omit this part when the file is absent. GitHub\'s REST API does not automatically apply the repository pull request template \u2014 it is a web-UI affordance \u2014 so the checklist must be inlined into the body here or the created PR has none.\n\n3. **Create the pull request**: Call the `create_pull_request` MCP tool with:\n - `head_branch`: the current branch from Stage 0\n - `base_branch`: the resolved base branch from Stage 0\n - `title`: the composed PR title\n - `body`: the composed PR body\n\n4. **Handle the response with graceful degradation**:\n - If the response contains `available: false`: Report the reason to the user and skip to Stage 2. Do not halt the pipeline.\n - If the response contains `created: false`: Log "PR already exists" and store the returned PR URL. Continue to Stage 2.\n - If the response contains `created: true`: Store the PR URL. Continue to Stage 2.\n - If an HTTP error occurs: Warn the user with the error details and continue to Stage 2. Do not halt the pipeline.\n\nThis stage is **non-critical** \u2014 warn on failure, continue to Stage 2 regardless.\n\n## Stage 2 \u2014 Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Pull Request Report\n\n**Ticket**: <ticket_key>\n**Branch**: <head_branch>\n**Base Branch**: <base_branch>\n**PR URL**: <pr_url or "N/A \u2014 see warnings">\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 1: PR creation failed or unavailable),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** \u2014 display the report regardless.\n\n## Final Report\n\nOn success, display the structured report from Stage 2 confirming that the pull request was created (or already existed), including the branch name, base branch, PR URL, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0), display which stage failed and the error details.\n',"critique-ticket.md":'Generate a ticket quality critique and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command triggers an AI-powered critique of a Jira ticket and saves the result locally. **No human confirmation gates** \u2014 the command runs end-to-end without pausing. `$ARGUMENTS` should contain a single Jira ticket key in `PROJECT-NUMBER` format (e.g., `BAPI-123`).\n\nIf any step fails, stop immediately and report which step failed and why.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate the ticket key format**: Validate that `ticket_key` matches the regex pattern `^[A-Za-z][A-Za-z0-9]+-\\d+$`. If validation fails, stop immediately and report: "The argument does not match the expected `PROJECT-NUMBER` format. Example: `BAPI-123`."\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Critique\n\nCall the `request_ticket_critique` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nIf the tool returns an error, stop immediately and report: "Critique generation failed." Include the error details.\n\n## Final Report\n\n**On success**, display a summary including:\n\n- Path to the saved critique document: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nNote: The critique was NOT pushed to Jira. To incorporate the critique findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n\n**On failure at any step**, stop immediately and display the step that failed and the error details.\n',"decision-page.md":'Turn open decisions from this conversation into an interactive HTML decision page, then fold the answers back in.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is a free-form description of what needs deciding \u2014 a topic ("how we handle rate limiting"), a list of specific questions, or empty. It is **not** a Jira ticket key.\n\nThis command exists so a decision page can be reached in ordinary conversation, without running a larger automation. A decision page frames each open choice as a card \u2014 the question, why it matters, 2-4 concrete options with the consequence of each, and a recommendation \u2014 and renders it as a local HTML page the user submits from their browser. The submitted JSON comes back to you and the decisions become settled.\n\nUse it whenever a conversation has accumulated more open choices than are comfortable to settle in prose. Do not use it to ask one simple question \u2014 ask that directly.\n\nRun every stage in the main conversation so the user sees the framing as it happens. If a stage fails, say which one and why.\n\n## Stage 1 \u2014 Frame the decisions\n\n1. **Gather the candidates.** Take the decisions named in `$ARGUMENTS` plus any open choice raised earlier in this conversation and not yet settled. If `$ARGUMENTS` is empty, use the conversation alone. If you find nothing genuinely open, say so and stop \u2014 do not manufacture cards to fill a page.\n\n2. **Write one card per decision.** Each card needs:\n - `id`: a short stable id, e.g. `D-1`, `D-2`. Ids must be unique \u2014 a duplicate is rejected, because the id is the key the user\'s answer is reported under.\n - `question`: the decision itself, phrased as a question.\n - `options`: 2-4 concrete option labels. Do **not** include "None of these" or "Ask about this" \u2014 the renderer appends both automatically, and passing "None of these" yourself is rejected.\n - `option_consequences`: one consequence per option, **parallel to and the same length as** `options`. Say what actually follows from choosing it, not a restatement of the label.\n - `why_it_matters`: the concrete impact of getting this wrong.\n - `recommendation_explanation`: why the recommended option is best.\n - `recommendation_index`: the 0-based index of the recommended option, within range of `options`.\n - `codebase_evidence` (optional): your assessment plus `file:line` citations, shown collapsed behind a disclosure.\n\n Give a real recommendation on every card. If one option is obviously right, still supply the strongest alternative as a second option so the user can see what they are ruling out.\n\n3. **Show the list and let the user correct it.** Present the questions and options in chat before rendering anything. The user may add a decision you missed, drop one that is already settled, or reject your framing of a question. Apply their corrections, then proceed. This check is cheap; a page built on the wrong questions is not.\n\n## Stage 2 \u2014 Render the page\n\n1. **Pick a slug.** Derive a kebab-case slug from the topic \u2014 a few meaningful words, lowercase, non-alphanumerics stripped, at most 60 characters. It **must** match `/^[A-Za-z][A-Za-z0-9_-]*$/`; if it would start with a digit or hyphen, prefix it with `decisions-`. This slug is the `ticket_key`, which accepts any such slug and does not have to be a Jira key.\n\n2. **Call `generate_decision_page`** with the routing fields at the root and everything else nested under `content`. **The nesting is required** \u2014 `actionable_items`, `system_goals`, `clear_improvements`, and `implementation_order` passed at the root are silently dropped by the tool\'s lean input schema, and a call with no `content` at all is rejected.\n - `ticket_key`: the slug.\n - `artifact_type`: `review_decisions` (the default).\n - `output_subdir`: `decisions`.\n - `output_filename`: `{slug}-decisions.html`.\n - `labels`: optional presentation overrides \u2014 `title`, `intro`, `section_heading`. Set a `title` that names the topic, and an `intro` that says what agreeing to these choices commits the user to.\n - `content`: an object holding `actionable_items`.\n\n ```typescript\n interface DecisionPageContent {\n actionable_items: Array<{\n id: string; // e.g. "D-1"; must be unique\n question: string;\n why_it_matters: string;\n recommendation_explanation: string;\n options: string[]; // 2-4 labels (no "None of these" / "Ask about this")\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based within options\n codebase_evidence?: string; // optional: assessment + file:line citations\n }>;\n }\n ```\n\n Example call:\n ```json\n {\n "ticket_key": "rate-limiting",\n "artifact_type": "review_decisions",\n "output_subdir": "decisions",\n "output_filename": "rate-limiting-decisions.html",\n "labels": { "title": "Rate Limiting Decisions", "section_heading": "Open Decisions" },\n "content": {\n "actionable_items": [\n {\n "id": "D-1",\n "question": "Where should the limit be enforced?",\n "why_it_matters": "Determines whether a burst is rejected before or after it reaches the database.",\n "recommendation_explanation": "Middleware keeps the limit in one place and protects every route without per-handler work.",\n "options": ["In middleware", "Per handler"],\n "option_consequences": ["One place to change; blunt for routes that need different budgets.", "Precise per route; every new route must remember to opt in."],\n "recommendation_index": 0,\n "codebase_evidence": "api/routes/__init__.py:41 already composes shared dependencies for every router."\n }\n ]\n }\n }\n ```\n\n3. **When the decisions come with framing worth showing**, use `artifact_type: "pre_ticket_planning"` instead and add a `system_goals` object inside `content` (`business_goal`, `desired_end_state`, `system_behavior`, and optionally `acceptance_criteria` and `nfrs`). Those render read-only above the cards, each with its own agree / ask / disagree control. Use this when the user needs to see the goal the decisions serve in order to answer them; the plain `review_decisions` page is the right default otherwise.\n\n4. **Handle the response `status`:**\n - `decision_page_generated`: surface the returned `file_path` and go to Stage 3.\n - `no_decisions_needed`: no page was written because there was nothing to render. Tell the user, and do not proceed to Stage 3.\n - `VALIDATION_ERROR`: the message names the field and restates the expected shape. Fix the payload and retry once. If it fails again, report the message verbatim rather than guessing further.\n\nIf the tool fails outright, **output a highly visible warning** (e.g. **\u26A0 WARNING: The decision page could not be generated** in bold) and fall back to settling the decisions in chat, one at a time. Do not continue silently \u2014 the failure must be visible in your output.\n\n## Stage 3 \u2014 Capture the answers (stop and wait)\n\n1. **Direct the user to the page.** Give them the `file_path` and tell them to open it in their browser. Explain that they can accept a recommendation, pick another option, reject them all, or flag a card for discussion, and that they can ask you questions in chat before submitting.\n\n2. **Treat each message as a commit or a discussion turn.**\n - **Commit:** trim the message and try to parse the whole trimmed message as JSON. Treat it as a commit only when the result is an object with all three top-level fields: `ticket_key` (string), `decisions` (object), and `general_comment` (string). The first valid commit-shaped paste commits \u2014 do not over-validate the individual cards.\n - **Discussion:** anything else. Answer it, then keep waiting. If a JSON-shaped paste is missing one of the three fields, say which one rather than treating it as a freeform question.\n - **In-flight overrides:** if the user changes an answer in chat ("go with per-handler for D-1"), record it as an override. On commit, the submitted JSON is the baseline and your recorded overrides win; acknowledge each overridden card in one line.\n\n3. **Resolve every "ask" (hard rule).** After accepting a commit, find every item in `decisions` where `choice === "ask"`. For each, present the evidence and keep discussing until the user gives an explicit answer. Do not proceed while any `ask` is unresolved, and do not honor "just skip those" \u2014 an unanswered card is an unmade decision.\n\n4. **Handle "None of these".** A `choice` of `"none"` means every option you offered was wrong. Ask what the user would do instead and record their answer as the decision. Do not re-render the page for this.\n\n**You MUST stop and wait for the user here.** Do not assume answers, do not proceed on the recommendations, and do not move to Stage 4 until the user commits or explicitly declines. If they decline, say the decisions are unsettled and stop.\n\n## Stage 4 \u2014 Fold the answers back\n\n1. **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\n Consider 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\n Emit 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\n If the review cannot be produced, report that in one line and continue without stalling the workflow or presenting the gate below.\n\n This review stays in chat: there is no document for this command to update.\n\n Then 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\n Literal `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\n2. **Restate every decision as settled**, in a short list: the question, the chosen answer, and \u2014 where the choice went against your recommendation or came from an override \u2014 one line on what changes as a result.\n\n3. **Carry `general_comment` as overarching guidance.** It applies across all the decisions, not to any one card. Say plainly how it changes the picture.\n\n4. **Name what these decisions now constrain.** One or two sentences on what is now fixed for the rest of the conversation. From here on, treat the settled answers as the contract \u2014 if later work would contradict one, say so and ask rather than quietly re-deciding.\n\nThere is no document to rewrite. The conversation is where the decisions live, unless the user asks you to record them somewhere.\n',"estimate-epic.md":"Estimate an entire Jira Epic or an explicit ticket-key group via the shared epic estimation orchestrator.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is either a Jira Epic key (e.g. `BAPI-518`) or an explicit `--tickets` key list \u2014 never both. This command calls the `estimate_epic` MCP tool, which delegates to the Bridge API epic estimation orchestrator, and renders the structured result.\n\nIf any step fails, stop immediately and report which step failed and why, preserving the user's originally entered epic key or ticket list in the report.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract exactly one key-source input, plus an optional `--allow-partial` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--tickets` appears, every token after it (up to the next flag or end of input) is the explicit ticket-key list \u2014 this is the `ticket_keys` mode.\n - Otherwise, the first token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`) is the `epic_key` \u2014 this is the epic mode.\n - `--allow-partial` may appear anywhere; if present, set `allow_partial_value = true`. If absent, omit `allow_partial` entirely (do not pass `false`).\n - Never resolve both an `epic_key` and a `ticket_keys` list from the same invocation \u2014 usage is one mode or the other.\n\n2. **Validate input**:\n - Usage forms: `/estimate-epic EPIC-KEY` or `/estimate-epic --tickets KEY-1 KEY-2 ...`, plus optional `--allow-partial`.\n - If neither an `epic_key` nor a `--tickets` list can be resolved, stop immediately and report:\n ```\n Usage: /estimate-epic EPIC-KEY [--allow-partial]\n /estimate-epic --tickets KEY-1 KEY-2 ... [--allow-partial]\n ```\n - If `--tickets` is present but followed by zero keys, stop immediately and report: \"`--tickets` requires at least one ticket key.\"\n - Do not invent or pass a `mode` parameter \u2014 there isn't one; the tool infers the source from whichever of `epic_key`/`ticket_keys` is supplied.\n\n## Step 2 \u2014 Call the Tool\n\nCall the `estimate_epic` MCP tool with:\n- `epic_key`: the resolved epic key \u2014 **only** when in epic mode. Omit entirely in ticket-key mode.\n- `ticket_keys`: the resolved ticket-key list \u2014 **only** when in ticket-key mode. Omit entirely in epic mode.\n- `allow_partial`: `allow_partial_value` if `--allow-partial` was passed; omit entirely otherwise (never pass `null`, an empty string, or an empty array for any absent field).\n\nNever pass both `epic_key` and `ticket_keys` in the same call.\n\nIf the tool returns an error envelope (a JSON object with an `error` field), stop and report the error message, preserving the epic key or ticket list the user originally entered.\n\n## Step 3 \u2014 Render the Result\n\nRender the successful result as a structured report \u2014 do not dump raw JSON by default:\n\n1. **Top**: the final estimate and its scale label (`estimate_label`) as the primary heading \u2014 this is the strongest element of the report.\n2. **Immediately after the summary**: `math_source`.\n3. **Next**: resolved child ticket keys (`child_ticket_keys`) and the per-child breakdown, presented compactly.\n4. **Only if non-empty**: a compact warning section listing `failed_child_keys` and `skipped_child_keys`.\n\nKeep the happy-path report concise and scannable. Use backticks for Jira keys and technical identifiers (e.g. `BAPI-518`).\n\n> Note: this tool does not accept a `recreate` parameter \u2014 the underlying epic estimation orchestrator (BAPI-522) always reuses cached child estimates and has no recreate knob to forward to.\n\n## Final Report\n\nOn successful completion, display a structured summary per Step 3 above. On failure, display the error message returned by the tool (or the usage error from Step 1), preserving the user's originally entered epic key or ticket list.\n","explore-ticket.md":`Explore the codebase for a task, settle its acceptance criteria with the user, then propose a design that meets them.
388
+ `,"conduct-epic.md":"---\nschedulable: true\narguments: {\"positionals\":[{\"name\":\"epicKey\",\"type\":\"string\",\"required\":true}],\"flags\":[{\"name\":\"tickets\",\"flag\":\"--tickets\",\"type\":\"string\"},{\"name\":\"baseBranch\",\"flag\":\"--base-branch\",\"type\":\"string\"},{\"name\":\"checkpointPath\",\"flag\":\"--checkpoint-path\",\"type\":\"string\"}]}\n---\n\n# Conduct Epic: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command drives one multi-ticket epic from an approved ticket list to a finished `epic/<EPIC>` branch, one ticket at a time. It is the LLM half of the LLM-as-conductor pivot: there is no server-side reconciler here, no background worker, and no durable run row \u2014 the checkpoint file written by the packaged `conduct-epic` CLI plus the live state of GitHub *is* the entire memory of the loop.\n\nCadence is not an argument of this command. `/loop` owns the interval, this command owns exactly one reconcile-then-act step, and the two compose without either one holding state for the other.\n\nIt composes work that already exists rather than reimplementing it: `/review-and-start --auto --base-branch 'epic/<EPIC>' <KEY>` spawns each ticket's worker, the `merge_pull_request` MCP tool merges a green and approved pull request, `parse_repository` / `get_parse_status` re-index the repository after each merge so the next ticket's plan sees its predecessor's code, and the packaged `conduct-epic` CLI (`init`, `status`, `checkpoint set`, `finish`, `spawn`) owns every durable file operation.\n\n---\n\n# Instructions\n\nYou are executing a 5-stage tick. Run the stages in order, take **exactly one** action from the Stage 3 detection table, write **exactly one** checkpoint in Stage 4, then stop. Do not loop internally, do not take a second action because the first one looked cheap, and do not carry assumptions from a previous tick \u2014 every tick reconciles from scratch.\n\nThe \"exactly one checkpoint\" rule has **three explicitly documented exemptions** and no others: the two print-only parks, `init_failed` (Stage 1) and `foreign_lock` (Stage 2), which stop before Stage 3; and the `all_done` tick (Row 1), which has no in-flight ticket to name in a `checkpoint set` command. Stage 4 states each one.\n\n## Stage 0 \u2014 Arguments and Ping\n\n1. **Parse `$ARGUMENTS`** into exactly one epic positional and the three optional flags. Accept no other input shape.\n\n - **`<EPIC>`**: exactly one positional token, which must match `[A-Z]+-[0-9]+` (e.g. `BAPI-798`). Zero epic positionals, more than one positional, or a positional that does not match the pattern is malformed input. Extra positionals are rejected rather than ignored.\n - **`--tickets <K1,K2,\u2026>`** (and the equals form `--tickets=<K1,K2,\u2026>`): a non-empty, comma-separated, **ordered** list of ticket keys. Preserve the caller's order exactly \u2014 it is the execution order of the epic. Every entry must match `[A-Z]+-[0-9]+` after trimming surrounding whitespace; reject a malformed key, an empty entry, and a duplicate key. This flag is required **only on the first tick** (see Stage 1); later ticks read the order from the checkpoint.\n - **`--base-branch <branch>`** (and the equals form `--base-branch=<branch>`): validated with the same rules as `/start-tickets` Stage 0 \u2014 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`). It is the branch `epic/<EPIC>` is cut from at `init` time; it is not the pull-request base of a ticket, which is always `epic/<EPIC>`.\n - **`--checkpoint-path <path>`** (and the equals form `--checkpoint-path=<path>`): must be a non-empty string after trimming, checked **before** it is used as a path or interpolated into a CLI invocation. When omitted, the CLI's own default (`~/.config/bridge/conduct/<repo>/<EPIC>.json`) applies and `status` prints the resolved path.\n\n Reject malformed input before any side effect: an unsupported flag, a flag given without its value, a `--tickets` list that fails the rules above, a `--base-branch` value that fails validation, an empty `--checkpoint-path`, a missing epic, or an extra positional. On any of these, stop immediately and display:\n\n ```\n Invalid arguments.\n Usage: /conduct-epic [flags] <EPIC>\n <EPIC> required, matches [A-Z]+-[0-9]+ (e.g. BAPI-798)\n --tickets K1,K2,\u2026 ordered ticket keys; required only on the first tick\n --base-branch <branch> branch epic/<EPIC> is cut from (default: the repo base)\n --checkpoint-path <path> override the checkpoint file location\n ```\n\n2. **Connectivity check**: call the `ping` MCP tool with **no parameters**. If the call fails, or does not return `\"status\": \"ok\"`, stop immediately \u2014 before Stage 1 initialization, before any CLI invocation, and before any state is written \u2014 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\n3. **Execution model.** This command is one tick; run it with `/loop 5m /conduct-epic <EPIC>`; each tick re-reads the checkpoint and GitHub, takes at most one action, and exits. `/loop` is the external driver that re-invokes this command \u2014 it is not an internal loop this command runs, and cadence is never an argument of this command.\n\n4. **Bash interpolation rule (global; applies to every Bash invocation in every stage).** Before interpolating any dynamic value \u2014 the epic key, a ticket key, a branch name, a checkpoint path, a prompt-file path, a JSON blob, a journal line \u2014 replace every `'` in the value with `'\\''`, then wrap the complete value in single quotes. Never expand a dynamic value unquoted, and never build a command by concatenating an unquoted variable. Credentials must never appear in a command argument, in printed output, in a journal line, or in a prompt file: the CLI and the MCP tools resolve their own credentials from the environment and the user-scoped credential store.\n\n5. **Packaged CLI launcher (`BAPI_MCP_CLI`); global, applies to every packaged-CLI invocation in every stage.** Resolve the launcher **once**, here in Stage 0, and reuse that one resolved value for the rest of the tick. 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\n When the override is set, apply item 4's single-quote escaping rule to `<launcher>` before interpolating it into a Bash command string, keep every dynamic argument independently quoted rather than concatenated into the launcher value, and never put a credential or a credential-bearing environment assignment into it. 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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Initialize If No Checkpoint\n\nRun the first status probe through the **Bash tool**, forwarding `--checkpoint-path '<path>'` only when the user supplied it:\n\n```\n<launcher> conduct-epic status '<EPIC>' --json\n```\n\nA zero-exit response whose `checkpoint_exists` is `false` is the **only** condition under which initialization is allowed.\n\n- **`checkpoint_exists` is `false`** \u2014 this is the first tick. `--tickets` is required here, and **only** here: if it was not supplied, halt with the Stage 0 usage message and initialize nothing. On every later tick `--tickets` is optional and ignored, because the ordered list already lives in the checkpoint. Otherwise run, forwarding `--base-branch '<b>'` and `--checkpoint-path '<p>'` only when supplied:\n\n ```\n <launcher> conduct-epic init '<EPIC>' --tickets '<K1,K2,\u2026>'\n ```\n\n Print the initialization preflight output **verbatim** \u2014 do not summarize it, do not suppress its announcements, and do not reorder it. `init` runs one preflight that lists every failure at once, and that listing is the operator's only diagnostic when it refuses.\n\n On a **non-zero** exit, `init_failed` is a **print-only park**: emit `NEEDS_HUMAN:init_failed` with the complete secret-free output as evidence, print exactly one bounded, secret-free stdout journal line describing this invocation, and stop the tick. Do **not** call `checkpoint set` and do not otherwise mutate durable state. There is nothing to write to: when initialization failed, no writable checkpoint may exist at all, and any checkpoint that does exist may be the unreadable one that caused the failure. Do not attempt a second initialization in the same tick and do not fall through to Stage 2.\n\n- **`checkpoint_exists` is `true`** \u2014 an epic that already has a checkpoint must **never** trigger `init`. The CLI deliberately refuses reinitialization (`already initialized`), so a retry is not a recovery path; it is a bug in the caller. Skip straight to Stage 2.\n\n- **The status command exits non-zero** (a corrupt or wrong-version checkpoint, for example) \u2014 treat it exactly like a failed init, including the print-only rule: preserve the secret-free stderr as evidence, emit `NEEDS_HUMAN:init_failed`, print one journal line, call no `checkpoint set`, and stop the tick. `status` never rewrites a checkpoint it could not read, so nothing has been damaged.\n\n## Stage 2 \u2014 Reconcile From Status JSON\n\nRun the status probe **again**, with the same conditional `--checkpoint-path '<path>'` forwarding:\n\n```\n<launcher> conduct-epic status '<EPIC>' --json\n```\n\nThis second response is the action snapshot. **This JSON object is the only evidence the tick acts on.** Worker claims are never trusted \u2014 a session that says \"CI passed\", \"review approved\", or \"PR merged\" has told you nothing this tick may use. Every one of those facts is re-derived here from GitHub and the server through `status`, and only from there.\n\nThe top-level contract is exactly: `ok`, `epic_key`, `epic_branch`, `checkpoint_path`, `checkpoint_exists`, `all_done`, `ticket`, `worktree_path`, `worktree_exists`, `branch_head`, `worker_commits_since_spawn`, `last_seen_head`, `last_state_change_at`, `stale_for_seconds`, `pr`, `merged_externally`, `ci`, `review`, `parse`, `deadlines`, `scope`, `lock`, `needs_human`, and `probe_errors`.\n\nThe nested objects the detection table reads are:\n\n- `ticket` \u2014 the in-flight ticket (the first entry that is not `done`, or `null` when `all_done`): `key`, `status` (`pending`, `in_progress`, `merged`, `done`, `needs_human`), `branch`, `pr_number`, `spawned_at`, `parse_requested_at`, `parse_requested_for_sha`, `review_verdictless_observations`, `review_verdictless_for_sha`, `respawns`, `conflict_attempts`, `counters.sessions_spawned`, `counters.plan_generations_observed`, `counters.merge_attempts`, and `journal`.\n - `review_verdictless_observations` is a **non-negative integer** and `review_verdictless_for_sha` is **a string or `null`**. They are Row 10's head-bound verdictless counter: the count is only meaningful for the head named beside it, and a count whose head does not equal `pr.head_sha` is spent evidence about code that no longer exists. Read them together or not at all.\n - `parse_requested_at` and `parse_requested_for_sha` are **each a string or `null`**. They are RETAINED for the audit trail of epics driven before the scope-status contract existed, and **no row reads them any more**: Row 5 asks the `scope` sub-object directly instead of reconstructing causality from a request timestamp. Do not write them and do not decide on them.\n - `journal` is the ticket's journal lines, **oldest-first, newest last**, exactly as stored. It is a human-readable audit trail and is **never** the source of a decision: it is capped at 50 lines and evicts oldest-first, so a marker searched for in it would silently vanish after roughly fifty wait ticks and the loop would re-request a parse it had already requested.\n- `pr` \u2014 `number`, `state` (`OPEN`, `MERGED`, `CLOSED`), `head_sha`, `base`, `mergeable`, `merge_state`, `updated_at`.\n- `ci` \u2014 `required`, `complete`, `stable_across_two_polls`, `head_sha`, and `checks` entries of `name`, `status`, `conclusion`, `required`.\n- `review` \u2014 `opted_in`, `source`, `available`, `verdict` (`approved`, `changes_requested`, `unknown`), `head_sha`, `verdictless_disposition`, `verdictless_ceiling`, `config_invalid`.\n - `verdictless_disposition` is `park`, `fail_open`, or `null`. **`null` means `park`** \u2014 it is what a condition that configured no disposition reports, and what an unreadable configuration reports. A value is only ever one of those three; the server-side parser refuses every other spelling outright rather than passing a partially honored one through.\n - `verdictless_ceiling` is the number of head-bound verdictless observations Row 10 makes before it decides. Read it from this snapshot and compare against it; never hard-code a bound.\n - `config_invalid` is `true` when the repository's `done_gate_config` exists but could not be read \u2014 a `malformed` or `invalid: \u2026` parse. It arrives with `opted_in: true` and `available: false`, because an unreadable review policy is **not** an absent one: reading it as \"no review opt-in\" would merge on CI alone on the strength of a typo. There is no readable condition in that state, so `verdictless_disposition` is `null` and Row 10 parks.\n- `parse` \u2014 `status` (`idle`, `queued`, `in_progress`, `succeeded`, `failed`), `terminal`, `started_at`, and `finished_at`. The last two are each **a string or `null`** and are the ISO-8601 times of the current or last parse run. A `null` on either is unavailable evidence and **never** permits advancement \u2014 in particular, missing timestamps can never satisfy Row 5's causal check. There is no repository-wide index-branch override field: BAPI-847 retired that control plane, and an epic now gets its own index scope instead of taking the repository's index away.\n- `deadlines` \u2014 `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\n- `scope` \u2014 the epic's index scope, read directly from the server: `scope_id`, `lifecycle_state`, `freshness_status`, `blocked_reason`, `required_commit_sha`, `indexed_commit_sha`, and `last_error`. It is `null` **only** when this epic declares no scope at all; that is not a probe failure and carries no `probe_errors` entry.\n - `freshness_status` is one of `fresh`, `pending`, `blocked`, `failed`, `unavailable`. **`fresh` is the only value that means the index covers this epic's merged code.** `pending` is a refresh still running. `blocked` is an epic advance the server REFUSED to index and will never resolve by waiting \u2014 `blocked_reason` names which refusal. `failed` is the scope's own generation failing. `unavailable` means the scope could not be read this tick, and is reported alongside a `{probe: \"scope\"}` entry in `probe_errors`.\n - `required_commit_sha` is the commit the scope must cover; `indexed_commit_sha` is the commit it actually has. **They are separate fields because they mean different things** \u2014 the required SHA moves the moment a merge is accepted, long before anything is indexed, so a required SHA equal to your merge commit is not evidence that your merge was indexed.\n- `lock` \u2014 `held_by_me`, `owner_pid`, `host`, `alive`.\n- `needs_human` \u2014 `null`, or `reason`, `evidence`, `at`.\n- `probe_errors` \u2014 entries of `probe` and `reason`.\n\nA failed probe leaves its sub-object `null` and is listed in `probe_errors`; it never fails the command. **A `null` sub-object is unavailable evidence, not a negative result.** Never infer a merge, an approval, a CI success, or a parse success from a `null` value, from a missing field, or from narrative output of any kind \u2014 an unavailable probe means \"wait for the next tick\", never \"proceed\".\n\n**`pr` is the one sub-object whose `null` has two distinct meanings, and `probe_errors` is what tells them apart:**\n\n- **`pr` is `null` and there is no `{probe: \"pr\"}` entry** \u2014 confirmed absence. `gh` was asked and answered that this branch has no pull request. This is the **normal** state of every tick between the first spawn and the moment the worker opens its pull request, it is a negative result the rows may act on, and Rows 6 and 7 exist precisely for it.\n- **`pr` is `null` and there IS a `{probe: \"pr\"}` entry** \u2014 unavailable evidence. `gh` could not answer: unauthenticated, rate-limited, offline, or output that did not parse. Treat it as \"wait for the next tick\" and never as absence; a pull request that exists but cannot be seen must not be reasoned about as one that does not exist.\n\nDo not collapse these two into \"no PR\". Reading an outage as absence is how the loop would respawn into, or abandon, a pull request that was there all along.\n\nTwo states stop the tick before any action is selected:\n\n- **Already parked.** If `needs_human` is not `null`, print the stable phrase `already parked`, followed by the persisted `reason`, the persisted string `evidence`, and the persisted `at` timestamp \u2014 then stop. Take no action this tick and write no checkpoint. A parked epic is a human's to unpark by editing the checkpoint (`needs_human` back to `null`, the ticket `status` back to `pending`/`in_progress`, counters adjusted if a budget is re-granted). Do not select a new recovery action on top of an existing one.\n- **Foreign lock.** If `lock.held_by_me` is `false` and `lock.alive` is `true`, another live process owns this epic. `foreign_lock` is a **print-only park**: emit `NEEDS_HUMAN:foreign_lock` carrying `lock.owner_pid` and `lock.host` as evidence, print one bounded, secret-free stdout journal line for this invocation, and stop. Do **not** call `checkpoint set`, spawn a session, merge a pull request, or start a parse while that lock is alive. The checkpoint belongs to the other live process; writing to it \u2014 even to record a park \u2014 is the two-authorities corruption the lock exists to prevent, and `checkpoint set` refuses a live foreign lock anyway.\n\n## Stage 3 \u2014 Detect and Take Exactly One Action\n\nEvaluate the rows below **strictly in written order, from top to bottom**. Evaluation stops at the first row whose condition matches; that row's action is the only action this tick performs, and control then proceeds directly to Stage 4. A later row is never \"also\" run because it happens to apply.\n\nOne row states a **forward-looking guard** in its own condition: Row 3 (`stalled`) matches only when no later action or fail-closed row would be selectable for this snapshot. That guard is part of Row 3's condition, not a departure from written order \u2014 the ordering rule still holds, and Row 3 simply does not match while a real action is available.\n\nEach row is marked **fail-open** (an uncertain or transient condition waits for the next tick) or **fail-closed** (the tick refuses to act and parks rather than guessing).\n\n### Row 1 \u2014 `all_done`: finish the epic and open its pull request\n\nWhen `all_done` is `true`, run `<launcher> conduct-epic finish '<EPIC>'` (forwarding `--checkpoint-path '<p>'` when supplied), then call the `create_pull_request` MCP tool with `head_branch` set to `epic/<EPIC>` and `base_branch` set to `main`. Assemble the `body` from the finish summary: the merged ticket pull requests and any skipped tickets. **Open the pull request; never merge it** \u2014 a human reviews and merges the epic into `main`. Then stop.\n\n**This tick writes no checkpoint and does not increment `counters.iterations`.** It is the third documented exemption from Stage 4's one-checkpoint-per-tick rule, and unlike the two print-only parks it reaches Stage 3. The reason is mechanical: `all_done` is `true` exactly when `ticket` is `null`, `checkpoint set` requires `--ticket <KEY>`, and there is no in-flight ticket to name. `finish` is this tick's durable act, and it is the last one the epic needs \u2014 so do not invent a ticket key to satisfy the rule, and do not write a checkpoint before or after `finish`.\n\n### Row 2 \u2014 Wrong base: do not touch a pull request that is not on the epic branch\n\nWhen `pr.base` is present and is not `epic/<EPIC>`, **do not touch the pull request** \u2014 no merge, no comment, no respawn. Select `NEEDS_HUMAN:wrong_base`, carrying the observed `pr.base`, `pr.number`, and the expected `epic/<EPIC>`. **Fail-closed**: only pull requests based on `epic/<EPIC>` are ever acted upon, and this row is evaluated before every work and recovery row precisely so a mis-based pull request cannot be merged, respawned into, or advanced by a later row.\n\n### Row 3 \u2014 Hard liveness: a stalled epic parks before it waits\n\nWhen `stale_for_seconds >= deadlines.hard_seconds` (default three hours, `10800`) **and no other row below is selectable this tick**, select `NEEDS_HUMAN:stalled`, carrying the observed `stale_for_seconds` and the `deadlines.hard_seconds` it exceeded. **Fail-closed**.\n\n**This row outranks wait rows only.** Before selecting it, check whether any of the following would otherwise be selectable for this snapshot; if any one of them would, take that row instead and do not park:\n\n- pending work (Row 4's first spawn),\n- Row 5's **action** branches only \u2014 branch 1's parse request, branch 3's completion, and branch 5's causal `parse_failed` park,\n- a targeted respawn (Rows 7, 9, and 11),\n- CI-red handling (Row 9) and review-remediation handling (Row 11),\n- conflict handling (Row 12),\n- ready-to-merge handling (Row 13),\n- a closed, unmerged pull request (Row 13a),\n- Row 10's **action** branch only \u2014 a verdictless review at or above `review.verdictless_ceiling`, whichever disposition it then applies. Row 10's below-ceiling branch is a wait and stays subordinate to this row, exactly as the old unbounded wait did.\n\n`stale_for_seconds` counts from the last observed head or status change, not from the last useful event \u2014 so an old but green and approved pull request accumulates staleness while being perfectly actionable. Parking that is the exact defect this guard removes. The row remains ahead of every wait row, because without it a wait would match forever and the epic would sit silent instead of asking for a human.\n\n**Row 5's wait branches are deliberately NOT in that list.** Branches 2, 4, and 6 \u2014 a parse that is queued or in progress, a non-causal `succeeded` or `failed`, an inconsistent request record \u2014 are waits, and exempting them would mean a merged ticket whose parse never starts waits forever with no human ever asked. They accumulate staleness like any other wait and park as `stalled` once `deadlines.hard_seconds` is exceeded.\n\n### Row 4 \u2014 Pending ticket: spawn the first worker\n\nWhen `ticket.status` is `pending`, spawn the ticket's session:\n\n```\n/review-and-start --auto --base-branch 'epic/<EPIC>' <KEY>\n```\n\nThen prepare the Stage 4 checkpoint values `spawned_at` (now, ISO-8601), `status=in_progress`, and `counters.sessions_spawned` = the Stage 2 value plus one.\n\n**Fail-closed**: refuse this spawn if the lock is foreign (Stage 2 has already parked in that case). The pull-request base of the spawned worker comes from BAPI-801's `BAPI_BASE_BRANCH` export \u2014 `/review-and-start --base-branch` forwards it into the spawned worker shell, and the worker's create-PR step resolves the base from it. That export is what makes the first pull request land on `epic/<EPIC>`; this loop never relies on it alone, because Row 2 independently re-checks the observed `pr.base` on every later tick.\n\n### Row 5 \u2014 Merged ticket: refresh the scope index, then mark done\n\nWhen `pr.state` is `MERGED`, or `merged_externally` is `true`, or `ticket.status` is `merged`, the ticket's code is on the epic branch. An **external merge is successful reconciliation, not an error** \u2014 a human who merged the pull request by hand did the loop's work for it, and `merged_externally` records exactly that.\n\n**The evidence this row acts on is `scope`, and only `scope`.** The epic's index scope is refreshed by the server the moment it observes the merge: it advances its own `required_commit_sha` to the merge commit and re-parses incrementally. So the question \"has this merge been indexed?\" is a question the scope can answer directly, and this row asks it instead of reconstructing an answer.\n\nThat is a deliberate replacement of the older mechanism. This row used to record the time it called `parse_repository` and the head SHA it called it for, then compare that timestamp against a repository-wide parse run's `started_at` / `finished_at` \u2014 because `parse.status` is repository-level and stays `succeeded` from any earlier parse of any earlier ticket, so \"succeeded\" alone proved nothing. Timestamp ordering was the only causality available. It is no longer needed, and inference is strictly worse than an answer: **do not call `parse_repository` from this row, and do not read `parse`, `ticket.parse_requested_at`, or `ticket.parse_requested_for_sha` as freshness evidence.** The server owns the refresh; this loop observes it.\n\nThis row is an **ordered state machine**, evaluated top to bottom, and the first matching branch is the tick's action:\n\n1. **`scope` is `null`** \u2014 this epic declares no index scope, so there is nothing to refresh and no freshness to establish. Call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. Journal that the ticket completed with no declared scope. **Fail-open.** An epic that never had a scope must not be blocked by one.\n\n2. **`scope.freshness_status` is `fresh`, and `scope.indexed_commit_sha` equals `scope.required_commit_sha`, both non-null** \u2014 the scope's index provably covers the commit the server is holding it to. Only then call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. Journal both observed watermarks.\n\n **Compare the scope's two watermarks against each other \u2014 never against `pr.head_sha` or `branch_head`.** Both of those are the *worker's* pre-merge branch tip: `pr.head_sha` is `headRefOid`, and `branch_head` is `git ls-remote` of the ticket's own branch. What lands on `epic/<EPIC>` is the merge commit GitHub creates, and that differs from the worker's tip under every merge strategy \u2014 merge, squash, and rebase alike. Comparing an indexed watermark against either one is therefore false essentially always, and a branch that waits on an always-false condition never marks anything done. For the same reason, do not invent a merge-commit field: the `scope` object carries exactly the seven fields named above, and none of them is one.\n\n The identity that IS causal runs between the scope's own two watermarks, and it is what replaces the old timestamp ordering. The server advances `required_commit_sha` the moment it observes this merge, and **only the parse** writes `indexed_commit_sha`; the two fields are owned by different writers precisely so their agreement means something. So `indexed == required` is the server's own statement that it has finished indexing everything it was asked to cover. A scope that finished refreshing for a **previous** ticket reads `fresh` too \u2014 but it reads it at that previous required commit, and the moment this merge is observed `required` moves ahead of `indexed` and `freshness_status` drops to `pending` until the re-parse lands. If either watermark is `null` the comparison cannot be made, so this branch does not match and the tick falls to branch 6 and waits.\n\n **The one gap this cannot see through** is the interval between the merge and the server observing it: in that window the scope still reads `fresh` at the previous ticket's watermark, and no field in the contract tells it apart from this ticket's. It is narrow in practice \u2014 the same merge event that makes `pr.state` read `MERGED` is the one that notifies the server, so a tick that reaches this row has almost always been preceded by that notification \u2014 and it closes on its own. It is not zero: a merge the server never observed at all would leave the watermarks agreeing at the previous commit, and this branch would mark the ticket done against an index that does not contain it. Treat a `done` whose journaled watermarks match the *previous* ticket's as that failure, not as a fresh index.\n\n3. **`scope.freshness_status` is `pending`, `unavailable`, or missing** \u2014 the refresh is still in flight, or the scope could not be read. Wait. Journal the observed `scope.lifecycle_state`, `scope.required_commit_sha`, and `scope.indexed_commit_sha`. Do not spawn anything and do not advance the next ticket. **An unread scope is never a fresh one.**\n\n4. **`scope.freshness_status` is `blocked`** \u2014 the server REFUSED to index this advance, and waiting will never change that. Select `NEEDS_HUMAN:shadow_stale_deadline`, with `scope.blocked_reason` as bounded string evidence, and state plainly in the evidence that **no epic advance was indexed**. **Fail-closed.**\n\n The controlled reasons and what each one means to a human:\n\n - `advance_blocked_base_merge` \u2014 the base branch was merged forward into the epic branch. The epic branch is pinned at its cut point; a base merge would move that pin.\n - `advance_blocked_unexpected_parent` \u2014 the merge commit does not descend directly from the branch head the scope pinned. Something other than a worker pull request landed on the branch.\n - `advance_blocked_history_changed` \u2014 the pinned head is gone from the branch's history. A force-push or rewrite.\n - `advance_blocked_unverifiable` \u2014 the advance could not be verified at all. Doubt blocks; it never indexes.\n\n **This park is immediate, and that is deliberate** \u2014 it is the one place the pilot escalates faster than v2. The v2 reconciler routes a blocked advance through the same `shadow.stale_deadline_seconds` clock it uses for an ordinary refresh hold, because its hold is anchored on a single durable episode timestamp that every hold reason shares. The pilot has no such episode and no typed `RunPolicy` deadline, and none of the four reasons above resolves by waiting, so waiting out a deadline would only delay a human by up to that deadline and change nothing else. Both conductors emit the **same** `shadow_stale_deadline` reason so one grep finds a refused advance either way; only the latency to the park differs. An operator comparing the two should expect the pilot to ask sooner, not to have asked for a different thing.\n\n5. **`scope.freshness_status` is `failed`** \u2014 the scope's own generation failed, which is a different problem from a refused advance. Select `NEEDS_HUMAN:parse_failed`, with `scope.lifecycle_state` and `scope.last_error` as bounded string evidence. **Fail-closed.**\n\n6. **None of branches 1\u20135 matched** \u2014 including a `fresh` scope whose indexed commit still trails its required commit, and a tick where either watermark is missing so no comparison can be made. Wait, and journal the observed scope fields. Neither advance nor park: hard liveness (Row 3) is what eventually escalates a wait that never resolves.\n\n**No next ticket is spawned until this one reaches `done`.** A merged ticket stays in flight until its scope is fresh for its own merge commit, so `ticket` still points at it and Row 4 cannot match for its successor \u2014 which is the whole point: the next ticket's review and plan must see this ticket's merged code.\n\n### Row 6 \u2014 Worktree working: wait\n\nWhen a worktree exists (`worktree_exists` is `true`), the pull request is **confirmed absent** (`pr` is `null` **and** `probe_errors` carries no `{probe: \"pr\"}` entry), and `worker_commits_since_spawn > 0`, the worker is making observable progress. Wait, and journal the observed `branch_head` and commit count. **Fail-open.**\n\nA `pr: null` accompanied by a PR probe error is unavailable evidence, not absence, and does not match this row \u2014 it falls through to Row 15 and waits.\n\n### Row 7 \u2014 Soft deadline with no progress: one targeted continuation\n\nWhen the pull request is **confirmed absent** (`pr` is `null` **and** no `{probe: \"pr\"}` entry), `worker_commits_since_spawn` is `0`, and `deadlines.elapsed_since_spawn_seconds >= deadlines.soft_seconds` (default one hour, `3600`), spend the single targeted respawn on kind `continue`, with the prompt:\n\n```\nBranch <b> for <KEY>: continue the existing plan; do not regenerate it; push when done\n```\n\nPrepare `respawns` = the Stage 2 value plus one. `respawns` is **one shared per-ticket budget**, not one allowance per row: Rows 7, 9, and 11 all spend the same single counter, so spending it here leaves nothing for a later CI fix or review fix on this ticket. The attempt **counts only if it pushed** \u2014 a later tick observing a non-null `branch_head` is the proof. A respawn that produces no push is a no-op, and a no-op respawn stops the loop rather than spinning: once the one targeted respawn is spent and the ticket still shows no pushed head, select `NEEDS_HUMAN:stalled`. **Fail-closed after one attempt**, which is what keeps a dead worker from being respawned without bound.\n\n### Row 8 \u2014 Pull request open, CI not settled: wait\n\nWhen a pull request is open and `ci.complete` is `false` **and no required check in `ci.checks` has already reached a terminal unsuccessful conclusion**, wait; or when `ci.complete` is `true` and green but `ci.stable_across_two_polls` is `false`, wait. **Fail-open.**\n\nThe boolean alone is not the condition. `ci.complete` is `false` both while checks are still running and once a required check has definitively failed, and those are opposite situations: the first is worth waiting on and the second never becomes green on its own. This row therefore covers pending and not-yet-stable checks **only** \u2014 a required check with a terminal unsuccessful conclusion is **not** consumed here and falls through to Row 9.\n\n### Row 9 \u2014 Pull request open, CI red: one targeted fix\n\nWhen a pull request is open, one or more required checks in `ci.checks` have a terminal unsuccessful conclusion, and there has been no new commit for over 60 minutes (`stale_for_seconds > 3600` is the authoritative no-new-commit duration), spend the single targeted respawn on kind `ci_fix`. Take the failing check names from `ci.checks` \u2014 the entries whose `required` is `true` \u2014 and use the prompt:\n\n```\nPR #N is red on <checks>: read the check annotations, fix, push; do not regenerate the plan\n```\n\nPrepare `respawns` = the Stage 2 value plus one; the attempt counts only if it pushed. A bare `/implement-ticket --auto` is **prohibited** here: it regenerates the plan, costs a full plan generation, and discards the failure detail the annotations already carry.\n\n`respawns` is **one shared per-ticket budget** across Rows 7, 9, and 11. A continuation respawn spent earlier on this ticket therefore leaves **no** CI-fix attempt: with the counter already at its limit, persistent red CI parks immediately as `NEEDS_HUMAN:ci_red` rather than getting a fix session of its own. Once the shared respawn is spent and CI is still red, select `NEEDS_HUMAN:ci_red` with the failing check names as bounded string evidence. **Fail-closed after one attempt.**\n\n### Row 10 \u2014 Review opted in and verdictless: count, then decide\n\nWhen `pr.state` is `OPEN`, `review.opted_in` is `true`, and the review is **verdictless for the current head** \u2014 that is, `review.available` is `false`, **or** `review.verdict` is neither `approved` nor `changes_requested` at `pr.head_sha` \u2014 the review has produced no usable answer for this code. Count the observation, then act on the count.\n\nThis row covers **both** verdictless shapes on purpose. `review.available` is `false` only when the review read itself failed. A reviewer that ran and died before publishing anything is a different shape: the read succeeds, `review.available` is `true`, and `review.verdict` is `unknown`. Both mean the same thing to this loop \u2014 no verdict exists for `pr.head_sha` \u2014 and a row that covered only the first would leave the second matching nothing at all.\n\n`changes_requested` at the current head is **explicitly excluded**, so Row 11 stays reachable: a reviewer that asked for changes produced a verdict, and that verdict is Row 11's business. A `changes_requested` verdict whose `review.head_sha` does not equal `pr.head_sha` is about code that no longer exists, so it is verdictless for the current head and does match here.\n\nThe `pr.state` is `OPEN` guard is load-bearing: without it a `CLOSED` pull request whose review is verdictless matches here, ahead of Row 13a, and the loop counts tick after tick on abandoned work instead of parking it.\n\n**Prepare the counter, bound to the current head.**\n\n- If `ticket.review_verdictless_for_sha` does **not** equal `pr.head_sha`, prepare `review_verdictless_observations` = `1` and `review_verdictless_for_sha` = `pr.head_sha`. **The counter resets on a new head.** Observations made against an abandoned head must never spend the budget belonging to the head that replaced it \u2014 a later push replaces the code the reviewer failed on, and the new code deserves its own full budget.\n- Otherwise prepare `review_verdictless_observations` = the Stage 2 value plus one, absolute, and leave `review_verdictless_for_sha` at `pr.head_sha`.\n\nWrite both prepared fields through the ordinary single `checkpoint set` for this tick, in every direction below \u2014 waiting, parking, and the waived merge alike.\n\n**Compare the prepared count with `review.verdictless_ceiling`**, which the Stage 2 snapshot carries. Compare two numbers read from the snapshot; never compare against a bound written into this prose.\n\n- **Below the ceiling** \u2014 wait one tick and journal the observation, naming the prepared count, the ceiling, and the observed `review.available` / `review.verdict`. This is today's behaviour, unchanged. This branch is a **wait**, so Row 3's hard-liveness park still outranks it exactly as it does now.\n- **At or above the ceiling** \u2014 apply `review.verdictless_disposition`. This branch is an **action**, so it outranks Row 3, and the ceiling is what an operator actually sees instead of a three-hour `stalled` that names the wrong failure.\n\n**At or above the ceiling, the disposition decides:**\n\n- **`park`** \u2014 the default, and the value used whenever `review.verdictless_disposition` is `null`, including when `review.config_invalid` is `true` (a review policy that could not be read carries no readable disposition, so it gets the safe one). Select `NEEDS_HUMAN:review_verdictless_ceiling_reached`, carrying the observed count, the ceiling, `pr.head_sha`, and `review.available` / `review.verdict` as bounded string evidence.\n- **`fail_open`** \u2014 treat the ticket as **review-opted-out for this tick** and fall through to Row 13's merge conditions. Row 13 still requires everything else it always required: `pr.state` is `OPEN`, `pr.base` is `epic/<EPIC>`, `ci.complete` is `true`, `ci.stable_across_two_polls` is `true` at `pr.head_sha`, and a non-conflicting pull request. **CI, not a verdict, is the whole of the evidence in that case** \u2014 journal `review_waived_verdictless_fail_open` and say the merge proceeded on stable CI evidence alone. Never journal, print, or record it as a review that passed or approved anything.\n\nAny value other than exactly `fail_open` resolves to `park`. There is no third direction, and an unreadable disposition is never treated as permission.\n\n### Row 11 \u2014 Changes requested for the current head: one targeted review fix\n\nWhen `pr.state` is `OPEN`, `review.verdict` is `changes_requested`, **and** `review.head_sha` equals `pr.head_sha`, spend the single targeted respawn on kind `review_fix`. The `OPEN` guard is what stops a `changes_requested` verdict left on a **closed** pull request's head from spending this ticket's one respawn on work nobody will merge \u2014 that snapshot belongs to Row 13a. The prompt carries the authoritative Stage 2 review evidence: the ticket key, the pull-request number, the reviewed head SHA, and the requested changes. A stale `review.head_sha` (one that does not equal `pr.head_sha`) is a verdict about code that no longer exists and never triggers this row. Prepare `respawns` = the Stage 2 value plus one.\n\n`respawns` is **one shared per-ticket budget** across Rows 7, 9, and 11. Any earlier continuation or CI-fix respawn on this ticket therefore leaves **no** review-fix attempt: with the counter already at its limit, requested changes on the current head park immediately. Once the shared respawn is spent and the verdict still stands for the current head, select `NEEDS_HUMAN:review_changes_requested`. **Fail-closed after one attempt.**\n\n### Row 12 \u2014 Conflicting pull request: at most two conflict sessions\n\nWhen `pr.state` is `OPEN` **and** either `pr.mergeable` is `CONFLICTING` or `pr.merge_state` is `DIRTY`, spawn a session of kind `conflict` with the prompt:\n\n```\nrebase onto origin/epic/<EPIC>, resolve, run tests, push\n```\n\nPrepare `conflict_attempts` = the Stage 2 value plus one. The conflict budget is **two** sessions and is counted separately from the single targeted respawn of Rows 7, 9, and 11 \u2014 a rebase is a different failure mode from a stalled or red worker. After the second conflict session, if the pull request is still `CONFLICTING`/`DIRTY`, select `NEEDS_HUMAN:conflict`. **Fail-closed after two attempts.**\n\nA **closed** pull request is frequently left `CONFLICTING`/`DIRTY` by GitHub, so without the `pr.state` is `OPEN` guard this row would match ahead of Row 13a and spend a rebase session resolving conflicts on a branch nobody will merge.\n\n### Row 13 \u2014 Ready to merge\n\nMerge only when **all** of the following hold on the fresh Stage 2 snapshot: the pull request is open (`pr.state` is `OPEN`); `pr.base` is `epic/<EPIC>`; `ci.complete` is `true` and `ci.stable_across_two_polls` is `true` for `ci.head_sha` equal to `pr.head_sha`; the pull request is not conflicting; and review is either opted out (`review.opted_in` is `false`), approved (`review.verdict` is `approved`) with `review.head_sha` equal to `pr.head_sha`, **or** waived by a `fail_open` verdictless ceiling reached in Row 10 this tick.\n\nWhen the waiver path is what reached this row, journal the shared token `review_waived_verdictless_fail_open` alongside the merge line and say plainly that the merge proceeded **on stable CI evidence alone**. That token is the same string the v2 conductor records for the same degradation, so one grep finds every merge that advanced without a verdict whichever conductor drove the epic. Never write it in language that claims the review passed or approved the pull request \u2014 it names what was missing, not what was satisfied.\n\nThen call the `merge_pull_request` MCP tool with exactly `pr_number` set to `pr.number` and `expected_head_sha` set to `pr.head_sha`. **The expected SHA is derived only from the fresh Stage 2 status** \u2014 never from the checkpoint, never from a worker's report, never from an earlier tick. The checkpoint deliberately stores no expected head; merge identity always comes from a freshly observed `pr.head_sha`. Prepare `counters.merge_attempts` = the Stage 2 value plus one for **every** invocation of the tool, successful or not.\n\nMap the returned envelope:\n\n- **`merged` is `true`** \u2014 the only success. It covers `outcome: merged` and `outcome: already_merged`, both of which carry that boolean. Prepare `status=merged` and top-level `counters.merges` = the Stage 2 value plus one.\n- **`outcome: refused` with `reason: head_sha_drift`** \u2014 the head moved under the merge. Journal the complete envelope (including `actual_head_sha`) and take a fresh status snapshot on the next tick. Never retry with the stale SHA.\n- **Outcome `lease_held`, `review_not_approved`, or `unknown`, or any envelope carrying `retry_hint: retry_later`** \u2014 journal it and wait for the next reconciliation tick.\n- **Outcome `dry_run`, `pending_approval`, `gate_unresolved`, `action_key_mismatch`, `review_unavailable`, `review_source_unsupported`, `error`, or any `refused` result carrying `retry_hint: needs_human`** \u2014 select `NEEDS_HUMAN:merge_blocked`. Preserve the **complete** envelope as the evidence, including `hint`, `actual_head_sha`, `ci_summary`, `paths`, and `http_status` whenever those are present; `hint` is usually the exact operator fix. **JSON-stringify that envelope into a bounded, secret-free string** \u2014 `evidence` is string data, never an object (see Stage 4).\n\n**Fail-closed**: only `merged: true` is success. A missing, `false`, or malformed `merged` value is never treated as a merge, no matter what `outcome` says alongside it.\n\n### Row 13a \u2014 Pull request closed without being merged\n\nWhen `pr.state` is `CLOSED` and the pull request was not merged, the ticket's work has been abandoned on GitHub and nothing this loop does can advance it. Select `NEEDS_HUMAN:merge_blocked`, with bounded string evidence that identifies `pr.state: CLOSED` along with `pr.number`. **Fail-closed** \u2014 a closed pull request is never respawned into, reopened, or merged by this loop.\n\n### Row 14 \u2014 Local-mode ticket operation refused\n\nWhen a ticket operation returns `409 UNSUPPORTED_IN_LOCAL_MODE`, tolerate it and journal it. The repository is running the local ticket backend, where that response is the documented terminal answer rather than a failure. It introduces **no** new parking reason. **Fail-open.**\n\n### Row 15 \u2014 No row matched: journal the snapshot and do nothing else\n\nWhen no row above matches, that is the tick's outcome, not a licence to improvise. Journal a concise summary of the Stage 2 snapshot, take **no** external action \u2014 no MCP tool call, no spawn, no merge, no parse \u2014 and change **no** row-specific checkpoint field. The single `checkpoint set` this tick writes therefore carries only the universal `counters.iterations` update and its one journal line.\n\nThis row exists because unmatched snapshots are real and reachable: a `stale_for_seconds` or `elapsed_since_spawn_seconds` that is `null` because nothing has been observed yet; a pull request whose CI is complete but not yet stable across two polls. Each of those is a legitimate \"wait for reality to move\" state, and a tick that improvised an action for it would be acting on evidence it does not have. **Fail-open.**\n\nAn open pull request awaiting a review whose `verdict` is still `unknown` is **no longer** one of these. Row 10 now matches that snapshot, counts it, and eventually decides \u2014 falling through to here would be the unbounded wait the ceiling exists to end.\n\n### Shared mechanics for every targeted session\n\nRows 7, 9, 11, and 12 spawn a session the same way. The four kinds are exactly `continue`, `ci_fix`, `review_fix`, and `conflict`.\n\n**First, write the prompt file** with the Write tool, at:\n\n```\n~/.config/bridge/conduct/<repo>/<EPIC>/prompts/<KEY>-<kind>-<n>.md\n```\n\nwhere `<EPIC>` and `<KEY>` are the validated keys, `<kind>` is one of the four kinds above, and `<n>` is the applicable absolute attempt number. **`<repo>` is the repository component of the resolved `checkpoint_path` that Stage 2's `status` returned** \u2014 read it from there rather than re-deriving it from credentials, from `BAPI_REPO_NAME`, or from anything remembered in conversation. `status` resolves that path itself, including any `--checkpoint-path` override and any `XDG_CONFIG_HOME` redirection, so it is the only value guaranteed to match where the CLI actually keeps this epic's state.\n\n**End every prompt with this exact wording**, so the spawned worker releases its worktree cleanly instead of lingering:\n\n```\nAfter the final pipeline step completes, cleanly end your worker session (for example by issuing the `/exit` command) so the worktree is released and no idle process lingers \u2014 but **only when no follow-up remains that you still own**. Do **not** exit while any of the following is true:\n\n- there are unresolved CI failures you are still correcting (the post-PR CI-correction loop in the CI-monitoring step still owns work),\n- review changes were requested and you have not yet addressed them,\n- there is a merge conflict on your PR that you still own,\n- you have unpushed local commits.\n\nExit only after your final branch state is pushed, the done-gate / CI-monitoring workflow required by the recipe has completed, and no CI/review follow-up remains. A clean `SessionEnd` is both the correct terminal lifecycle signal and the point at which the worker should exit.\n```\n\n**Then spawn**, forwarding `--checkpoint-path '<p>'` only when supplied:\n\n```\n<launcher> conduct-epic spawn '<EPIC>' --ticket '<KEY>' --prompt-file '<path>'\n```\n\n`spawn` opens exactly **one** agent tab in the ticket's `worktree_path` running the prompt file's contents. It refuses when the worktree is missing, the prompt file is unreadable, or the lock is held by another live process.\n\n**The budgets are this command's job, not the CLI's.** `spawn` never checks them: it will happily open a fifth tab if asked. One targeted respawn **shared** across Rows 7, 9, and 11 \u2014 a single per-ticket `respawns` counter, not one allowance per row \u2014 and two conflict sessions in Row 12, are enforced here, by reading the Stage 2 `respawns` and `conflict_attempts` before choosing the row.\n\nAfter a **successful** spawn, prepare `counters.sessions_spawned` = the Stage 2 value plus one. If the spawn command itself fails, do **not** advance `respawns`, `conflict_attempts`, or `counters.sessions_spawned` \u2014 a session that never opened has consumed no budget.\n\nKeep credentials, raw environment values, and unrelated command output out of prompt files and out of the spawn command's arguments. The spawned agent resolves its own credentials.\n\n## Stage 4 \u2014 Checkpoint and Stop\n\nEvery tick that reaches this stage ends with **exactly one** checkpoint command and **exactly one** journal line. There are **three exemptions**, and they divide into two kinds:\n\n- **Two print-only parks, before Stage 3.** `init_failed` (Stage 1) and `foreign_lock` (Stage 2) stop the tick *before* Stage 3 and write nothing durable at all \u2014 they print their `NEEDS_HUMAN:` line and one stdout journal line and stop. Because they never reach Stage 3 they also never increment `counters.iterations`.\n- **The `all_done` tick, inside Stage 3.** Row 1 reaches Stage 3 but has **no in-flight ticket**: `all_done` is `true` exactly when `ticket` is `null`, and `checkpoint set` requires `--ticket <KEY>`. That tick runs `finish`, opens the epic pull request, writes **no** checkpoint, and \u2014 as the single stated exception to the rule below \u2014 does **not** increment `counters.iterations`.\n\nEvery other tick, including a Row 15 fallthrough, writes here. Run, forwarding `--checkpoint-path '<p>'` whenever the user supplied it:\n\n```\n<launcher> conduct-epic checkpoint set '<EPIC>' --ticket '<KEY>' --field <name> <absolute-value> \u2026 --journal '<line>'\n```\n\nRepeat `--field <name> <absolute-value>` once per changed field, and pass `--journal '<line>'` exactly once. Do not issue a second `checkpoint set` in the same tick, and do not split the fields across two invocations \u2014 one tick, one auditable write.\n\n**Every value is absolute, computed from the Stage 2 snapshot.** Relative or guessed increments are prohibited: the CLI stores what it is given, so a \"+1\" that was never resolved against a fresh read silently corrupts the count. Compute `n + 1` from the Stage 2 value for `counters.sessions_spawned`, `respawns`, `conflict_attempts`, `counters.merge_attempts`, `counters.iterations`, and `counters.merges`.\n\n`review_verdictless_observations` follows the same absolute rule with one addition: when `ticket.review_verdictless_for_sha` does not equal `pr.head_sha`, the absolute value is `1` rather than `n + 1`, because the counter is bound to a head and resets when the head moves. `review_verdictless_for_sha` is written as the observed `pr.head_sha`. Row 10 owns both fields; no other row writes them.\n\nInclude only the fields the selected row actually affected \u2014 typically some of `status`, `spawned_at`, `respawns`, `conflict_attempts`, `review_verdictless_observations`, `review_verdictless_for_sha`, `counters.sessions_spawned`, `counters.merge_attempts`, `counters.iterations`, and `counters.merges`.\n\n**`parse_requested_at` and `parse_requested_for_sha` are no longer written by any row.** The CLI still accepts them so an older checkpoint stays readable, but Row 5 now reads the `scope` sub-object \u2014 the server's own answer about whether this merge was indexed \u2014 rather than recording a request and timing it. Writing them would record evidence nothing reads.\n\n**`counters.iterations` increments exactly once for every tick that reaches Stage 3**, and it is written in that tick's single `checkpoint set` as the Stage 2 absolute value plus one. It is the one field every such tick updates, including a Row 15 fallthrough \u2014 which is why a fallthrough tick's checkpoint contains only `counters.iterations` and its journal line, with no status, retry, merge, or parking mutation. The two print-only parks never reach Stage 3 and so never increment it, and the `all_done` tick reaches Stage 3 but writes no checkpoint, so it does not increment it either.\n\n**Parking** adds two fields to the same single command:\n\n```\n--field status needs_human --field needs_human '{\"reason\":\"<reason>\",\"evidence\":\"<bounded secret-free JSON-stringified envelope or output>\",\"at\":\"<ISO-8601 timestamp>\"}'\n```\n\n**`evidence` is a JSON string, never an object.** The CLI's checkpoint schema accepts only `{reason: string, evidence: string, at: string}` and rejects anything else outright, so an object-valued `evidence` makes `checkpoint set` exit non-zero: the `NEEDS_HUMAN:` line prints, the park never persists, and the next tick repeats the failing action. When the evidence is structured \u2014 a merge envelope, a command's output \u2014 JSON-stringify it and escape every embedded quote and control character so the result is a single valid JSON string value. Keep it bounded and secret-free.\n\nThe `reason` is one of the closed list below and `at` is an ISO-8601 timestamp. Every `NEEDS_HUMAN:<reason>` line printed by a stage carries the **same** evidence that is persisted here \u2014 the printed line and the checkpoint never disagree.\n\nThe parking vocabulary is closed \u2014 **eleven reasons** and no others \u2014 and it has two partitions:\n\n- **Nine persisted reasons**, each written durably by the single `checkpoint set` above: `stalled`, `ci_red`, `review_changes_requested`, `merge_blocked`, `conflict`, `parse_failed`, `shadow_stale_deadline`, `wrong_base`, and `review_verdictless_ceiling_reached`. A persisted park is what makes the *next* tick report `already parked` and stop.\n - `shadow_stale_deadline` is Row 5 branch 4's reason, and it is deliberately **the same token the v2 conductor parks under** for the same condition. Both conductors reaching for one string is what lets an operator grep for a refused epic advance without first working out which conductor drove the epic. It is distinct from `parse_failed`: `parse_failed` means the index generation broke, while `shadow_stale_deadline` means the index refused to accept the branch advance at all.\n - `review_verdictless_ceiling_reached` is Row 10's park, and it is **byte-identical to v2's own token** for the same reason `shadow_stale_deadline` is shared: one grep finds a verdictless ceiling whichever conductor drove the epic. Four alternatives were considered and rejected. `stalled` is the label this row exists to stop emitting \u2014 it says the worker died when what actually died was the reviewer. `merge_blocked` is wrong because the merge tool was never called, and its evidence table is built entirely around merge envelopes. `review_changes_requested` is factually false: nobody requested changes, nobody said anything. And a fresh `review_unavailable` token would collide with the merge tool's existing `review_unavailable` *outcome*, which Row 13 already maps to `merge_blocked` \u2014 two different conditions answering to one string is exactly the confusion a closed vocabulary exists to prevent.\n- **Two print-only reasons**, which are printed and journaled to stdout for the current invocation only and write nothing durable: `init_failed` and `foreign_lock`. Neither may call `checkpoint set`. A print-only park leaves no durable record, so it does not produce an `already parked` tick \u2014 the next tick reconciles from scratch and reports the condition again if it persists.\n\nDo not invent a new reason; a genuinely new failure mode is a change to this command and to the BAPI-805 runbook together.\n\nThe journal line is one line containing the ISO-8601 time, the selected action, and concise evidence. Print it **last**, after the checkpoint command has succeeded, so the operator's final line of output is the tick's durable record.\n\nEvery dynamic value in this stage follows the Stage 0 single-quote rule \u2014 the epic key, the ticket key, the checkpoint path, the `needs_human` JSON, and the journal line are each escaped (`'` \u2192 `'\\''`) and wrapped in single quotes. Credentials never appear in a checkpoint argument or in journal evidence.\n\n## Operational Guarantees\n\n- **Spec freshness is `/review-and-start`'s job, not a separate check.** Each ticket's review phase runs in a worktree cut from the current `epic/<EPIC>` tip, so its review and its plan already see every predecessor's merged code. This command runs no separate spec-freshness check and needs none.\n- **The checkpoint plus GitHub are the resume point.** Nothing relies on conversation memory. A sleeping laptop merely misses ticks; the next invocation reconciles from scratch and continues where reality actually is.\n- **This command never creates an `epic_run`.** It must never be combined with `setup-epic` on the same epic \u2014 the v2 conductor stays active there, and two authorities transitioning one epic is exactly the failure this pivot removes.\n- **`/loop 5m /conduct-epic <EPIC>` is the driver.** The operator runbook is BAPI-805's, not this file's.\n- **Recovery is bounded**: one targeted respawn *shared* across Rows 7, 9, and 11, and two conflict sessions, then park. There is no third chance and no escalating retry.\n- **The first spawn relies on BAPI-801's `BAPI_BASE_BRANCH` contract**, while every tick still independently verifies the observed `pr.base` (Row 2). The export makes the right thing happen; the check catches it when it does not.\n","council.md":'Convene a multi-perspective council on a task via Bridge API and save the resulting report locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\nParse `$ARGUMENTS`. The supported invocation is exactly:\n\n```text\n/council <task description> [--mode technical|design|discovery|general] [--debate] [--lenses a,b] [--ticket PROJ-123]\n```\n\nParsing rules:\n\n- Keep every non-flag token in its original order; the joined result is the required `task_description`. Remove each recognized flag, and the value token that belongs to it, from that text.\n- `--mode <value>` accepts exactly `technical`, `design`, `discovery`, or `general`. When `--mode` is omitted, the selected mode is `technical`.\n- `--debate` is a valueless boolean flag. It takes no following token.\n- `--lenses <a,b>` takes one comma-separated value. Split it on commas and keep the non-empty entries as the `lenses` array.\n- `--ticket <KEY>` captures the immediately following token as the ticket key.\n- A missing value for `--mode`, `--lenses`, or `--ticket` \u2014 including a value position occupied by another recognized flag \u2014 is a validation failure. Never let the next flag become a flag\'s value.\n\nValidation must finish before any MCP tool call. Stop immediately, display the usage response below, and make no tool call when `$ARGUMENTS` is empty, when it contains only flags, when a flag that needs a value has none, or when `--mode` is given an unsupported value:\n\n```text\nUsage: /council <task description> [--mode technical|design|discovery|general] [--debate] [--lenses a,b] [--ticket PROJ-123]\nExample: /council "How should we add rate limiting to the LLM client?" --mode technical\n```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall `get_docs_dir` (no parameters). Store the returned path as `docs_dir`. This is context only \u2014 do not slugify it, predict a filename from it, or otherwise construct a report path yourself.\n\n## Step 3 \u2014 Convene the Council\n\nBefore calling the tool, tell the user calmly what to expect:\n\n```text\nConvening the council. This commonly takes around 15 minutes, and may continue in the background if the client deadline expires.\n```\n\nThen call `request_council` with:\n\n- `task_description`: the parsed task text\n- `mode`: the selected mode\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `debate`: `true` \u2014 include this parameter **only** when `--debate` was supplied\n- `lenses`: the parsed array \u2014 include this parameter **only** when `--lenses` was supplied\n- `ticket_number`: the captured key \u2014 include this parameter **only** when `--ticket` was supplied\n\nOmit an optional parameter entirely rather than sending a placeholder: never send `debate` with a false value, never send an empty `lenses` array, and never send an empty `ticket_number` string. Do not send any other parameter \u2014 no `providers`, no `concerns`, no prior `brainstorm_id` to refine, and no lens pair of your own. Omitted `lenses` already defaults server-side; do not re-implement that default here.\n\n## Step 4 \u2014 Report the Outcome\n\nKeep the report status-first and compact: status, then the next action, then supporting detail such as the saved path, `brainstorm_id`, or mode.\n\n**Completed.** The tool appends a `Saved files:` block listing one `- <path>` line per saved report. Collect those lines as `saved_paths`; each entry is a `saved_path` reported by the tool. Display them before any optional task, mode, or `docs_dir` context, and never invent or predict a filename:\n\n```text\nCouncil complete.\nSaved to: {saved_path}\n```\n\n**Backgrounded.** A response that exceeded the client deadline but carries a `brainstorm_id` is a successful submission, not a failure. Do not display "failed", an error banner, or unrecoverable-error wording for it. Display the exact returned id and the recovery action:\n\n```text\nCouncil submitted and still running in the background.\nRetrieve it with `get_council` using {"brainstorm_id": "<the exact id returned>", "save_locally": true}.\n```\n\n**Not indexed.** When a `technical` or `discovery` request reports that the repository is not indexed, say so and name the workaround \u2014 those two modes are codebase-grounded and need an indexed repository, while `general` needs no index:\n\n```text\nThis repository is not indexed, and {mode} mode needs an indexed repository.\nRerun the same task with `--mode general`.\n```\n\n**Failed.** A tool error that carries no `brainstorm_id` is a genuine failure. Surface the tool\'s own actionable message, stop, and do not invent a retrieval handle:\n\n```text\nCouncil failed: <error message from the tool>\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```text\n## Council Report\n\n- **Saved to**: {saved_path}\n- **Task**: <task_description>\n- **Mode**: <selected mode>\n- **Status**: Completed\n```\n\nFor a backgrounded council, replace the saved-path line with the returned `brainstorm_id` and the `get_council` recovery action, and set the status to `Submitted \u2014 running in the background`.\n',"create-doc.md":'Generate a design document (TDD, FSD, or PRD) for a Jira ticket, wait for the result, and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, a required `--doc-type` flag, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--doc-type` appears followed by one of `tdd`, `fsd`, or `prd`, capture that as `doc_type`.\n - If `--doc-type` is absent, or is followed by anything other than `tdd`/`fsd`/`prd` (or is the last token), stop immediately and report: "Usage error: --doc-type requires a document type (tdd, fsd, or prd)."\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate ticket key format**: Confirm the ticket key matches the Jira key pattern `[A-Za-z][A-Za-z0-9]+-\\d+`. If it does not match (or `ticket_key` is empty or missing), stop immediately and display:\n\n ```\n Usage: /create-doc <ticket_key> --doc-type <tdd|fsd|prd> [--second-opinion [provider]] [--provider <name>] (e.g., /create-doc BAPI-150 --doc-type fsd)\n ```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Design Document\n\nCall the `create_doc` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `doc_type`: the parsed `doc_type` (`tdd`, `fsd`, or `prd`)\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nThis step may take 2-4 minutes while the backend processes the document.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nDesign document generation failed: <error message from the tool>\n```\n\nIf generation did not finish, the document can be retrieved later with the `get_doc` MCP tool using the same `ticket_number` and `doc_type`.\n\n## Step 4 \u2014 Confirm Success\n\nResolve the local file path from `doc_type`:\n- `tdd` \u2192 `{docs_dir}/architecture/<ticket_key>-architecture-plan.md`\n- `fsd` \u2192 `{docs_dir}/fsd/<ticket_key>-fsd-plan.md`\n- `prd` \u2192 `{docs_dir}/prd/<ticket_key>-prd-plan.md`\n\nDisplay a confirmation message:\n\n```\nDesign document generated successfully for <ticket_key>\nSaved to: <local file path>\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Design Document Report\n\n- **Ticket**: <ticket_key>\n- **Doc Type**: <doc_type>\n- **Status**: Generated successfully\n- **Local File**: <local file path>\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n',"create-pr.md":'# Create PR: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), fetches the ticket summary, determines the base branch, and creates a pull request on the configured VCS provider. It is designed to run after `/commit-ticket` completes.\n\nIf any critical stage fails (Stage 0), stop immediately and report which stage failed and why. Non-critical stages (Stage 1 and Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 3-stage pipeline to create a pull request for a Jira ticket. Execute all stages in sequence.\n\n## Stage 0 \u2014 Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` \u2014 one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: \'<value>\'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /create-pr <ticket_key> (e.g., /create-pr BAPI-150)\n ```\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\n3. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `head_branch`. Verify that `head_branch` contains the `ticket_key` (case-insensitive comparison). If the branch does not contain the ticket key, stop immediately and display:\n\n ```\n Current branch \'<head_branch>\' does not contain ticket key <ticket_key>.\n Please switch to the correct feature branch before running /create-pr.\n ```\n\n4. **Resolve base branch**: Resolve the base through this ordered precedence and take the first tier that yields a usable value.\n\n 1. **`BAPI_BASE_BRANCH` from the environment, when set and non-empty.** Read it first, explicitly, with Bash \u2014 never infer the base from branch ancestry or the repository default branch:\n\n ```bash\n echo "${BAPI_BASE_BRANCH:-}"\n ```\n\n The `:-` form returns an empty line when the variable is unset, so the read never fails the stage. The packaged `start-tickets` exports this variable into a worker\'s shell for **every** resolved run base \u2014 the ordinary `main` case included, not only an epic branch \u2014 so under a packaged spawn this tier always wins over the repository-wide configured value.\n 2. **The repository\'s configured base branch** \u2014 only when the environment value is unset. Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch`.\n 3. **`main`** \u2014 the expected fallback default.\n\n Tiers 2 and 3 exist for a workflow where the environment contract is genuinely absent: `/create-pr` invoked by hand, or a legacy worker started outside packaged `start-tickets`. They are not the normal packaged-worker path \u2014 a packaged worker always arrives with `BAPI_BASE_BRANCH` set.\n\n Treat a null, empty, or whitespace-only value, an HTTP 400 Validation Error / Invalid field name, or any lookup error as not set, and fall back to `main` rather than failing the stage. Store the resolved value as `base_branch`.\n\n5. **Fetch ticket summary**: Call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key`. Extract the ticket summary from the response. If the tool returns an error, log a warning and use a generic summary based on the ticket key.\n\n6. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Create Pull Request\n\n1. **Compose PR title**: Format the title as `<ticket_key>: <ticket_summary>`. Truncate to 72 characters if needed.\n\n2. **Compose PR body**: Build a PR body that includes, in this order:\n - A brief description derived from the ticket summary\n - A plain text reference to the local implementation plan: `Implementation Plan available locally at {docs_dir}/plans/{ticket_key}-plan.md` (do not use markdown hyperlink syntax \u2014 the local path is sufficient for team members pulling the branch)\n - The checklist text of `.github/PULL_REQUEST_TEMPLATE.md`, read from the current worktree when that file exists and appended after the plan reference without rewriting its markdown structure. Omit this part when the file is absent. GitHub\'s REST API does not automatically apply the repository pull request template \u2014 it is a web-UI affordance \u2014 so the checklist must be inlined into the body here or the created PR has none.\n\n3. **Create the pull request**: Call the `create_pull_request` MCP tool with:\n - `head_branch`: the current branch from Stage 0\n - `base_branch`: the resolved base branch from Stage 0\n - `title`: the composed PR title\n - `body`: the composed PR body\n\n4. **Handle the response with graceful degradation**:\n - If the response contains `available: false`: Report the reason to the user and skip to Stage 2. Do not halt the pipeline.\n - If the response contains `created: false`: Log "PR already exists" and store the returned PR URL. Continue to Stage 2.\n - If the response contains `created: true`: Store the PR URL. Continue to Stage 2.\n - If an HTTP error occurs: Warn the user with the error details and continue to Stage 2. Do not halt the pipeline.\n\nThis stage is **non-critical** \u2014 warn on failure, continue to Stage 2 regardless.\n\n## Stage 2 \u2014 Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Pull Request Report\n\n**Ticket**: <ticket_key>\n**Branch**: <head_branch>\n**Base Branch**: <base_branch>\n**PR URL**: <pr_url or "N/A \u2014 see warnings">\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 1: PR creation failed or unavailable),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** \u2014 display the report regardless.\n\n## Final Report\n\nOn success, display the structured report from Stage 2 confirming that the pull request was created (or already existed), including the branch name, base branch, PR URL, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0), display which stage failed and the error details.\n',"critique-ticket.md":'Generate a ticket quality critique and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command triggers an AI-powered critique of a Jira ticket and saves the result locally. **No human confirmation gates** \u2014 the command runs end-to-end without pausing. `$ARGUMENTS` should contain a single Jira ticket key in `PROJECT-NUMBER` format (e.g., `BAPI-123`).\n\nIf any step fails, stop immediately and report which step failed and why.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate the ticket key format**: Validate that `ticket_key` matches the regex pattern `^[A-Za-z][A-Za-z0-9]+-\\d+$`. If validation fails, stop immediately and report: "The argument does not match the expected `PROJECT-NUMBER` format. Example: `BAPI-123`."\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Critique\n\nCall the `request_ticket_critique` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nIf the tool returns an error, stop immediately and report: "Critique generation failed." Include the error details.\n\n## Final Report\n\n**On success**, display a summary including:\n\n- Path to the saved critique document: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nNote: The critique was NOT pushed to Jira. To incorporate the critique findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n\n**On failure at any step**, stop immediately and display the step that failed and the error details.\n',"decision-page.md":'Turn open decisions from this conversation into an interactive HTML decision page, then fold the answers back in.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is a free-form description of what needs deciding \u2014 a topic ("how we handle rate limiting"), a list of specific questions, or empty. It is **not** a Jira ticket key.\n\nThis command exists so a decision page can be reached in ordinary conversation, without running a larger automation. A decision page frames each open choice as a card \u2014 the question, why it matters, 2-4 concrete options with the consequence of each, and a recommendation \u2014 and renders it as a local HTML page the user submits from their browser. The submitted JSON comes back to you and the decisions become settled.\n\nUse it whenever a conversation has accumulated more open choices than are comfortable to settle in prose. Do not use it to ask one simple question \u2014 ask that directly.\n\nRun every stage in the main conversation so the user sees the framing as it happens. If a stage fails, say which one and why.\n\n## Stage 1 \u2014 Frame the decisions\n\n1. **Gather the candidates.** Take the decisions named in `$ARGUMENTS` plus any open choice raised earlier in this conversation and not yet settled. If `$ARGUMENTS` is empty, use the conversation alone. If you find nothing genuinely open, say so and stop \u2014 do not manufacture cards to fill a page.\n\n2. **Write one card per decision.** Each card needs:\n - `id`: a short stable id, e.g. `D-1`, `D-2`. Ids must be unique \u2014 a duplicate is rejected, because the id is the key the user\'s answer is reported under.\n - `question`: the decision itself, phrased as a question.\n - `options`: 2-4 concrete option labels. Do **not** include "None of these" or "Ask about this" \u2014 the renderer appends both automatically, and passing "None of these" yourself is rejected.\n - `option_consequences`: one consequence per option, **parallel to and the same length as** `options`. Say what actually follows from choosing it, not a restatement of the label.\n - `why_it_matters`: the concrete impact of getting this wrong.\n - `recommendation_explanation`: why the recommended option is best.\n - `recommendation_index`: the 0-based index of the recommended option, within range of `options`.\n - `codebase_evidence` (optional): your assessment plus `file:line` citations, shown collapsed behind a disclosure.\n\n Give a real recommendation on every card. If one option is obviously right, still supply the strongest alternative as a second option so the user can see what they are ruling out.\n\n3. **Show the list and let the user correct it.** Present the questions and options in chat before rendering anything. The user may add a decision you missed, drop one that is already settled, or reject your framing of a question. Apply their corrections, then proceed. This check is cheap; a page built on the wrong questions is not.\n\n## Stage 2 \u2014 Render the page\n\n1. **Pick a slug.** Derive a kebab-case slug from the topic \u2014 a few meaningful words, lowercase, non-alphanumerics stripped, at most 60 characters. It **must** match `/^[A-Za-z][A-Za-z0-9_-]*$/`; if it would start with a digit or hyphen, prefix it with `decisions-`. This slug is the `ticket_key`, which accepts any such slug and does not have to be a Jira key.\n\n2. **Call `generate_decision_page`** with the routing fields at the root and everything else nested under `content`. **The nesting is required** \u2014 `actionable_items`, `system_goals`, `clear_improvements`, and `implementation_order` passed at the root are silently dropped by the tool\'s lean input schema, and a call with no `content` at all is rejected.\n - `ticket_key`: the slug.\n - `artifact_type`: `review_decisions` (the default).\n - `output_subdir`: `decisions`.\n - `output_filename`: `{slug}-decisions.html`.\n - `labels`: optional presentation overrides \u2014 `title`, `intro`, `section_heading`. Set a `title` that names the topic, and an `intro` that says what agreeing to these choices commits the user to.\n - `content`: an object holding `actionable_items`.\n\n ```typescript\n interface DecisionPageContent {\n actionable_items: Array<{\n id: string; // e.g. "D-1"; must be unique\n question: string;\n why_it_matters: string;\n recommendation_explanation: string;\n options: string[]; // 2-4 labels (no "None of these" / "Ask about this")\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based within options\n codebase_evidence?: string; // optional: assessment + file:line citations\n }>;\n }\n ```\n\n Example call:\n ```json\n {\n "ticket_key": "rate-limiting",\n "artifact_type": "review_decisions",\n "output_subdir": "decisions",\n "output_filename": "rate-limiting-decisions.html",\n "labels": { "title": "Rate Limiting Decisions", "section_heading": "Open Decisions" },\n "content": {\n "actionable_items": [\n {\n "id": "D-1",\n "question": "Where should the limit be enforced?",\n "why_it_matters": "Determines whether a burst is rejected before or after it reaches the database.",\n "recommendation_explanation": "Middleware keeps the limit in one place and protects every route without per-handler work.",\n "options": ["In middleware", "Per handler"],\n "option_consequences": ["One place to change; blunt for routes that need different budgets.", "Precise per route; every new route must remember to opt in."],\n "recommendation_index": 0,\n "codebase_evidence": "api/routes/__init__.py:41 already composes shared dependencies for every router."\n }\n ]\n }\n }\n ```\n\n3. **When the decisions come with framing worth showing**, use `artifact_type: "pre_ticket_planning"` instead and add a `system_goals` object inside `content` (`business_goal`, `desired_end_state`, `system_behavior`, and optionally `acceptance_criteria` and `nfrs`). Those render read-only above the cards, each with its own agree / ask / disagree control. Use this when the user needs to see the goal the decisions serve in order to answer them; the plain `review_decisions` page is the right default otherwise.\n\n4. **Handle the response `status`:**\n - `decision_page_generated`: surface the returned `file_path` and go to Stage 3.\n - `no_decisions_needed`: no page was written because there was nothing to render. Tell the user, and do not proceed to Stage 3.\n - `VALIDATION_ERROR`: the message names the field and restates the expected shape. Fix the payload and retry once. If it fails again, report the message verbatim rather than guessing further.\n\nIf the tool fails outright, **output a highly visible warning** (e.g. **\u26A0 WARNING: The decision page could not be generated** in bold) and fall back to settling the decisions in chat, one at a time. Do not continue silently \u2014 the failure must be visible in your output.\n\n## Stage 3 \u2014 Capture the answers (stop and wait)\n\n1. **Direct the user to the page.** Give them the `file_path` and tell them to open it in their browser. Explain that they can accept a recommendation, pick another option, reject them all, or flag a card for discussion, and that they can ask you questions in chat before submitting.\n\n2. **Treat each message as a commit or a discussion turn.**\n - **Commit:** trim the message and try to parse the whole trimmed message as JSON. Treat it as a commit only when the result is an object with all three top-level fields: `ticket_key` (string), `decisions` (object), and `general_comment` (string). The first valid commit-shaped paste commits \u2014 do not over-validate the individual cards.\n - **Discussion:** anything else. Answer it, then keep waiting. If a JSON-shaped paste is missing one of the three fields, say which one rather than treating it as a freeform question.\n - **In-flight overrides:** if the user changes an answer in chat ("go with per-handler for D-1"), record it as an override. On commit, the submitted JSON is the baseline and your recorded overrides win; acknowledge each overridden card in one line.\n\n3. **Resolve every "ask" (hard rule).** After accepting a commit, find every item in `decisions` where `choice === "ask"`. For each, present the evidence and keep discussing until the user gives an explicit answer. Do not proceed while any `ask` is unresolved, and do not honor "just skip those" \u2014 an unanswered card is an unmade decision.\n\n4. **Handle "None of these".** A `choice` of `"none"` means every option you offered was wrong. Ask what the user would do instead and record their answer as the decision. Do not re-render the page for this.\n\n**You MUST stop and wait for the user here.** Do not assume answers, do not proceed on the recommendations, and do not move to Stage 4 until the user commits or explicitly declines. If they decline, say the decisions are unsettled and stop.\n\n## Stage 4 \u2014 Fold the answers back\n\n1. **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\n Consider 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\n Emit 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\n If the review cannot be produced, report that in one line and continue without stalling the workflow or presenting the gate below.\n\n This review stays in chat: there is no document for this command to update.\n\n Then 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\n Literal `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\n2. **Restate every decision as settled**, in a short list: the question, the chosen answer, and \u2014 where the choice went against your recommendation or came from an override \u2014 one line on what changes as a result.\n\n3. **Carry `general_comment` as overarching guidance.** It applies across all the decisions, not to any one card. Say plainly how it changes the picture.\n\n4. **Name what these decisions now constrain.** One or two sentences on what is now fixed for the rest of the conversation. From here on, treat the settled answers as the contract \u2014 if later work would contradict one, say so and ask rather than quietly re-deciding.\n\nThere is no document to rewrite. The conversation is where the decisions live, unless the user asks you to record them somewhere.\n',"estimate-epic.md":"Estimate an entire Jira Epic or an explicit ticket-key group via the shared epic estimation orchestrator.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is either a Jira Epic key (e.g. `BAPI-518`) or an explicit `--tickets` key list \u2014 never both. This command calls the `estimate_epic` MCP tool, which delegates to the Bridge API epic estimation orchestrator, and renders the structured result.\n\nIf any step fails, stop immediately and report which step failed and why, preserving the user's originally entered epic key or ticket list in the report.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract exactly one key-source input, plus an optional `--allow-partial` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--tickets` appears, every token after it (up to the next flag or end of input) is the explicit ticket-key list \u2014 this is the `ticket_keys` mode.\n - Otherwise, the first token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`) is the `epic_key` \u2014 this is the epic mode.\n - `--allow-partial` may appear anywhere; if present, set `allow_partial_value = true`. If absent, omit `allow_partial` entirely (do not pass `false`).\n - Never resolve both an `epic_key` and a `ticket_keys` list from the same invocation \u2014 usage is one mode or the other.\n\n2. **Validate input**:\n - Usage forms: `/estimate-epic EPIC-KEY` or `/estimate-epic --tickets KEY-1 KEY-2 ...`, plus optional `--allow-partial`.\n - If neither an `epic_key` nor a `--tickets` list can be resolved, stop immediately and report:\n ```\n Usage: /estimate-epic EPIC-KEY [--allow-partial]\n /estimate-epic --tickets KEY-1 KEY-2 ... [--allow-partial]\n ```\n - If `--tickets` is present but followed by zero keys, stop immediately and report: \"`--tickets` requires at least one ticket key.\"\n - Do not invent or pass a `mode` parameter \u2014 there isn't one; the tool infers the source from whichever of `epic_key`/`ticket_keys` is supplied.\n\n## Step 2 \u2014 Call the Tool\n\nCall the `estimate_epic` MCP tool with:\n- `epic_key`: the resolved epic key \u2014 **only** when in epic mode. Omit entirely in ticket-key mode.\n- `ticket_keys`: the resolved ticket-key list \u2014 **only** when in ticket-key mode. Omit entirely in epic mode.\n- `allow_partial`: `allow_partial_value` if `--allow-partial` was passed; omit entirely otherwise (never pass `null`, an empty string, or an empty array for any absent field).\n\nNever pass both `epic_key` and `ticket_keys` in the same call.\n\nIf the tool returns an error envelope (a JSON object with an `error` field), stop and report the error message, preserving the epic key or ticket list the user originally entered.\n\n## Step 3 \u2014 Render the Result\n\nRender the successful result as a structured report \u2014 do not dump raw JSON by default:\n\n1. **Top**: the final estimate and its scale label (`estimate_label`) as the primary heading \u2014 this is the strongest element of the report.\n2. **Immediately after the summary**: `math_source`.\n3. **Next**: resolved child ticket keys (`child_ticket_keys`) and the per-child breakdown, presented compactly.\n4. **Only if non-empty**: a compact warning section listing `failed_child_keys` and `skipped_child_keys`.\n\nKeep the happy-path report concise and scannable. Use backticks for Jira keys and technical identifiers (e.g. `BAPI-518`).\n\n> Note: this tool does not accept a `recreate` parameter \u2014 the underlying epic estimation orchestrator (BAPI-522) always reuses cached child estimates and has no recreate knob to forward to.\n\n## Final Report\n\nOn successful completion, display a structured summary per Step 3 above. On failure, display the error message returned by the tool (or the usage error from Step 1), preserving the user's originally entered epic key or ticket list.\n","explore-ticket.md":`Explore the codebase for a task, settle its acceptance criteria with the user, then propose a design that meets them.
389
389
 
390
390
  $ARGUMENTS
391
391
 
@@ -2501,7 +2501,7 @@ or failed (with the CLI error).
2501
2501
  WHERE run_id = @run_id AND worker_id = @worker_id AND state = 'pending'
2502
2502
  AND julianday(available_at) <= julianday('now')
2503
2503
  ORDER BY seq ASC
2504
- 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 row of pending){if(toDelivered.run({seq:row.seq}).changes!==1)continue;insertRelayAuditEvent(db,{id:`message.delivered:${row.id}`,source:"conductor-worker",type:"message.delivered",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message delivered to worker",status:"delivered",details:{message_id:row.id,message_type:row.type}}}),toAcked.run({seq:row.seq}),insertRelayAuditEvent(db,{id:`message.acked:${row.id}`,source:"conductor-worker",type:"message.acked",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message acknowledged by worker",status:"acked",details:{message_id:row.id,message_type:row.type}}});let finalRow=reread.get(row.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("conductor-bin.js"),conductorNodePath:deps.execPath??process.execPath,hookBinPath:resolveBinPath("conductor-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 row of rows){let base={...row,runId:context.runId};if(row.status!=="created"||!row.path){out.push(base);continue}if(!isClaude){out.push(base);continue}let workerId=mintStartTicketsWorkerId(row.key,context.agentName,fragment?fragment():randomCorrelationFragment()),conductorEnv=buildConductorWorkerEnv(context,{workerId,ticketKey:row.key,worktreePath:row.path},deps.env),result=await provisionConductorHookForWorktree(row.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 createHash2}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 createHash2("sha256").update(json).digest("hex")}var GIT_CI_PRODUCER,GIT_HOOK_PRODUCER,REQUIRED_CI_CHECKS_GREEN,REVIEW_STATE,DEFAULT_GATE_NAME,REVIEW_PASSED,REVIEW_CHANGES_REQUESTED,CONTROL_CHAR_RE,SHA_RE,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",REVIEW_PASSED="review.passed",REVIEW_CHANGES_REQUESTED="review.changes_requested",CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/,SHA_RE=/^[0-9a-f]{40}$|^[0-9a-f]{64}$/}});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,adoptCurrentHeadAndUnparkTicket:()=>adoptCurrentHeadAndUnparkTicket,advanceEpicTicketStatus:()=>advanceEpicTicketStatus,approveEpicPlan:()=>approveEpicPlan,bootstrapConductorSupervisorDefaults:()=>bootstrapConductorSupervisorDefaults,buildConductorJiraUrl:()=>buildConductorJiraUrl,buildConductorVcsUrl:()=>buildConductorVcsUrl,buildEpicDispatchKey:()=>buildEpicDispatchKey,claimEpicSupervisionLease:()=>claimEpicSupervisionLease,createEpicRun:()=>createEpicRun,createEpicRunWithDisposition:()=>createEpicRunWithDisposition,createEpicTicketStatus:()=>createEpicTicketStatus,deletePullRequestBranch:()=>deletePullRequestBranch,extractSanitizedErrorDiagnostics:()=>extractSanitizedErrorDiagnostics,fetchActiveEpicRuns:()=>fetchActiveEpicRuns,fetchConductorConfigField:()=>fetchConductorConfigField,fetchConductorJsonPatchWithTimeout:()=>fetchConductorJsonPatchWithTimeout,fetchConductorJsonPostWithTimeout:()=>fetchConductorJsonPostWithTimeout,fetchConductorJsonPutWithTimeout:()=>fetchConductorJsonPutWithTimeout,fetchConductorJsonWithTimeout:()=>fetchConductorJsonWithTimeout,fetchConductorReadiness:()=>fetchConductorReadiness,fetchEffectiveSupervisorConfig:()=>fetchEffectiveSupervisorConfig,fetchEffectiveSupervisorSetup:()=>fetchEffectiveSupervisorSetup,fetchEpicRunState:()=>fetchEpicRunState,fetchParseStatus:()=>fetchParseStatus,fetchPrReviewStatus:()=>fetchPrReviewStatus,fetchShadowDispatchFreshness:()=>fetchShadowDispatchFreshness,getEpicPlan:()=>getEpicPlan,mergePullRequestForGate:()=>mergePullRequestForGate,parseConductorReadinessResponse:()=>parseConductorReadinessResponse,parseConductorSupervisorBootstrapResponse:()=>parseConductorSupervisorBootstrapResponse,pollCiChecksForCommit:()=>pollCiChecksForCommit,reconcileShadowMerge:()=>reconcileShadowMerge,recordEpicDispatch:()=>recordEpicDispatch,remediateEpicTicket:()=>remediateEpicTicket,replaceEpicRunPolicy:()=>replaceEpicRunPolicy,resolveConductorBridgeApiAccess:()=>resolveConductorBridgeApiAccess,safeDiagnosticMessage:()=>safeDiagnosticMessage,stopEpicRun:()=>stopEpicRun,storeEpicPlan:()=>storeEpicPlan,transitionEpicDispatch:()=>transitionEpicDispatch,transitionJiraStatus:()=>transitionJiraStatus,triggerRepositoryParse:()=>triggerRepositoryParse,unparkEpicTicket:()=>unparkEpicTicket,updateApprovedPlanNodeTicketSpec:()=>updateApprovedPlanNodeTicketSpec,updateEpicRunStatus:()=>updateEpicRunStatus,validateEpicPlan:()=>validateEpicPlan});import os3 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??os3.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(text4){return text4.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(text4){let redacted=redactErrorPreview(text4).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"):[],path53=(parts[0]==="body"?parts.slice(1):parts).slice(0,CONDUCTOR_VALIDATION_LOC_MAX_PARTS).join(".");return path53?`${path53}: ${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,errorCode4,message;if(Array.isArray(detail))message=formatValidationDetailItem(detail[0]);else if(detail&&typeof detail=="object"){let d=detail;typeof d.error_code=="string"&&(errorCode4=d.error_code),typeof d.message=="string"&&(message=d.message)}else typeof detail=="string"&&(message=detail);!errorCode4&&typeof record.error_code=="string"&&(errorCode4=record.error_code),!message&&typeof record.message=="string"&&(message=record.message);let diagnostics={};errorCode4&&(diagnostics.errorCode=boundedErrorPreview(errorCode4)),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=text4=>{let out2=text4;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={}){try{let diagnostics=extractSanitizedErrorDiagnostics(stripWebhookUrlsDeep(await resp.json())),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(/\/+$/,""),path53=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path53}`).toString()}function conductorPostHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}async function fetchConductorJsonWithMethodAndTimeout(method,url,headers,body,timeoutMs,fetchImpl){let{body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl);return parsed}async function fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl){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);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,row=p.row;if(p.claimed===!0&&row&&typeof row=="object")return{ok:!0,kind:"acquired-or-renewed",row};if(p.claimed===!1&&p.reason==="lease_held"&&row&&typeof row=="object")return{ok:!1,kind:"held-by-other",reason:"lease_held",row};if(p.claimed===!1&&p.reason==="terminal"&&row&&typeof row=="object")return{ok:!1,kind:"terminal",reason:"terminal",row};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 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)}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,row=p.row;if(!row||typeof row!="object")throw new ConductorBridgeApiError("server");let dispatch=row;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 path53=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path53),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);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;if(p.valid!==!0||typeof planHash!="string"||planHash.trim()===""||typeof serializationEnabled!="boolean"||typeof insertedEdges!="number"||!Number.isSafeInteger(insertedEdges)||insertedEdges<0)throw new ConductorBridgeApiError("server");return{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 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`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob,plan_hash:request.planHash});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")}}}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,SHADOW_FRESHNESS_VERDICTS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,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();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),errorCode4=diagnostics?.errorCode,bodyPreview=diagnostics?.bodyPreview;if(isKnownKind){let parts=[`Conductor Bridge API request failed (${kindOrMessage}${typeof status=="number"?`, status ${status}`:""})`];errorCode4&&parts.push(`code=${errorCode4}`),bodyPreview&&parts.push(bodyPreview),super(parts.join(": "))}else super(kindOrMessage);this.name="ConductorBridgeApiError",this.kind=isKnownKind?kindOrMessage:"http",typeof status=="number"&&(this.status=status),errorCode4&&(this.errorCode=errorCode4),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";SHADOW_FRESHNESS_VERDICTS=new Set(["not_applicable","covered","stale","failed"]);CONDUCTOR_REVIEW_ALIGNMENT_STATUSES=new Set(["aligned","divergent","not_configured","invalid"]),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,init_pr_base_contract=__esm({"src/pr-base-contract.ts"(){"use strict";PR_BASE_BRANCH_ENV_VAR="BAPI_BASE_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 path16 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"?path16.win32:path16.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"}});import path17 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 i=0;i<branch.length;i++){let code=branch.charCodeAt(i);if(code<=31||code===127)return"branch name must not contain control characters."}return null}function normalizeRepoKey(cwd){return path17.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 fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!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_start_tickets_prereqs();repoFetchLocks=new Map}});import{execFile}from"child_process";import{readFile as readFile5,writeFile as writeFile3,mkdir as mkdir3,mkdtemp,stat as stat3,readdir as readdir2,rm as rm2}from"fs/promises";import os4 from"node:os";import path18 from"path";import{existsSync as existsSync2}from"node:fs";function appendSummaryRowWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function getStartTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,"","Flags:"," --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)"," --workflow implement|review-and-implement Slash command each spawned worktree runs (default: implement). review-and-implement runs /review-ticket then, after a per-ticket halt gate, /implement-ticket in the same session; --auto applies to the selected workflow."," --tier cheap|basic|premium Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. It is still mapped to a model through the agent registry and any configured difficulty_model_tier_overrides, then validated \u2014 it is NOT a raw --model alias, and never carries an API key or credential. A malformed value fails open to premium routing."," --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, a supervisor peer tab, and check_messages message-relay polling (default: off \u2014 a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>')"," -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(`
2504
+ 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 row of pending){if(toDelivered.run({seq:row.seq}).changes!==1)continue;insertRelayAuditEvent(db,{id:`message.delivered:${row.id}`,source:"conductor-worker",type:"message.delivered",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message delivered to worker",status:"delivered",details:{message_id:row.id,message_type:row.type}}}),toAcked.run({seq:row.seq}),insertRelayAuditEvent(db,{id:`message.acked:${row.id}`,source:"conductor-worker",type:"message.acked",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message acknowledged by worker",status:"acked",details:{message_id:row.id,message_type:row.type}}});let finalRow=reread.get(row.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("conductor-bin.js"),conductorNodePath:deps.execPath??process.execPath,hookBinPath:resolveBinPath("conductor-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 row of rows){let base={...row,runId:context.runId};if(row.status!=="created"||!row.path){out.push(base);continue}if(!isClaude){out.push(base);continue}let workerId=mintStartTicketsWorkerId(row.key,context.agentName,fragment?fragment():randomCorrelationFragment()),conductorEnv=buildConductorWorkerEnv(context,{workerId,ticketKey:row.key,worktreePath:row.path},deps.env),result=await provisionConductorHookForWorktree(row.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 createHash2}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 createHash2("sha256").update(json).digest("hex")}var GIT_CI_PRODUCER,GIT_HOOK_PRODUCER,REQUIRED_CI_CHECKS_GREEN,REVIEW_STATE,DEFAULT_GATE_NAME,REVIEW_PASSED,REVIEW_CHANGES_REQUESTED,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",REVIEW_PASSED="review.passed",REVIEW_CHANGES_REQUESTED="review.changes_requested",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"}});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,adoptCurrentHeadAndUnparkTicket:()=>adoptCurrentHeadAndUnparkTicket,advanceEpicTicketStatus:()=>advanceEpicTicketStatus,approveEpicPlan:()=>approveEpicPlan,bootstrapConductorSupervisorDefaults:()=>bootstrapConductorSupervisorDefaults,buildConductorJiraUrl:()=>buildConductorJiraUrl,buildConductorVcsUrl:()=>buildConductorVcsUrl,buildEpicDispatchKey:()=>buildEpicDispatchKey,claimEpicSupervisionLease:()=>claimEpicSupervisionLease,createEpicRun:()=>createEpicRun,createEpicRunWithDisposition:()=>createEpicRunWithDisposition,createEpicTicketStatus:()=>createEpicTicketStatus,deletePullRequestBranch:()=>deletePullRequestBranch,extractSanitizedErrorDiagnostics:()=>extractSanitizedErrorDiagnostics,fetchActiveEpicRuns:()=>fetchActiveEpicRuns,fetchConductorConfigField:()=>fetchConductorConfigField,fetchConductorJsonPatchWithTimeout:()=>fetchConductorJsonPatchWithTimeout,fetchConductorJsonPostWithTimeout:()=>fetchConductorJsonPostWithTimeout,fetchConductorJsonPutWithTimeout:()=>fetchConductorJsonPutWithTimeout,fetchConductorJsonWithTimeout:()=>fetchConductorJsonWithTimeout,fetchConductorReadiness:()=>fetchConductorReadiness,fetchEffectiveSupervisorConfig:()=>fetchEffectiveSupervisorConfig,fetchEffectiveSupervisorSetup:()=>fetchEffectiveSupervisorSetup,fetchEpicRunState:()=>fetchEpicRunState,fetchParseStatus:()=>fetchParseStatus,fetchPrReviewStatus:()=>fetchPrReviewStatus,fetchShadowDispatchFreshness:()=>fetchShadowDispatchFreshness,getEpicPlan:()=>getEpicPlan,mergePullRequestForGate:()=>mergePullRequestForGate,parseConductorReadinessResponse:()=>parseConductorReadinessResponse,parseConductorSupervisorBootstrapResponse:()=>parseConductorSupervisorBootstrapResponse,pollCiChecksForCommit:()=>pollCiChecksForCommit,reconcileShadowMerge:()=>reconcileShadowMerge,recordEpicDispatch:()=>recordEpicDispatch,remediateEpicTicket:()=>remediateEpicTicket,replaceEpicRunPolicy:()=>replaceEpicRunPolicy,resolveConductorBridgeApiAccess:()=>resolveConductorBridgeApiAccess,safeDiagnosticMessage:()=>safeDiagnosticMessage,stopEpicRun:()=>stopEpicRun,storeEpicPlan:()=>storeEpicPlan,transitionEpicDispatch:()=>transitionEpicDispatch,transitionJiraStatus:()=>transitionJiraStatus,triggerRepositoryParse:()=>triggerRepositoryParse,unparkEpicTicket:()=>unparkEpicTicket,updateApprovedPlanNodeTicketSpec:()=>updateApprovedPlanNodeTicketSpec,updateEpicRunStatus:()=>updateEpicRunStatus,validateEpicPlan:()=>validateEpicPlan});import os3 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??os3.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(text4){return text4.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(text4){let redacted=redactErrorPreview(text4).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"):[],path53=(parts[0]==="body"?parts.slice(1):parts).slice(0,CONDUCTOR_VALIDATION_LOC_MAX_PARTS).join(".");return path53?`${path53}: ${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,errorCode4,message;if(Array.isArray(detail))message=formatValidationDetailItem(detail[0]);else if(detail&&typeof detail=="object"){let d=detail;typeof d.error_code=="string"&&(errorCode4=d.error_code),typeof d.message=="string"&&(message=d.message)}else typeof detail=="string"&&(message=detail);!errorCode4&&typeof record.error_code=="string"&&(errorCode4=record.error_code),!message&&typeof record.message=="string"&&(message=record.message);let diagnostics={};errorCode4&&(diagnostics.errorCode=boundedErrorPreview(errorCode4)),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=text4=>{let out2=text4;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={}){try{let diagnostics=extractSanitizedErrorDiagnostics(stripWebhookUrlsDeep(await resp.json())),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(/\/+$/,""),path53=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path53}`).toString()}function conductorPostHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}async function fetchConductorJsonWithMethodAndTimeout(method,url,headers,body,timeoutMs,fetchImpl){let{body:parsed}=await fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl);return parsed}async function fetchConductorJsonWithMethodStatusAndTimeout(method,url,headers,body,timeoutMs,fetchImpl){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);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,row=p.row;if(p.claimed===!0&&row&&typeof row=="object")return{ok:!0,kind:"acquired-or-renewed",row};if(p.claimed===!1&&p.reason==="lease_held"&&row&&typeof row=="object")return{ok:!1,kind:"held-by-other",reason:"lease_held",row};if(p.claimed===!1&&p.reason==="terminal"&&row&&typeof row=="object")return{ok:!1,kind:"terminal",reason:"terminal",row};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 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)}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,row=p.row;if(!row||typeof row!="object")throw new ConductorBridgeApiError("server");let dispatch=row;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 path53=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path53),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);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;if(p.valid!==!0||typeof planHash!="string"||planHash.trim()===""||typeof serializationEnabled!="boolean"||typeof insertedEdges!="number"||!Number.isSafeInteger(insertedEdges)||insertedEdges<0)throw new ConductorBridgeApiError("server");return{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 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`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob,plan_hash:request.planHash});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")}}}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,SHADOW_FRESHNESS_VERDICTS,CONDUCTOR_REVIEW_ALIGNMENT_STATUSES,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();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),errorCode4=diagnostics?.errorCode,bodyPreview=diagnostics?.bodyPreview;if(isKnownKind){let parts=[`Conductor Bridge API request failed (${kindOrMessage}${typeof status=="number"?`, status ${status}`:""})`];errorCode4&&parts.push(`code=${errorCode4}`),bodyPreview&&parts.push(bodyPreview),super(parts.join(": "))}else super(kindOrMessage);this.name="ConductorBridgeApiError",this.kind=isKnownKind?kindOrMessage:"http",typeof status=="number"&&(this.status=status),errorCode4&&(this.errorCode=errorCode4),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";SHADOW_FRESHNESS_VERDICTS=new Set(["not_applicable","covered","stale","failed"]);CONDUCTOR_REVIEW_ALIGNMENT_STATUSES=new Set(["aligned","divergent","not_configured","invalid"]),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,init_pr_base_contract=__esm({"src/pr-base-contract.ts"(){"use strict";PR_BASE_BRANCH_ENV_VAR="BAPI_BASE_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 path16 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"?path16.win32:path16.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"}});import path17 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 i=0;i<branch.length;i++){let code=branch.charCodeAt(i);if(code<=31||code===127)return"branch name must not contain control characters."}return null}function normalizeRepoKey(cwd){return path17.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 fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!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_start_tickets_prereqs();repoFetchLocks=new Map}});import{execFile}from"child_process";import{readFile as readFile5,writeFile as writeFile3,mkdir as mkdir3,mkdtemp,stat as stat3,readdir as readdir2,rm as rm2}from"fs/promises";import os4 from"node:os";import path18 from"path";import{existsSync as existsSync2}from"node:fs";function appendSummaryRowWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function getStartTicketsUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,"","Flags:"," --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)"," --workflow implement|review-and-implement Slash command each spawned worktree runs (default: implement). review-and-implement runs /review-ticket then, after a per-ticket halt gate, /implement-ticket in the same session; --auto applies to the selected workflow."," --tier cheap|basic|premium Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. It is still mapped to a model through the agent registry and any configured difficulty_model_tier_overrides, then validated \u2014 it is NOT a raw --model alias, and never carries an API key or credential. A malformed value fails open to premium routing."," --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, a supervisor peer tab, and check_messages message-relay polling (default: off \u2014 a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>')"," -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(`
2505
2505
  `)}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=DEFAULT_AGENT_NAME,baseBranch="main",conductorEnabled=!1,workflow="implement",reviewRoundsRaw,injectedTier,branchEntries=[],keys=[];for(let i=0;i<argv.length;i++){let arg=argv[i],takeValue4=()=>{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=takeValue4(),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}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}if(arg==="--workflow"||arg.startsWith("--workflow=")){let value;if(arg.startsWith("--workflow="))value=arg.slice(11);else if(value=takeValue4(),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=takeValue4(),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=takeValue4())}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=takeValue4(),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=takeValue4();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=takeValue4(),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=takeValue4()}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==="--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}return{status:"ok",options:{keys,terminal,dryRun,autoApprove,refreshMain,maxParallel,branchOverrides,agentName,baseBranch,conductorEnabled,workflow,reviewRounds,...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){if(!isSupportedStartTicketsPlatform(deps.platform))return{ok:!1,error:unsupportedPlatformMessage(deps.platform)};let platform=deps.platform,prBaseBranch=resolvePrBaseBranchEnvValue(conductorEnabled,baseBranch);return{ok:!0,config:{platform,worktrunkBinary:resolveWorktrunkBinary(platform,deps.env),buildAgentShellCommand:(key,worktreePath,modelAlias)=>prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch),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 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});return result.ok?(result.warning&&warn(result.warning),{ok:!0}):result.reason==="unsupported-platform"?{ok:!1,error:result.error}:{ok:!1,error:appendDoctorHint(result.error)}}function parseGitWorktreeList(output){let entries=[],current=null;for(let rawLine of output.split(`
2506
2506
  `)){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}`,fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!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 createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{};return runWithConcurrency(options.keys,options.maxParallel,key=>createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior))}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 buildConductorMessageRelayLaunchInstruction(){return"Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing."}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",command=`${workflow==="review-and-implement"?"/review-and-implement":"/implement-ticket"} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`));let parts=[command];return opts.conductorEnabled&&(parts.push(buildConductorMessageRelayLaunchInstruction()),parts.push(buildPrBaseContractLaunchInstruction())),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias){let argv=[agent.command];return agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias),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){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(worktreePath)}' && ${invocation}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch)}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(`
2507
2507
  `)}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(`
@@ -2543,7 +2543,7 @@ ${stderr}`.matchAll(/^job\s+(\d+)\s+at\b/gim)];return matches.length>0?matches[m
2543
2543
  `);lines.push(["ID","COMMAND","RUN_AT","BACKEND","AGENT","NATIVE","LATEST","UNIT_PATH"].join(" "));for(let e of report.entries)lines.push([e.metadata.id,scheduleCommandLabel(e.metadata),e.metadata.run_at_iso,e.metadata.backend,e.metadata.agent,e.status,latestRunStatus(e.metadata)||"-",e.metadata.unit_path??"-"].join(" "));return lines.join(`
2544
2544
  `)}async function orchestrateScheduleCancel(options,deps){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,notFound:!0,error:`No schedule found with id '${options.id}'.`};if(options.agent!==void 0&&metadata.agent!==options.agent)return{ok:!1,notFound:!0,error:`Schedule '${options.id}' does not match agent filter '${options.agent}'.`};if(options.backend!==void 0&&metadata.backend!==options.backend)return{ok:!1,notFound:!0,error:`Schedule '${options.id}' does not match backend filter '${options.backend}'.`};let backend=getSchedulerBackendByName(metadata.backend);if(!backend)return{ok:!1,error:`Unknown backend '${metadata.backend}' recorded for '${options.id}'.`};let cancelResult=await backend.cancel({deps,metadata});if(!cancelResult.ok)return{ok:!1,error:cancelResult.error??"Backend cancel failed."};let canceledAtIso=new Date(deps.now?deps.now():Date.now()).toISOString();return await appendScheduleRunEvent(options.id,{status:"canceled",at:canceledAtIso},deps.homeDir,deps.platform).catch(()=>{}),await deleteScheduleMetadata(options.id,deps.homeDir,deps.platform),{ok:!0,id:options.id,backend:metadata.backend,nativeRemoved:cancelResult.nativeRemoved,stale:cancelResult.stale,metadataRemoved:!0}}function formatScheduleCancelResult(result){return result.ok?[`Schedule '${result.id}' canceled.`,` backend: ${result.backend}`,` native removed: ${result.nativeRemoved?"yes":`no${result.stale?" (stale)":""}`}`,` metadata removed: ${result.metadataRemoved?"yes":"no"}`," logs: preserved"].join(`
2545
2545
  `):`Error: ${result.error}`}async function orchestrateScheduleDoctor(deps){let platformResult=getSchedulerBackendsForPlatform(deps.platform),envPath=deps.env.PATH??deps.env.Path??"",claudeResolved=!!await resolveCommandOnPath("claude",envPath,deps),cursorResolved=!!await resolveCommandOnPath("cursor-agent",envPath,deps),npxResolved=!!await resolveCommandOnPath("npx",envPath,deps),cursorApiKeyPresent=!!deps.env.CURSOR_API_KEY,bridgeCredentialResolved=deps.bridgeCredentialResolved?.()??!!deps.env.BAPI_API_KEY;if(!platformResult.ok)return{platform:deps.platform,platformSupported:!1,candidateBackends:[],backendAvailability:[],claudeResolved,cursorResolved,npxResolved,cursorApiKeyPresent,bridgeCredentialResolved,unsupportedMessage:platformResult.error};let candidateBackends=platformResult.backends.map(b=>b.name),backendAvailability=[];for(let backend of platformResult.backends)backendAvailability.push({backend:backend.name,available:await backend.isAvailable(deps)});return{platform:deps.platform,platformSupported:!0,candidateBackends,backendAvailability,claudeResolved,cursorResolved,npxResolved,cursorApiKeyPresent,bridgeCredentialResolved}}function formatScheduleDoctorReport(report,json){if(json)return JSON.stringify(report,null,2);let lines=["schedule-run doctor (read-only diagnostics)",`Platform: ${report.platform}`];if(!report.platformSupported)lines.push(report.unsupportedMessage??unsupportedSchedulerPlatformMessage(report.platform));else{lines.push(`Candidate backends (in order): ${report.candidateBackends.join(", ")}`);for(let a of report.backendAvailability)lines.push(` ${a.available?"AVAILABLE ":"UNAVAILABLE"} ${a.backend}`)}return lines.push(`claude on PATH: ${report.claudeResolved?"yes":"no"}`),lines.push(`cursor-agent on PATH: ${report.cursorResolved?"yes":"no"}`),lines.push(`npx on PATH: ${report.npxResolved?"yes":"no"}`),lines.push(`CURSOR_API_KEY set: ${report.cursorApiKeyPresent?"yes":"no"}`),lines.push(`Bridge credential: ${report.bridgeCredentialResolved?"resolved":"not resolved"}`),lines.join(`
2546
- `)}function nowIso2(deps){return new Date(deps.now?deps.now():Date.now()).toISOString()}async function orchestrateScheduleExecute(options,deps,io){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,exitCode:1,error:`No schedule found with id '${options.id}'.`};let agentInvocation=metadata.agent_invocation??metadata.invocation;if(!agentInvocation||!agentInvocation.exe)return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:"missing agent_invocation"},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Schedule '${options.id}' has no agent invocation to run.`};await appendScheduleRunEvent(options.id,{status:"started",at:nowIso2(deps)},deps.homeDir,deps.platform).catch(()=>{});let env={...deps.env};metadata.env_path&&(env.PATH=metadata.env_path,deps.platform==="win32"&&(env.Path=metadata.env_path)),env.BRIDGE_GPT_SCHEDULE_ID=metadata.id,metadata.command&&(env.BRIDGE_GPT_COMMAND=metadata.command),metadata.args&&(env.BRIDGE_GPT_COMMAND_ARGS_JSON=JSON.stringify(metadata.args)),metadata.repo_path&&(env.BRIDGE_GPT_REPO_PATH=metadata.repo_path),metadata.agent&&(env.BRIDGE_GPT_AGENT=metadata.agent),metadata.agent_path&&(env.BRIDGE_GPT_AGENT_PATH=metadata.agent_path),metadata.idea_file&&(env.BRIDGE_GPT_IDEA_FILE=metadata.idea_file);let result;try{result=await deps.runCommand(agentInvocation.exe,agentInvocation.args,{cwd:metadata.repo_path,env})}catch(error){let msg=error instanceof Error?error.message:String(error);return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:`agent launch failed: ${msg}`},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Failed to launch agent: ${msg}`}}return result.stdout&&io.writeStdout(result.stdout),result.stderr&&io.writeStderr(result.stderr),result.exitCode===0?(await appendScheduleRunEvent(options.id,{status:"completed",at:nowIso2(deps),exit_code:0},deps.homeDir,deps.platform).catch(()=>{}),{ok:!0,exitCode:0}):(await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),exit_code:result.exitCode},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:result.exitCode})}async function runScheduleRunCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseScheduleRunArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getScheduleRunUsage()),1;let deps=overrides.deps??createDefaultScheduleRunDeps();try{switch(parsed.subcommand){case"create":{let result=await orchestrateScheduleCreate(parsed.options,deps);return result.ok?(log(formatScheduleCreateResult(result)),0):(errorLog(formatScheduleCreateResult(result)),1)}case"list":{let report=await orchestrateScheduleList(parsed.options,deps);return log(formatScheduleListResult(report,parsed.options.json)),0}case"cancel":{let result=await orchestrateScheduleCancel(parsed.options,deps);return result.ok?(log(formatScheduleCancelResult(result)),0):(errorLog(formatScheduleCancelResult(result)),1)}case"doctor":{let report=await orchestrateScheduleDoctor(deps);return log(formatScheduleDoctorReport(report,parsed.options.json)),report.platformSupported?0:1}case"_execute":{let io={writeStdout:overrides.writeStdout??(chunk=>process.stdout.write(chunk)),writeStderr:overrides.writeStderr??(chunk=>process.stderr.write(chunk))},result=await orchestrateScheduleExecute(parsed.options,deps,io);return!result.ok&&result.error&&errorLog(`Error: ${result.error}`),result.exitCode}}}catch(error){let detail=error instanceof Error?error.message:String(error);return errorLog(`Internal error: ${detail}`),errorLog("Error: schedule-run failed unexpectedly. See the message above for local diagnostics."),1}return 1}var VALID_BACKEND_NAMES,SCHEDULE_ID_PATTERN,init_schedule_run=__esm({"src/schedule-run.ts"(){"use strict";init_scheduler_backends();init_schedule_store();init_agent_launchers();init_claude();init_command_catalog();init_scheduled_prompt();init_mcp_identity();VALID_BACKEND_NAMES=["launchd","task-scheduler","systemd-user","at-fallback"],SCHEDULE_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/}});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 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(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),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),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))for(let entry of rawChecks){if(!isPlainObject4(entry))continue;let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(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();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"]}});var recovery_operations_exports={};__export(recovery_operations_exports,{RECOVERY_TICKET_RETRY_LIMIT:()=>RECOVERY_TICKET_RETRY_LIMIT,abandonEpicRunRecovery:()=>abandonEpicRunRecovery,adoptCurrentHeadAndUnparkWithRetry:()=>adoptCurrentHeadAndUnparkWithRetry,stopEpicRunRecovery:()=>stopEpicRunRecovery,unparkEpicTicketWithRetry:()=>unparkEpicTicketWithRetry});import{randomUUID as randomUUID3}from"crypto";async function stopEpicRunRecovery(access2,options){try{return(await stopEpicRun(access2,{epicRunId:options.epicRunId,reason:options.reason})).committed?{ok:!0,kind:"committed",epicRunId:options.epicRunId}:{ok:!0,kind:"already-stopped",epicRunId:options.epicRunId}}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===409&&err.errorCode==="RUN_TERMINAL"){let status=await readTerminalRunStatus(access2,options.epicRunId);return{ok:!1,kind:"terminal",epicRunId:options.epicRunId,status}}return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"stop request failed")}}}async function readTerminalRunStatus(access2,epicRunId){try{return(await fetchEpicRunState(access2,epicRunId)).epic_run.status}catch{return"terminal"}}async function abandonEpicRunRecovery(access2,options){let state;try{state=await fetchEpicRunState(access2,options.epicRunId)}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"could not read run state")}}let currentStatus=state.epic_run.status;if(currentStatus==="abandoned")return{ok:!0,kind:"already-abandoned",epicRunId:options.epicRunId};try{return await updateEpicRunStatus(access2,{epicKey:options.epicRunId,status:"abandoned",expectedStatus:currentStatus}),{ok:!0,kind:"abandoned",epicRunId:options.epicRunId}}catch(err){return err instanceof ConductorBridgeApiError&&err.status===400?{ok:!1,kind:"concurrent-change",epicRunId:options.epicRunId,message:"the run's status changed concurrently; re-check its state and retry"}:{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"abandon request failed")}}}async function ticketRecoveryWithRetry(access2,options,mutate){let idempotencyKey=randomUUID3();for(let attempt=0;attempt<RECOVERY_TICKET_RETRY_LIMIT;attempt+=1){let state;try{state=await fetchEpicRunState(access2,options.epicRunId)}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,ticketKey:options.ticketKey,message:safeDiagnosticMessage(err,"could not read run state")}}let ticket=state.ticket_statuses.find(t=>t.ticket_key===options.ticketKey);if(!ticket)return{ok:!1,kind:"ticket-not-found",epicRunId:options.epicRunId,ticketKey:options.ticketKey};let result;try{result=await mutate(access2,{epicRunId:options.epicRunId,ticketKey:options.ticketKey,expectedRowVersion:ticket.row_version,idempotencyKey,reason:options.reason})}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,ticketKey:options.ticketKey,message:safeDiagnosticMessage(err,"recovery request failed")}}if(result.ok)return{ok:!0,kind:"unparked",epicRunId:options.epicRunId,ticketKey:options.ticketKey,status:result.ticket_status.status}}return{ok:!1,kind:"concurrent-change-exhausted",epicRunId:options.epicRunId,ticketKey:options.ticketKey}}async function unparkEpicTicketWithRetry(access2,options){return ticketRecoveryWithRetry(access2,options,(a,args)=>unparkEpicTicket(a,args))}async function adoptCurrentHeadAndUnparkWithRetry(access2,options){return ticketRecoveryWithRetry(access2,options,(a,args)=>adoptCurrentHeadAndUnparkTicket(a,args))}var RECOVERY_TICKET_RETRY_LIMIT,init_recovery_operations=__esm({"src/conductor/recovery-operations.ts"(){"use strict";init_bridge_api_client();RECOVERY_TICKET_RETRY_LIMIT=3}});import{createHash as createHash5}from"node:crypto";function makeProducerDedupeKey(dimensions){let canonical={};for(let[key,value]of Object.entries(dimensions))value!=null&&(canonical[key]=value);return stableJsonHash(canonical)}function makeStableProducerEventId(dedupeKey){let h=createHash5("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError2(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function eventAlreadyExists(dedupeKey,deps={}){let pollEvents=deps.pollEvents??(options=>pollConductorEvents(options)),sinceSeq=1;for(let page=0;page<LEDGER_SCAN_MAX_PAGES;page+=1){let result;try{result=await pollEvents({since_seq:sinceSeq,data_mode:"full",limit:LEDGER_SCAN_PAGE_LIMIT})}catch{return!1}for(let event of result.events){if(!event||typeof event!="object")continue;let data=event.data;if(data&&typeof data=="object"){let details=data.details;if(details&&typeof details=="object"&&details.dedupe_key===dedupeKey)return!0}}if(result.count===0||result.next_seq<=sinceSeq)break;sinceSeq=result.next_seq}return!1}async function emitConductorEventIfNew(input,dimensions,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,dedupeKey=makeProducerDedupeKey(dimensions);if(await eventAlreadyExists(dedupeKey,deps))return{emitted:!1,reason:"duplicate"};let eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}};try{return await emitEvent({...input,id:eventId,data}),{emitted:!0,event_id:eventId}}catch(error){if(isDuplicateConstraintError2(error))return{emitted:!1,reason:"duplicate"};throw error}}var LEDGER_SCAN_PAGE_LIMIT,LEDGER_SCAN_MAX_PAGES,init_producer_ledger=__esm({"src/conductor/producer-ledger.ts"(){"use strict";init_store();init_git_ci_types();LEDGER_SCAN_PAGE_LIMIT=500,LEDGER_SCAN_MAX_PAGES=200}});function buildReviewObservationEventInput(binding,snapshot,eventType,reason,runId=null,workerId=null){return{source:"review",type:eventType,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:REVIEW_PRODUCER_OBSERVED_VIA,data:{summary:eventType===REVIEW_PASSED?`Review passed for ${binding.subject}`:`Review changes requested for ${binding.subject}`,status:eventType===REVIEW_PASSED?"passed":"changes_requested",details:{repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_decision:snapshot.review_decision,approvals:snapshot.approvals,sticky_verdict:snapshot.sticky_verdict,review_state_hash:snapshot.review_state_hash,reason}}}}async function observeReviewWithResolved(binding,access2,gateConfig,deps={}){let fetchStatus=deps.fetchReviewStatus??fetchPrReviewStatus,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={snapshot:null,review_passed_emitted:!1,review_changes_requested_emitted:!1,reason:"observed"},reviewCondition=gateConfig.conditions.find(c=>c.type==="review_state")??null;if(reviewCondition===null)return result.reason="no-review-condition",result;let rawStatus;try{rawStatus=await fetchStatus(access2,binding.pr_number)}catch{return result.reason="review-poll-failed",result}let snapshot=normalizeReviewSnapshot(rawStatus);if(result.snapshot=snapshot,snapshot===null)return result.reason="review-snapshot-unavailable",result;let evalResult=evaluateReviewCondition(reviewCondition,snapshot),baseDimensions={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_state_hash:snapshot.review_state_hash};if(evalResult.changesRequested){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_CHANGES_REQUESTED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_CHANGES_REQUESTED,...baseDimensions});result.review_changes_requested_emitted=decision.emitted,result.reason="review changes requested"}else if(evalResult.passed){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_PASSED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_PASSED,...baseDimensions});result.review_passed_emitted=decision.emitted,result.reason="review passed"}else result.reason=`review not yet passed: ${evalResult.reason}`;return result}var REVIEW_PRODUCER_OBSERVED_VIA,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();REVIEW_PRODUCER_OBSERVED_VIA="pr-review-producer"}});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}}}function defaultSleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}async function observeWithResolved(binding,access2,gateConfig,deps,expectedBaseBranch){let emitConductorEventFn=deps.emitConductorEvent??emitConductorEvent,emitIfNew=deps.emitIfNew??((input,dimensions)=>emitConductorEventIfNew(input,dimensions,{emitEvent:emitConductorEventFn})),pollCi=deps.pollCi??pollCiChecksForCommit,now=deps.now??(()=>new Date().toISOString()),run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let 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 expectedBase=typeof expectedBaseBranch=="string"?expectedBaseBranch.trim():"";if(expectedBase){let observedBase=typeof binding.base_ref=="string"?binding.base_ref.trim():"";if(observedBase!==expectedBase){let actual=observedBase.length>0?observedBase:"(unresolved)";return result.gate_met=!1,result.reason=`pr-base-mismatch: PR #${binding.pr_number} targets base '${actual}' but the run base is '${expectedBase}'. Rebuild the branch from fresh origin/${expectedBase} and cherry-pick only this ticket's commits; do not retarget the PR base in the GitHub UI.`,result}}let rawPoll;try{rawPoll=await pollCi(access2,binding.head_sha)}catch{return result.ci_status="unavailable",result.reason="ci-poll-failed",result}let snapshot=normalizeCiSnapshot(rawPoll),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)result.ci_status="pending";else{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}if(!gateConfig.enabled||!gateConfig.valid)return result.reason=`gate inactive: ${gateConfig.reason}`,result;let reviewSnapshot=null;try{reviewSnapshot=(await observeReviewWithResolved(binding,access2,gateConfig,{emitIfNew,env:deps.env})).snapshot}catch{reviewSnapshot=null}let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now(),reviewSnapshot);if(!evaluation.met)return result.reason=evaluation.reason,result;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}return result.reason="gate met",result}function clampInt(value,fallback,min,max){return typeof value!="number"||!Number.isFinite(value)?fallback:Math.min(max,Math.max(min,Math.floor(value)))}async function waitForDoneGate(params={},deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:params.worktreePath??deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault,sleep3=deps.sleep??defaultSleep2,now=deps.now??(()=>new Date().toISOString()),timeoutMs=clampInt(params.timeoutMs,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,0,WAIT_FOR_GATE_TIMEOUT_MAX_MS),pollIntervalMs=clampInt(params.pollIntervalMs,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,WAIT_FOR_GATE_TIMEOUT_MAX_MS),bindingResult=resolveBinding({repoName:params.repoName,prNumber:params.prNumber,headSha:params.headSha,cwd:params.worktreePath??deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{gate_met:!1,timed_out:!1,reason:`no binding: ${bindingResult.reason}`,repo:null,pr_number:null,head_sha:null};let binding=bindingResult.binding,accessResult=await resolveAccess2();if(!accessResult.ok)return{gate_met:!1,timed_out:!1,reason:`access unavailable: ${accessResult.error}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let rawConfig;try{rawConfig=await fetchGateConfig(accessResult.access)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig);if(!gateConfig.enabled||!gateConfig.valid)return{gate_met:!1,timed_out:!1,reason:`gate inactive: ${gateConfig.reason}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let deadline=Date.now()+timeoutMs,loopDeps={...deps,now};for(;;){let observation=await observeWithResolved(binding,accessResult.access,gateConfig,loopDeps);if(observation.gate_met)return{gate_met:!0,timed_out:!1,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,gate_event_summary:observation.gate_event_summary};if(Date.now()>=deadline)return{gate_met:!1,timed_out:!0,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let remaining=deadline-Date.now();await sleep3(Math.min(pollIntervalMs,Math.max(1,remaining)))}}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,WAIT_FOR_GATE_TIMEOUT_MAX_MS,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,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",WAIT_FOR_GATE_TIMEOUT_MAX_MS=12e4,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS=12e4,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS=5e3,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS=500}});function parseBoundedSupervisorInt(raw,fallback,min,max){if(raw===void 0)return fallback;let trimmed=raw.trim();if(trimmed.length===0||!/^[+-]?\d+$/.test(trimmed))return fallback;let parsed=Number.parseInt(trimmed,10);return Number.isFinite(parsed)?Math.min(max,Math.max(min,parsed)):fallback}function resolveSupervisorConfig(overrides={},env=process.env){let wake_interval_ms=clampOverride(overrides.wake_interval_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_WAKE_INTERVAL_MS,45e3,3e4,6e4),3e4,6e4),global_timeout_ms=clampOverride(overrides.global_timeout_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_GLOBAL_TIMEOUT_MS,864e5,3e5,6048e5),3e5,6048e5),escalation_cooldown_ms=clampOverride(overrides.escalation_cooldown_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_ESCALATION_COOLDOWN_MS,9e5,3e5,864e5),3e5,864e5),quiet_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_QUIET_AFTER_MS,3e5,6e4,36e5),liveness_stalled_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LIVENESS_STALLED_AFTER_MS,12e5,3e5,144e5),dead_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_DEAD_AFTER_MS,72e5,6e5,864e5),poll_limit=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT,POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2);return{wake_interval_ms,global_timeout_ms,stall_thresholds_ms:resolveStallThresholds(env),liveness:{quiet_after_ms,stalled_after_ms:liveness_stalled_after_ms,dead_after_ms},escalation_cooldown_ms,poll_limit}}function clampOverride(override,base,min,max){return override===void 0||!Number.isFinite(override)?base:Math.min(max,Math.max(min,Math.floor(override)))}function resolveStallThresholds(env){return{not_started:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_NOT_STARTED_MS,15*6e4,6e4,36e5),active:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_ACTIVE_MS,2*36e5,10*6e4,12*36e5),stalled:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_STALLED_MS,30*6e4,5*6e4,6*36e5),blocked:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_BLOCKED_MS,24*36e5,30*6e4,168*36e5),candidate_done:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_CANDIDATE_DONE_MS,30*6e4,5*6e4,6*36e5),verifying:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_VERIFYING_MS,2*36e5,10*6e4,12*36e5),unknown:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_UNKNOWN_MS,30*6e4,5*6e4,6*36e5),complete:Number.MAX_SAFE_INTEGER,failed:Number.MAX_SAFE_INTEGER}}var POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2,init_supervisor_config=__esm({"src/conductor/supervisor-config.ts"(){"use strict";POLL_LIMIT_DEFAULT2=200,POLL_LIMIT_MIN=1,POLL_LIMIT_MAX2=1e3}});import{createHash as createHash6}from"node:crypto";function normalizeDimension(value){return(value??"").trim().toLowerCase()}function makeSupervisorIdempotencyKey(meta){return[normalizeDimension(meta.run_id),normalizeDimension(meta.worker_id)||"(run)",normalizeDimension(meta.reason),normalizeDimension(meta.kind),normalizeDimension(meta.cooldown_window)].join("|")}function makeSupervisorAssessmentEventId(idempotencyKey){let h=createHash6("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError3(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitSupervisorAssessmentIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,idempotencyKey=makeSupervisorIdempotencyKey(input.idempotency),eventId=makeSupervisorAssessmentEventId(idempotencyKey),details={...input.details??{},idempotency_key:idempotencyKey,reason:input.idempotency.reason,kind:input.idempotency.kind,cooldown_window:input.idempotency.cooldown_window,classification:input.assessment.classification,confidence:input.assessment.confidence},event={id:eventId,source:"conductor-supervisor",type:"supervisor.assessment",run_id:input.run_id,worker_id:input.idempotency.worker_id??input.worker_id??null,producer:"conductor-supervisor",observed_via:"supervisor",data:{summary:`supervisor assessment: ${input.idempotency.reason}`,status:"escalated",reason:input.idempotency.reason,details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError3(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_supervisor_ledger=__esm({"src/conductor/supervisor-ledger.ts"(){"use strict";init_store()}});function buildSupervisorEscalationWorkerMessage(candidate,assessment,state){let details={reason:candidate.reason,state:candidate.state,liveness:candidate.liveness,elapsed_ms:candidate.elapsed_ms,assessment_source:"deterministic"};return{run_id:state.run_id,worker_id:candidate.worker_id,type:`supervisor.${candidate.reason}`,cause_seq:state.last_seq,payload:{summary:`supervisor escalation: ${candidate.reason}`,status:"escalated",details},source:"conductor-supervisor",producer:"worker-message-relay"}}async function sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,deps={}){let sendMessage=deps.sendMessage??sendWorkerMessage,input=buildSupervisorEscalationWorkerMessage(candidate,assessment,state);return sendMessage(input)}var init_supervisor_message_relay=__esm({"src/conductor/supervisor-message-relay.ts"(){"use strict";init_store()}});function isTerminalState(state){return TERMINAL_STATES.has(state)}function isoToMs(value){if(typeof value!="string"||value.length===0)return null;let ms=Date.parse(value);return Number.isFinite(ms)?ms:null}function msToIso(now){return new Date(now).toISOString()}function createEmptySupervisorRunState(runId,config,now){let startedIso=msToIso(now);return{run_id:runId,status:"unknown",last_seq:0,last_event_time:null,workers:{},gates:{},latest_assessment:null,escalations:[],started_at:startedIso,updated_at:startedIso,global_deadline_at:msToIso(now+config.global_timeout_ms),roster_discovered:!1}}function isValidSupervisorSummary(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)&&value.kind===SUPERVISOR_SUMMARY_KIND}function hydrateSupervisorRunStateFromSnapshot(snapshot,runId,config,now){let empty=createEmptySupervisorRunState(runId,config,now),summary=snapshot?.projection?.summary;return isValidSupervisorSummary(summary)?{...empty,status:typeof summary.status=="string"?summary.status:empty.status,last_seq:typeof summary.last_seq=="number"&&summary.last_seq>=0?summary.last_seq:empty.last_seq,last_event_time:typeof summary.last_event_time=="string"?summary.last_event_time:null,workers:isPlainRecord(summary.workers)?summary.workers:{},gates:isPlainRecord(summary.gates)?summary.gates:{},latest_assessment:summary.latest_assessment&&typeof summary.latest_assessment=="object"?summary.latest_assessment:null,escalations:Array.isArray(summary.escalations)?summary.escalations:[],started_at:typeof summary.started_at=="string"?summary.started_at:empty.started_at,global_deadline_at:typeof summary.global_deadline_at=="string"?summary.global_deadline_at:empty.global_deadline_at,roster_discovered:summary.roster_discovered===!0,run_id:runId}:empty}function isPlainRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function ensureWorkerState(state,workerId,options={}){let existing=state.workers[workerId];if(existing)return options.ticketKey&&!existing.ticket_key&&(existing.ticket_key=options.ticketKey),existing;let created={worker_id:workerId,ticket_key:options.ticketKey??null,state:options.fromRoster?"not_started":"unknown",liveness:"unknown",first_seen_seq:options.seq??null,last_event_seq:options.seq??null,last_event_time:null,last_progress_time:null,last_heartbeat_time:null,blocked_reason:null,terminal_reason:null,observed_event_types:[]};return state.workers[workerId]=created,created}function noteObservedType(worker,eventType){worker.observed_event_types.includes(eventType)||worker.observed_event_types.push(eventType)}function eventDetails(event){let details=event.data?.details;return isPlainRecord(details)?details:{}}function extractRoster(event){let candidates=[eventDetails(event).workers,event.data?.workers,isPlainRecord(event.data?.raw)?event.data.raw.workers:void 0];for(let candidate of candidates)if(Array.isArray(candidate)){let roster=[];for(let entry of candidate){if(!isPlainRecord(entry))continue;let workerId=entry.worker_id;if(typeof workerId!="string"||workerId.length===0)continue;let ticketKey=entry.ticket_key;roster.push({worker_id:workerId,ticket_key:typeof ticketKey=="string"?ticketKey:null})}if(roster.length>0)return roster}return[]}function eventStatus(event){let status=event.data?.status;return typeof status=="string"?status.trim().toLowerCase():""}function eventReason(event){let reason=event.data?.reason??eventDetails(event).reason;return typeof reason=="string"?reason.trim().toLowerCase():""}function applyConductorEventToSupervisorState(state,event,now){if(event.run_id!==state.run_id)return state;let eventType=event.type,eventTimeMs=isoToMs(event.time)??now,eventTimeIso=event.time??msToIso(now);if(typeof event.seq=="number"&&event.seq>state.last_seq&&(state.last_seq=event.seq),state.last_event_time=eventTimeIso,state.status==="unknown"&&(state.status="active"),eventType==="run.started"){let roster=extractRoster(event);roster.length>0&&(state.roster_discovered=!0);for(let member of roster){let worker2=ensureWorkerState(state,member.worker_id,{fromRoster:!0,ticketKey:member.ticket_key,seq:event.seq});noteObservedType(worker2,eventType),worker2.last_event_seq=event.seq??worker2.last_event_seq,worker2.last_event_time=eventTimeIso}return state.updated_at=msToIso(now),state}if(eventType==="supervisor.assessment"||eventType==="message.sent")return state.updated_at=msToIso(now),state;let workerId=event.worker_id;if(typeof workerId!="string"||workerId.length===0)return applyRunLevelEvent(state,event,eventType),state.updated_at=msToIso(now),state;let worker=ensureWorkerState(state,workerId,{seq:event.seq});switch(noteObservedType(worker,eventType),worker.last_event_seq=event.seq??worker.last_event_seq,worker.last_event_time=eventTimeIso,eventType){case"run.heartbeat":{worker.last_heartbeat_time=eventTimeIso,!isTerminalState(worker.state)&&worker.state!=="blocked"&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"agent.notification":{let status=eventStatus(event),reason=eventReason(event);BLOCKED_STATUS_TOKENS.has(status)||BLOCKED_STATUS_TOKENS.has(reason)?isTerminalState(worker.state)||(worker.state="blocked",worker.blocked_reason=status||reason||"blocked"):!isTerminalState(worker.state)&&worker.state==="not_started"&&(worker.state="active");break}case"tool.intent":case"worktree.changed":case"git.commit_created":{PROGRESS_EVENT_TYPES.has(eventType)&&(worker.last_progress_time=eventTimeIso,isTerminalState(worker.state)||((worker.state==="not_started"||worker.state==="unknown"||worker.state==="stalled"||worker.state==="blocked")&&(worker.state="active"),worker.blocked_reason=null));break}case"gate.met":{isTerminalState(worker.state)||(worker.state="candidate_done");break}case"ci.passed":{isTerminalState(worker.state)||(worker.state="verifying"),worker.last_progress_time=eventTimeIso;break}case"ci.failed":{let reason=eventReason(event);eventStatus(event)==="terminal"||reason==="terminal"||reason==="give_up"?(worker.state="failed",worker.terminal_reason=reason||"ci_failed"):isTerminalState(worker.state)||(worker.last_progress_time=eventTimeIso);break}case"run.stopped":{let status=eventStatus(event),reason=eventReason(event);FAILED_STATUS_TOKENS.has(status)||FAILED_STATUS_TOKENS.has(reason)?(worker.state="failed",worker.terminal_reason=reason||status||"failed"):(worker.state="complete",worker.terminal_reason=reason||status||"complete");break}case"message.delivered":case"message.acked":{!isTerminalState(worker.state)&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"merge.succeeded":{worker.state="complete",worker.terminal_reason=worker.terminal_reason||"merge_succeeded";break}case"merge.failed":break;case"merge.dry_run":break;case"merge.pending_approval":break;default:break}return state.updated_at=msToIso(now),state}function applyRunLevelEvent(state,event,eventType){switch(eventType){case"gate.met":state.gates.gate_met=!0;break;case"ci.passed":state.gates.ci="passed";break;case"ci.failed":state.gates.ci="failed";break;case"git.pr_opened":state.gates.pr_opened=!0;break;case"merge.succeeded":case"merge.failed":case"merge.dry_run":case"merge.pending_approval":state.gates.merge=eventType.slice(6);break;default:break}}function classifyWorkerLiveness(worker,config,now){if(isTerminalState(worker.state))return"alive";let lastSignalMs=mostRecentSignalMs(worker);if(lastSignalMs===null)return"unknown";let elapsed=now-lastSignalMs;return elapsed>=config.liveness.dead_after_ms?"dead":elapsed>=config.liveness.stalled_after_ms?"stalled":elapsed>=config.liveness.quiet_after_ms?"quiet":"alive"}function mostRecentSignalMs(worker){let candidates=[isoToMs(worker.last_heartbeat_time),isoToMs(worker.last_event_time),isoToMs(worker.last_progress_time)].filter(v=>v!==null);return candidates.length===0?null:Math.max(...candidates)}function stateAnchorMs(worker){return worker.state==="active"||worker.state==="verifying"?mostRecentSignalMs(worker):isoToMs(worker.last_event_time)??isoToMs(worker.last_heartbeat_time)??isoToMs(worker.last_progress_time)}function applySupervisorHousekeeping(state,config,now){for(let worker of Object.values(state.workers)){if(worker.liveness=classifyWorkerLiveness(worker,config,now),isTerminalState(worker.state)||worker.state==="stalled")continue;let threshold=config.stall_thresholds_ms[worker.state],anchor=stateAnchorMs(worker);anchor!==null&&now-anchor>=threshold&&(worker.state="stalled")}return state.updated_at=msToIso(now),state}function isSupervisorRunTerminal(state){let workers=Object.values(state.workers);return workers.length===0||!state.roster_discovered?!1:workers.every(w=>isTerminalState(w.state))}function hasSupervisorGlobalTimeoutElapsed(state,now){let deadlineMs=isoToMs(state.global_deadline_at);return deadlineMs===null?!1:now>=deadlineMs}function compactWorker(worker){return{worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness,last_event_seq:worker.last_event_seq,last_event_time:worker.last_event_time,last_progress_time:worker.last_progress_time,last_heartbeat_time:worker.last_heartbeat_time,blocked_reason:worker.blocked_reason,terminal_reason:worker.terminal_reason}}function toSupervisorProjectionInput(state){let summary={kind:SUPERVISOR_SUMMARY_KIND,run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,workers:state.workers,gates:state.gates,latest_assessment:state.latest_assessment,escalations:state.escalations,started_at:state.started_at,updated_at:state.updated_at,global_deadline_at:state.global_deadline_at,roster_discovered:state.roster_discovered};return{run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,active_workers:Object.values(state.workers).map(compactWorker),gates:state.gates,assessment:state.latest_assessment,summary}}var SUPERVISOR_SUMMARY_KIND,TERMINAL_STATES,PROGRESS_EVENT_TYPES,BLOCKED_STATUS_TOKENS,FAILED_STATUS_TOKENS,init_supervisor_state=__esm({"src/conductor/supervisor-state.ts"(){"use strict";SUPERVISOR_SUMMARY_KIND="supervisor_projection_summary",TERMINAL_STATES=new Set(["complete","failed"]);PROGRESS_EVENT_TYPES=new Set(["tool.intent","worktree.changed","git.commit_created"]),BLOCKED_STATUS_TOKENS=new Set(["blocked","waiting_for_input","needs_input"]),FAILED_STATUS_TOKENS=new Set(["failed","error","errored","aborted","cancelled","canceled"])}});function elapsedSinceSignal(worker,now){let candidates=[worker.last_event_time,worker.last_progress_time,worker.last_heartbeat_time].map(iso=>iso?Date.parse(iso):NaN).filter(v=>Number.isFinite(v));return candidates.length===0?0:Math.max(0,now-Math.max(...candidates))}function findSupervisorEscalationCandidates(state,config,now){let candidates=[];for(let worker of Object.values(state.workers)){if(worker.state==="complete"||worker.state==="failed")continue;let elapsed=elapsedSinceSignal(worker,now),baseContext={worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness};if(worker.liveness==="dead"){candidates.push({reason:"worker_dead",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});continue}switch(worker.state){case"not_started":worker.liveness!=="alive"&&candidates.push({reason:"worker_not_started",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"blocked":candidates.push({reason:"worker_blocked",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:{...baseContext,blocked_reason:worker.blocked_reason}});break;case"stalled":candidates.push({reason:"worker_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"candidate_done":worker.liveness!=="alive"&&candidates.push({reason:"candidate_done_stuck",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"verifying":worker.liveness==="stalled"&&candidates.push({reason:"verification_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;default:break}}let deadlineMs=state.global_deadline_at?Date.parse(state.global_deadline_at):NaN;return Number.isFinite(deadlineMs)&&now>=deadlineMs&&candidates.push({reason:"global_timeout",kind:ESCALATION_KIND,worker_id:null,state:null,liveness:null,elapsed_ms:Math.max(0,now-deadlineMs),context:{run_id:state.run_id,deadline_at:state.global_deadline_at,worker_count:Object.keys(state.workers).length}}),candidates}function cooldownWindowFor(now,cooldownMs){let width=cooldownMs>0?cooldownMs:1;return String(Math.floor(now/width))}function shouldEmitEscalation(state,candidate,config,now){let cooldownWindow=cooldownWindowFor(now,config.escalation_cooldown_ms);return{emit:!state.escalations.some(record=>record.reason===candidate.reason&&(record.worker_id??null)===(candidate.worker_id??null)&&record.cooldown_window===cooldownWindow&&(record.outcome==="emitted"||record.outcome==="duplicate")),cooldown_window:cooldownWindow}}function recordEscalationResult(state,candidate,cooldownWindow,idempotencyKey,outcome2,now){let record={idempotency_key:idempotencyKey,worker_id:candidate.worker_id??null,reason:candidate.reason,kind:candidate.kind,cooldown_window:cooldownWindow,outcome:outcome2,recorded_at:new Date(now).toISOString()};return state.escalations.push(record),record}function formatElapsed(ms){let totalSeconds=Math.max(0,Math.floor(ms/1e3)),hours=Math.floor(totalSeconds/3600),minutes=Math.floor(totalSeconds%3600/60),seconds=totalSeconds%60;return hours>0?`${hours}h${minutes}m`:minutes>0?`${minutes}m`:`${seconds}s`}function formatEscalationForTerminal(runId,candidate){let worker=candidate.worker_id?` worker=${candidate.worker_id}`:"",stateBit=candidate.state?` state=${candidate.state}`:"",liveBit=candidate.liveness?` liveness=${candidate.liveness}`:"",elapsed=` elapsed=${formatElapsed(candidate.elapsed_ms)}`;return`[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`}var ESCALATION_KIND,init_supervisor_escalation=__esm({"src/conductor/supervisor-escalation.ts"(){"use strict";ESCALATION_KIND="escalation"}});function buildGateIdentity(gateName,configHash){let name=gateName.trim(),hash=typeof configHash=="string"?configHash.trim():"";return hash?`${name}@${hash.toLowerCase()}`:name}function makeMergeActionKey(repo,prNumber,headSha,gateIdentity){let r=normalizeRepoName(repo),pr=normalizePrNumber(prNumber),sha=normalizeSha(headSha),gate=(gateIdentity??"").trim();if(r===null||pr===null||sha===null||gate.length===0)throw new Error("invalid merge action key component");return`merge:${r}:${pr}:${sha}:${gate}`}var init_merge_identity=__esm({"src/conductor/merge-identity.ts"(){"use strict";init_git_ci_types()}});function isPlainObject10(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeEventHeadSha(value){if(typeof value!="string")return null;let trimmed=value.trim();return/^[0-9a-f]{7,40}$/i.test(trimmed)?trimmed.toLowerCase():null}function getRawEventDetails(event){let details=event.data?.details;return isPlainObject10(details)?details:null}function parseHeadObservation(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseMergeLifecycle(event){let details=getRawEventDetails(event);return{action_key:details&&typeof details.action_key=="string"&&details.action_key.trim().length>0?details.action_key.trim():null}}function parseGateMet(event){let details=getRawEventDetails(event);if(!details)return{head_sha:null,repo:null,pr_number:null,gate_name:null,config_hash:null,required_checks:[]};let gateName=typeof details.gate_name=="string"&&details.gate_name.trim().length>0?details.gate_name.trim():null,configHash=typeof details.config_hash=="string"&&details.config_hash.trim().length>0?details.config_hash.trim():null,ciCheckStatus=isPlainObject10(details.ci_check_status)?details.ci_check_status:null,requiredChecks=(Array.isArray(details.required_checks)?details.required_checks:ciCheckStatus&&Array.isArray(ciCheckStatus.required_checks)?ciCheckStatus.required_checks:[]).filter(c=>typeof c=="string"&&c.trim().length>0);return{head_sha:normalizeSha(details.head_sha),repo:normalizeRepoName(details.repo),pr_number:normalizePrNumber(details.pr_number),gate_name:gateName,config_hash:configHash,required_checks:requiredChecks}}function parseSpecReview(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseEmpty(){return EMPTY_DETAILS}function getEventDetails(event,expectedType){if(event.type!==expectedType)return null;let parser=EVENT_PARSERS[expectedType];return parser(event)}function getMergeIdentity(event){if(event.type!=="gate.met")return null;let details=getEventDetails(event,"gate.met");if(details===null)return null;let{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName}=details;if(repo===null||prNumber===null||headSha===null||gateName===null)return null;let gateIdentity=buildGateIdentity(gateName,details.config_hash),actionKey=makeMergeActionKey(repo,prNumber,headSha,gateIdentity);return{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName,config_hash:details.config_hash,required_checks:details.required_checks,gate_identity:gateIdentity,action_key:actionKey,gate_event:{id:typeof event.id=="string"?event.id:void 0,seq:typeof event.seq=="number"?event.seq:void 0,time:typeof event.time=="string"?event.time:void 0}}}var EMPTY_DETAILS,EVENT_PARSERS,init_event_accessors=__esm({"src/conductor/event-accessors.ts"(){"use strict";init_git_ci_types();init_merge_identity();EMPTY_DETAILS=Object.freeze({});EVENT_PARSERS={"run.started":parseEmpty,"run.heartbeat":parseEmpty,"run.stopped":parseEmpty,"agent.notification":parseEmpty,"tool.intent":parseEmpty,"worktree.changed":parseEmpty,"git.commit_created":parseEmpty,"git.pr_opened":parseHeadObservation,"ci.passed":parseHeadObservation,"ci.failed":parseHeadObservation,"gate.met":parseGateMet,"supervisor.assessment":parseEmpty,"message.sent":parseEmpty,"message.delivered":parseEmpty,"message.acked":parseEmpty,"merge.dry_run":parseMergeLifecycle,"merge.attempted":parseMergeLifecycle,"merge.succeeded":parseHeadObservation,"merge.failed":parseMergeLifecycle,"merge.conflict":parseHeadObservation,"merge.pending_approval":parseMergeLifecycle,"review.passed":parseHeadObservation,"review.changes_requested":parseHeadObservation,"spec_review.passed":parseSpecReview,"spec_review.changes_requested":parseSpecReview,"parse.triggered":parseEmpty,"parse.succeeded":parseEmpty,"parse.failed":parseEmpty}}});import{createHash as createHash7}from"node:crypto";function extractMergeActionIdentityFromGateEvent(event){return getMergeIdentity(event)}function makeMergeEventId(eventType,actionKey){let h=createHash7("sha256").update(`${eventType}:${actionKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}async function lookupMergeEventByActionKey(eventType,actionKey,deps){let db=await(deps.openDb??(()=>openReadonlyConductorDatabaseIfExists()))();if(!db)return!1;try{return db.prepare(`SELECT 1 FROM events
2546
+ `)}function nowIso2(deps){return new Date(deps.now?deps.now():Date.now()).toISOString()}async function orchestrateScheduleExecute(options,deps,io){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,exitCode:1,error:`No schedule found with id '${options.id}'.`};let agentInvocation=metadata.agent_invocation??metadata.invocation;if(!agentInvocation||!agentInvocation.exe)return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:"missing agent_invocation"},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Schedule '${options.id}' has no agent invocation to run.`};await appendScheduleRunEvent(options.id,{status:"started",at:nowIso2(deps)},deps.homeDir,deps.platform).catch(()=>{});let env={...deps.env};metadata.env_path&&(env.PATH=metadata.env_path,deps.platform==="win32"&&(env.Path=metadata.env_path)),env.BRIDGE_GPT_SCHEDULE_ID=metadata.id,metadata.command&&(env.BRIDGE_GPT_COMMAND=metadata.command),metadata.args&&(env.BRIDGE_GPT_COMMAND_ARGS_JSON=JSON.stringify(metadata.args)),metadata.repo_path&&(env.BRIDGE_GPT_REPO_PATH=metadata.repo_path),metadata.agent&&(env.BRIDGE_GPT_AGENT=metadata.agent),metadata.agent_path&&(env.BRIDGE_GPT_AGENT_PATH=metadata.agent_path),metadata.idea_file&&(env.BRIDGE_GPT_IDEA_FILE=metadata.idea_file);let result;try{result=await deps.runCommand(agentInvocation.exe,agentInvocation.args,{cwd:metadata.repo_path,env})}catch(error){let msg=error instanceof Error?error.message:String(error);return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:`agent launch failed: ${msg}`},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Failed to launch agent: ${msg}`}}return result.stdout&&io.writeStdout(result.stdout),result.stderr&&io.writeStderr(result.stderr),result.exitCode===0?(await appendScheduleRunEvent(options.id,{status:"completed",at:nowIso2(deps),exit_code:0},deps.homeDir,deps.platform).catch(()=>{}),{ok:!0,exitCode:0}):(await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),exit_code:result.exitCode},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:result.exitCode})}async function runScheduleRunCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseScheduleRunArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getScheduleRunUsage()),1;let deps=overrides.deps??createDefaultScheduleRunDeps();try{switch(parsed.subcommand){case"create":{let result=await orchestrateScheduleCreate(parsed.options,deps);return result.ok?(log(formatScheduleCreateResult(result)),0):(errorLog(formatScheduleCreateResult(result)),1)}case"list":{let report=await orchestrateScheduleList(parsed.options,deps);return log(formatScheduleListResult(report,parsed.options.json)),0}case"cancel":{let result=await orchestrateScheduleCancel(parsed.options,deps);return result.ok?(log(formatScheduleCancelResult(result)),0):(errorLog(formatScheduleCancelResult(result)),1)}case"doctor":{let report=await orchestrateScheduleDoctor(deps);return log(formatScheduleDoctorReport(report,parsed.options.json)),report.platformSupported?0:1}case"_execute":{let io={writeStdout:overrides.writeStdout??(chunk=>process.stdout.write(chunk)),writeStderr:overrides.writeStderr??(chunk=>process.stderr.write(chunk))},result=await orchestrateScheduleExecute(parsed.options,deps,io);return!result.ok&&result.error&&errorLog(`Error: ${result.error}`),result.exitCode}}}catch(error){let detail=error instanceof Error?error.message:String(error);return errorLog(`Internal error: ${detail}`),errorLog("Error: schedule-run failed unexpectedly. See the message above for local diagnostics."),1}return 1}var VALID_BACKEND_NAMES,SCHEDULE_ID_PATTERN,init_schedule_run=__esm({"src/schedule-run.ts"(){"use strict";init_scheduler_backends();init_schedule_store();init_agent_launchers();init_claude();init_command_catalog();init_scheduled_prompt();init_mcp_identity();VALID_BACKEND_NAMES=["launchd","task-scheduler","systemd-user","at-fallback"],SCHEDULE_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/}});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 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),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))for(let entry of rawChecks){if(!isPlainObject4(entry))continue;let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(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();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"]}});var recovery_operations_exports={};__export(recovery_operations_exports,{RECOVERY_TICKET_RETRY_LIMIT:()=>RECOVERY_TICKET_RETRY_LIMIT,abandonEpicRunRecovery:()=>abandonEpicRunRecovery,adoptCurrentHeadAndUnparkWithRetry:()=>adoptCurrentHeadAndUnparkWithRetry,stopEpicRunRecovery:()=>stopEpicRunRecovery,unparkEpicTicketWithRetry:()=>unparkEpicTicketWithRetry});import{randomUUID as randomUUID3}from"crypto";async function stopEpicRunRecovery(access2,options){try{return(await stopEpicRun(access2,{epicRunId:options.epicRunId,reason:options.reason})).committed?{ok:!0,kind:"committed",epicRunId:options.epicRunId}:{ok:!0,kind:"already-stopped",epicRunId:options.epicRunId}}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===409&&err.errorCode==="RUN_TERMINAL"){let status=await readTerminalRunStatus(access2,options.epicRunId);return{ok:!1,kind:"terminal",epicRunId:options.epicRunId,status}}return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"stop request failed")}}}async function readTerminalRunStatus(access2,epicRunId){try{return(await fetchEpicRunState(access2,epicRunId)).epic_run.status}catch{return"terminal"}}async function abandonEpicRunRecovery(access2,options){let state;try{state=await fetchEpicRunState(access2,options.epicRunId)}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"could not read run state")}}let currentStatus=state.epic_run.status;if(currentStatus==="abandoned")return{ok:!0,kind:"already-abandoned",epicRunId:options.epicRunId};try{return await updateEpicRunStatus(access2,{epicKey:options.epicRunId,status:"abandoned",expectedStatus:currentStatus}),{ok:!0,kind:"abandoned",epicRunId:options.epicRunId}}catch(err){return err instanceof ConductorBridgeApiError&&err.status===400?{ok:!1,kind:"concurrent-change",epicRunId:options.epicRunId,message:"the run's status changed concurrently; re-check its state and retry"}:{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,message:safeDiagnosticMessage(err,"abandon request failed")}}}async function ticketRecoveryWithRetry(access2,options,mutate){let idempotencyKey=randomUUID3();for(let attempt=0;attempt<RECOVERY_TICKET_RETRY_LIMIT;attempt+=1){let state;try{state=await fetchEpicRunState(access2,options.epicRunId)}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,ticketKey:options.ticketKey,message:safeDiagnosticMessage(err,"could not read run state")}}let ticket=state.ticket_statuses.find(t=>t.ticket_key===options.ticketKey);if(!ticket)return{ok:!1,kind:"ticket-not-found",epicRunId:options.epicRunId,ticketKey:options.ticketKey};let result;try{result=await mutate(access2,{epicRunId:options.epicRunId,ticketKey:options.ticketKey,expectedRowVersion:ticket.row_version,idempotencyKey,reason:options.reason})}catch(err){return{ok:!1,kind:"unavailable",epicRunId:options.epicRunId,ticketKey:options.ticketKey,message:safeDiagnosticMessage(err,"recovery request failed")}}if(result.ok)return{ok:!0,kind:"unparked",epicRunId:options.epicRunId,ticketKey:options.ticketKey,status:result.ticket_status.status}}return{ok:!1,kind:"concurrent-change-exhausted",epicRunId:options.epicRunId,ticketKey:options.ticketKey}}async function unparkEpicTicketWithRetry(access2,options){return ticketRecoveryWithRetry(access2,options,(a,args)=>unparkEpicTicket(a,args))}async function adoptCurrentHeadAndUnparkWithRetry(access2,options){return ticketRecoveryWithRetry(access2,options,(a,args)=>adoptCurrentHeadAndUnparkTicket(a,args))}var RECOVERY_TICKET_RETRY_LIMIT,init_recovery_operations=__esm({"src/conductor/recovery-operations.ts"(){"use strict";init_bridge_api_client();RECOVERY_TICKET_RETRY_LIMIT=3}});import{createHash as createHash5}from"node:crypto";function makeProducerDedupeKey(dimensions){let canonical={};for(let[key,value]of Object.entries(dimensions))value!=null&&(canonical[key]=value);return stableJsonHash(canonical)}function makeStableProducerEventId(dedupeKey){let h=createHash5("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError2(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function eventAlreadyExists(dedupeKey,deps={}){let pollEvents=deps.pollEvents??(options=>pollConductorEvents(options)),sinceSeq=1;for(let page=0;page<LEDGER_SCAN_MAX_PAGES;page+=1){let result;try{result=await pollEvents({since_seq:sinceSeq,data_mode:"full",limit:LEDGER_SCAN_PAGE_LIMIT})}catch{return!1}for(let event of result.events){if(!event||typeof event!="object")continue;let data=event.data;if(data&&typeof data=="object"){let details=data.details;if(details&&typeof details=="object"&&details.dedupe_key===dedupeKey)return!0}}if(result.count===0||result.next_seq<=sinceSeq)break;sinceSeq=result.next_seq}return!1}async function emitConductorEventIfNew(input,dimensions,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,dedupeKey=makeProducerDedupeKey(dimensions);if(await eventAlreadyExists(dedupeKey,deps))return{emitted:!1,reason:"duplicate"};let eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}};try{return await emitEvent({...input,id:eventId,data}),{emitted:!0,event_id:eventId}}catch(error){if(isDuplicateConstraintError2(error))return{emitted:!1,reason:"duplicate"};throw error}}var LEDGER_SCAN_PAGE_LIMIT,LEDGER_SCAN_MAX_PAGES,init_producer_ledger=__esm({"src/conductor/producer-ledger.ts"(){"use strict";init_store();init_git_ci_types();LEDGER_SCAN_PAGE_LIMIT=500,LEDGER_SCAN_MAX_PAGES=200}});function buildReviewObservationEventInput(binding,snapshot,eventType,reason,runId=null,workerId=null){return{source:"review",type:eventType,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:REVIEW_PRODUCER_OBSERVED_VIA,data:{summary:eventType===REVIEW_PASSED?`Review passed for ${binding.subject}`:`Review changes requested for ${binding.subject}`,status:eventType===REVIEW_PASSED?"passed":"changes_requested",details:{repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_decision:snapshot.review_decision,approvals:snapshot.approvals,sticky_verdict:snapshot.sticky_verdict,review_state_hash:snapshot.review_state_hash,reason}}}}async function observeReviewWithResolved(binding,access2,gateConfig,deps={}){let fetchStatus=deps.fetchReviewStatus??fetchPrReviewStatus,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={snapshot:null,review_passed_emitted:!1,review_changes_requested_emitted:!1,reason:"observed"},reviewCondition=gateConfig.conditions.find(c=>c.type==="review_state")??null;if(reviewCondition===null)return result.reason="no-review-condition",result;let rawStatus;try{rawStatus=await fetchStatus(access2,binding.pr_number)}catch{return result.reason="review-poll-failed",result}let snapshot=normalizeReviewSnapshot(rawStatus);if(result.snapshot=snapshot,snapshot===null)return result.reason="review-snapshot-unavailable",result;let evalResult=evaluateReviewCondition(reviewCondition,snapshot),baseDimensions={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_state_hash:snapshot.review_state_hash};if(evalResult.changesRequested){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_CHANGES_REQUESTED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_CHANGES_REQUESTED,...baseDimensions});result.review_changes_requested_emitted=decision.emitted,result.reason="review changes requested"}else if(evalResult.passed){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_PASSED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_PASSED,...baseDimensions});result.review_passed_emitted=decision.emitted,result.reason="review passed"}else result.reason=`review not yet passed: ${evalResult.reason}`;return result}var REVIEW_PRODUCER_OBSERVED_VIA,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();REVIEW_PRODUCER_OBSERVED_VIA="pr-review-producer"}});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}}}function defaultSleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}async function observeWithResolved(binding,access2,gateConfig,deps,expectedBaseBranch){let emitConductorEventFn=deps.emitConductorEvent??emitConductorEvent,emitIfNew=deps.emitIfNew??((input,dimensions)=>emitConductorEventIfNew(input,dimensions,{emitEvent:emitConductorEventFn})),pollCi=deps.pollCi??pollCiChecksForCommit,now=deps.now??(()=>new Date().toISOString()),run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let 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 expectedBase=typeof expectedBaseBranch=="string"?expectedBaseBranch.trim():"";if(expectedBase){let observedBase=typeof binding.base_ref=="string"?binding.base_ref.trim():"";if(observedBase!==expectedBase){let actual=observedBase.length>0?observedBase:"(unresolved)";return result.gate_met=!1,result.reason=`pr-base-mismatch: PR #${binding.pr_number} targets base '${actual}' but the run base is '${expectedBase}'. Rebuild the branch from fresh origin/${expectedBase} and cherry-pick only this ticket's commits; do not retarget the PR base in the GitHub UI.`,result}}let rawPoll;try{rawPoll=await pollCi(access2,binding.head_sha)}catch{return result.ci_status="unavailable",result.reason="ci-poll-failed",result}let snapshot=normalizeCiSnapshot(rawPoll),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)result.ci_status="pending";else{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}if(!gateConfig.enabled||!gateConfig.valid)return result.reason=`gate inactive: ${gateConfig.reason}`,result;let reviewSnapshot=null;try{reviewSnapshot=(await observeReviewWithResolved(binding,access2,gateConfig,{emitIfNew,env:deps.env})).snapshot}catch{reviewSnapshot=null}let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now(),reviewSnapshot);if(!evaluation.met)return result.reason=evaluation.reason,result;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}return result.reason="gate met",result}function clampInt(value,fallback,min,max){return typeof value!="number"||!Number.isFinite(value)?fallback:Math.min(max,Math.max(min,Math.floor(value)))}async function waitForDoneGate(params={},deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:params.worktreePath??deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault,sleep3=deps.sleep??defaultSleep2,now=deps.now??(()=>new Date().toISOString()),timeoutMs=clampInt(params.timeoutMs,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,0,WAIT_FOR_GATE_TIMEOUT_MAX_MS),pollIntervalMs=clampInt(params.pollIntervalMs,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,WAIT_FOR_GATE_TIMEOUT_MAX_MS),bindingResult=resolveBinding({repoName:params.repoName,prNumber:params.prNumber,headSha:params.headSha,cwd:params.worktreePath??deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{gate_met:!1,timed_out:!1,reason:`no binding: ${bindingResult.reason}`,repo:null,pr_number:null,head_sha:null};let binding=bindingResult.binding,accessResult=await resolveAccess2();if(!accessResult.ok)return{gate_met:!1,timed_out:!1,reason:`access unavailable: ${accessResult.error}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let rawConfig;try{rawConfig=await fetchGateConfig(accessResult.access)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig);if(!gateConfig.enabled||!gateConfig.valid)return{gate_met:!1,timed_out:!1,reason:`gate inactive: ${gateConfig.reason}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let deadline=Date.now()+timeoutMs,loopDeps={...deps,now};for(;;){let observation=await observeWithResolved(binding,accessResult.access,gateConfig,loopDeps);if(observation.gate_met)return{gate_met:!0,timed_out:!1,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,gate_event_summary:observation.gate_event_summary};if(Date.now()>=deadline)return{gate_met:!1,timed_out:!0,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let remaining=deadline-Date.now();await sleep3(Math.min(pollIntervalMs,Math.max(1,remaining)))}}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,WAIT_FOR_GATE_TIMEOUT_MAX_MS,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,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",WAIT_FOR_GATE_TIMEOUT_MAX_MS=12e4,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS=12e4,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS=5e3,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS=500}});function parseBoundedSupervisorInt(raw,fallback,min,max){if(raw===void 0)return fallback;let trimmed=raw.trim();if(trimmed.length===0||!/^[+-]?\d+$/.test(trimmed))return fallback;let parsed=Number.parseInt(trimmed,10);return Number.isFinite(parsed)?Math.min(max,Math.max(min,parsed)):fallback}function resolveSupervisorConfig(overrides={},env=process.env){let wake_interval_ms=clampOverride(overrides.wake_interval_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_WAKE_INTERVAL_MS,45e3,3e4,6e4),3e4,6e4),global_timeout_ms=clampOverride(overrides.global_timeout_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_GLOBAL_TIMEOUT_MS,864e5,3e5,6048e5),3e5,6048e5),escalation_cooldown_ms=clampOverride(overrides.escalation_cooldown_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_ESCALATION_COOLDOWN_MS,9e5,3e5,864e5),3e5,864e5),quiet_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_QUIET_AFTER_MS,3e5,6e4,36e5),liveness_stalled_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LIVENESS_STALLED_AFTER_MS,12e5,3e5,144e5),dead_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_DEAD_AFTER_MS,72e5,6e5,864e5),poll_limit=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT,POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2);return{wake_interval_ms,global_timeout_ms,stall_thresholds_ms:resolveStallThresholds(env),liveness:{quiet_after_ms,stalled_after_ms:liveness_stalled_after_ms,dead_after_ms},escalation_cooldown_ms,poll_limit}}function clampOverride(override,base,min,max){return override===void 0||!Number.isFinite(override)?base:Math.min(max,Math.max(min,Math.floor(override)))}function resolveStallThresholds(env){return{not_started:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_NOT_STARTED_MS,15*6e4,6e4,36e5),active:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_ACTIVE_MS,2*36e5,10*6e4,12*36e5),stalled:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_STALLED_MS,30*6e4,5*6e4,6*36e5),blocked:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_BLOCKED_MS,24*36e5,30*6e4,168*36e5),candidate_done:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_CANDIDATE_DONE_MS,30*6e4,5*6e4,6*36e5),verifying:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_VERIFYING_MS,2*36e5,10*6e4,12*36e5),unknown:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_UNKNOWN_MS,30*6e4,5*6e4,6*36e5),complete:Number.MAX_SAFE_INTEGER,failed:Number.MAX_SAFE_INTEGER}}var POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2,init_supervisor_config=__esm({"src/conductor/supervisor-config.ts"(){"use strict";POLL_LIMIT_DEFAULT2=200,POLL_LIMIT_MIN=1,POLL_LIMIT_MAX2=1e3}});import{createHash as createHash6}from"node:crypto";function normalizeDimension(value){return(value??"").trim().toLowerCase()}function makeSupervisorIdempotencyKey(meta){return[normalizeDimension(meta.run_id),normalizeDimension(meta.worker_id)||"(run)",normalizeDimension(meta.reason),normalizeDimension(meta.kind),normalizeDimension(meta.cooldown_window)].join("|")}function makeSupervisorAssessmentEventId(idempotencyKey){let h=createHash6("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError3(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitSupervisorAssessmentIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,idempotencyKey=makeSupervisorIdempotencyKey(input.idempotency),eventId=makeSupervisorAssessmentEventId(idempotencyKey),details={...input.details??{},idempotency_key:idempotencyKey,reason:input.idempotency.reason,kind:input.idempotency.kind,cooldown_window:input.idempotency.cooldown_window,classification:input.assessment.classification,confidence:input.assessment.confidence},event={id:eventId,source:"conductor-supervisor",type:"supervisor.assessment",run_id:input.run_id,worker_id:input.idempotency.worker_id??input.worker_id??null,producer:"conductor-supervisor",observed_via:"supervisor",data:{summary:`supervisor assessment: ${input.idempotency.reason}`,status:"escalated",reason:input.idempotency.reason,details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError3(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_supervisor_ledger=__esm({"src/conductor/supervisor-ledger.ts"(){"use strict";init_store()}});function buildSupervisorEscalationWorkerMessage(candidate,assessment,state){let details={reason:candidate.reason,state:candidate.state,liveness:candidate.liveness,elapsed_ms:candidate.elapsed_ms,assessment_source:"deterministic"};return{run_id:state.run_id,worker_id:candidate.worker_id,type:`supervisor.${candidate.reason}`,cause_seq:state.last_seq,payload:{summary:`supervisor escalation: ${candidate.reason}`,status:"escalated",details},source:"conductor-supervisor",producer:"worker-message-relay"}}async function sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,deps={}){let sendMessage=deps.sendMessage??sendWorkerMessage,input=buildSupervisorEscalationWorkerMessage(candidate,assessment,state);return sendMessage(input)}var init_supervisor_message_relay=__esm({"src/conductor/supervisor-message-relay.ts"(){"use strict";init_store()}});function isTerminalState(state){return TERMINAL_STATES.has(state)}function isoToMs(value){if(typeof value!="string"||value.length===0)return null;let ms=Date.parse(value);return Number.isFinite(ms)?ms:null}function msToIso(now){return new Date(now).toISOString()}function createEmptySupervisorRunState(runId,config,now){let startedIso=msToIso(now);return{run_id:runId,status:"unknown",last_seq:0,last_event_time:null,workers:{},gates:{},latest_assessment:null,escalations:[],started_at:startedIso,updated_at:startedIso,global_deadline_at:msToIso(now+config.global_timeout_ms),roster_discovered:!1}}function isValidSupervisorSummary(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)&&value.kind===SUPERVISOR_SUMMARY_KIND}function hydrateSupervisorRunStateFromSnapshot(snapshot,runId,config,now){let empty=createEmptySupervisorRunState(runId,config,now),summary=snapshot?.projection?.summary;return isValidSupervisorSummary(summary)?{...empty,status:typeof summary.status=="string"?summary.status:empty.status,last_seq:typeof summary.last_seq=="number"&&summary.last_seq>=0?summary.last_seq:empty.last_seq,last_event_time:typeof summary.last_event_time=="string"?summary.last_event_time:null,workers:isPlainRecord(summary.workers)?summary.workers:{},gates:isPlainRecord(summary.gates)?summary.gates:{},latest_assessment:summary.latest_assessment&&typeof summary.latest_assessment=="object"?summary.latest_assessment:null,escalations:Array.isArray(summary.escalations)?summary.escalations:[],started_at:typeof summary.started_at=="string"?summary.started_at:empty.started_at,global_deadline_at:typeof summary.global_deadline_at=="string"?summary.global_deadline_at:empty.global_deadline_at,roster_discovered:summary.roster_discovered===!0,run_id:runId}:empty}function isPlainRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function ensureWorkerState(state,workerId,options={}){let existing=state.workers[workerId];if(existing)return options.ticketKey&&!existing.ticket_key&&(existing.ticket_key=options.ticketKey),existing;let created={worker_id:workerId,ticket_key:options.ticketKey??null,state:options.fromRoster?"not_started":"unknown",liveness:"unknown",first_seen_seq:options.seq??null,last_event_seq:options.seq??null,last_event_time:null,last_progress_time:null,last_heartbeat_time:null,blocked_reason:null,terminal_reason:null,observed_event_types:[]};return state.workers[workerId]=created,created}function noteObservedType(worker,eventType){worker.observed_event_types.includes(eventType)||worker.observed_event_types.push(eventType)}function eventDetails(event){let details=event.data?.details;return isPlainRecord(details)?details:{}}function extractRoster(event){let candidates=[eventDetails(event).workers,event.data?.workers,isPlainRecord(event.data?.raw)?event.data.raw.workers:void 0];for(let candidate of candidates)if(Array.isArray(candidate)){let roster=[];for(let entry of candidate){if(!isPlainRecord(entry))continue;let workerId=entry.worker_id;if(typeof workerId!="string"||workerId.length===0)continue;let ticketKey=entry.ticket_key;roster.push({worker_id:workerId,ticket_key:typeof ticketKey=="string"?ticketKey:null})}if(roster.length>0)return roster}return[]}function eventStatus(event){let status=event.data?.status;return typeof status=="string"?status.trim().toLowerCase():""}function eventReason(event){let reason=event.data?.reason??eventDetails(event).reason;return typeof reason=="string"?reason.trim().toLowerCase():""}function applyConductorEventToSupervisorState(state,event,now){if(event.run_id!==state.run_id)return state;let eventType=event.type,eventTimeMs=isoToMs(event.time)??now,eventTimeIso=event.time??msToIso(now);if(typeof event.seq=="number"&&event.seq>state.last_seq&&(state.last_seq=event.seq),state.last_event_time=eventTimeIso,state.status==="unknown"&&(state.status="active"),eventType==="run.started"){let roster=extractRoster(event);roster.length>0&&(state.roster_discovered=!0);for(let member of roster){let worker2=ensureWorkerState(state,member.worker_id,{fromRoster:!0,ticketKey:member.ticket_key,seq:event.seq});noteObservedType(worker2,eventType),worker2.last_event_seq=event.seq??worker2.last_event_seq,worker2.last_event_time=eventTimeIso}return state.updated_at=msToIso(now),state}if(eventType==="supervisor.assessment"||eventType==="message.sent")return state.updated_at=msToIso(now),state;let workerId=event.worker_id;if(typeof workerId!="string"||workerId.length===0)return applyRunLevelEvent(state,event,eventType),state.updated_at=msToIso(now),state;let worker=ensureWorkerState(state,workerId,{seq:event.seq});switch(noteObservedType(worker,eventType),worker.last_event_seq=event.seq??worker.last_event_seq,worker.last_event_time=eventTimeIso,eventType){case"run.heartbeat":{worker.last_heartbeat_time=eventTimeIso,!isTerminalState(worker.state)&&worker.state!=="blocked"&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"agent.notification":{let status=eventStatus(event),reason=eventReason(event);BLOCKED_STATUS_TOKENS.has(status)||BLOCKED_STATUS_TOKENS.has(reason)?isTerminalState(worker.state)||(worker.state="blocked",worker.blocked_reason=status||reason||"blocked"):!isTerminalState(worker.state)&&worker.state==="not_started"&&(worker.state="active");break}case"tool.intent":case"worktree.changed":case"git.commit_created":{PROGRESS_EVENT_TYPES.has(eventType)&&(worker.last_progress_time=eventTimeIso,isTerminalState(worker.state)||((worker.state==="not_started"||worker.state==="unknown"||worker.state==="stalled"||worker.state==="blocked")&&(worker.state="active"),worker.blocked_reason=null));break}case"gate.met":{isTerminalState(worker.state)||(worker.state="candidate_done");break}case"ci.passed":{isTerminalState(worker.state)||(worker.state="verifying"),worker.last_progress_time=eventTimeIso;break}case"ci.failed":{let reason=eventReason(event);eventStatus(event)==="terminal"||reason==="terminal"||reason==="give_up"?(worker.state="failed",worker.terminal_reason=reason||"ci_failed"):isTerminalState(worker.state)||(worker.last_progress_time=eventTimeIso);break}case"run.stopped":{let status=eventStatus(event),reason=eventReason(event);FAILED_STATUS_TOKENS.has(status)||FAILED_STATUS_TOKENS.has(reason)?(worker.state="failed",worker.terminal_reason=reason||status||"failed"):(worker.state="complete",worker.terminal_reason=reason||status||"complete");break}case"message.delivered":case"message.acked":{!isTerminalState(worker.state)&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"merge.succeeded":{worker.state="complete",worker.terminal_reason=worker.terminal_reason||"merge_succeeded";break}case"merge.failed":break;case"merge.dry_run":break;case"merge.pending_approval":break;default:break}return state.updated_at=msToIso(now),state}function applyRunLevelEvent(state,event,eventType){switch(eventType){case"gate.met":state.gates.gate_met=!0;break;case"ci.passed":state.gates.ci="passed";break;case"ci.failed":state.gates.ci="failed";break;case"git.pr_opened":state.gates.pr_opened=!0;break;case"merge.succeeded":case"merge.failed":case"merge.dry_run":case"merge.pending_approval":state.gates.merge=eventType.slice(6);break;default:break}}function classifyWorkerLiveness(worker,config,now){if(isTerminalState(worker.state))return"alive";let lastSignalMs=mostRecentSignalMs(worker);if(lastSignalMs===null)return"unknown";let elapsed=now-lastSignalMs;return elapsed>=config.liveness.dead_after_ms?"dead":elapsed>=config.liveness.stalled_after_ms?"stalled":elapsed>=config.liveness.quiet_after_ms?"quiet":"alive"}function mostRecentSignalMs(worker){let candidates=[isoToMs(worker.last_heartbeat_time),isoToMs(worker.last_event_time),isoToMs(worker.last_progress_time)].filter(v=>v!==null);return candidates.length===0?null:Math.max(...candidates)}function stateAnchorMs(worker){return worker.state==="active"||worker.state==="verifying"?mostRecentSignalMs(worker):isoToMs(worker.last_event_time)??isoToMs(worker.last_heartbeat_time)??isoToMs(worker.last_progress_time)}function applySupervisorHousekeeping(state,config,now){for(let worker of Object.values(state.workers)){if(worker.liveness=classifyWorkerLiveness(worker,config,now),isTerminalState(worker.state)||worker.state==="stalled")continue;let threshold=config.stall_thresholds_ms[worker.state],anchor=stateAnchorMs(worker);anchor!==null&&now-anchor>=threshold&&(worker.state="stalled")}return state.updated_at=msToIso(now),state}function isSupervisorRunTerminal(state){let workers=Object.values(state.workers);return workers.length===0||!state.roster_discovered?!1:workers.every(w=>isTerminalState(w.state))}function hasSupervisorGlobalTimeoutElapsed(state,now){let deadlineMs=isoToMs(state.global_deadline_at);return deadlineMs===null?!1:now>=deadlineMs}function compactWorker(worker){return{worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness,last_event_seq:worker.last_event_seq,last_event_time:worker.last_event_time,last_progress_time:worker.last_progress_time,last_heartbeat_time:worker.last_heartbeat_time,blocked_reason:worker.blocked_reason,terminal_reason:worker.terminal_reason}}function toSupervisorProjectionInput(state){let summary={kind:SUPERVISOR_SUMMARY_KIND,run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,workers:state.workers,gates:state.gates,latest_assessment:state.latest_assessment,escalations:state.escalations,started_at:state.started_at,updated_at:state.updated_at,global_deadline_at:state.global_deadline_at,roster_discovered:state.roster_discovered};return{run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,active_workers:Object.values(state.workers).map(compactWorker),gates:state.gates,assessment:state.latest_assessment,summary}}var SUPERVISOR_SUMMARY_KIND,TERMINAL_STATES,PROGRESS_EVENT_TYPES,BLOCKED_STATUS_TOKENS,FAILED_STATUS_TOKENS,init_supervisor_state=__esm({"src/conductor/supervisor-state.ts"(){"use strict";SUPERVISOR_SUMMARY_KIND="supervisor_projection_summary",TERMINAL_STATES=new Set(["complete","failed"]);PROGRESS_EVENT_TYPES=new Set(["tool.intent","worktree.changed","git.commit_created"]),BLOCKED_STATUS_TOKENS=new Set(["blocked","waiting_for_input","needs_input"]),FAILED_STATUS_TOKENS=new Set(["failed","error","errored","aborted","cancelled","canceled"])}});function elapsedSinceSignal(worker,now){let candidates=[worker.last_event_time,worker.last_progress_time,worker.last_heartbeat_time].map(iso=>iso?Date.parse(iso):NaN).filter(v=>Number.isFinite(v));return candidates.length===0?0:Math.max(0,now-Math.max(...candidates))}function findSupervisorEscalationCandidates(state,config,now){let candidates=[];for(let worker of Object.values(state.workers)){if(worker.state==="complete"||worker.state==="failed")continue;let elapsed=elapsedSinceSignal(worker,now),baseContext={worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness};if(worker.liveness==="dead"){candidates.push({reason:"worker_dead",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});continue}switch(worker.state){case"not_started":worker.liveness!=="alive"&&candidates.push({reason:"worker_not_started",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"blocked":candidates.push({reason:"worker_blocked",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:{...baseContext,blocked_reason:worker.blocked_reason}});break;case"stalled":candidates.push({reason:"worker_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"candidate_done":worker.liveness!=="alive"&&candidates.push({reason:"candidate_done_stuck",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"verifying":worker.liveness==="stalled"&&candidates.push({reason:"verification_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;default:break}}let deadlineMs=state.global_deadline_at?Date.parse(state.global_deadline_at):NaN;return Number.isFinite(deadlineMs)&&now>=deadlineMs&&candidates.push({reason:"global_timeout",kind:ESCALATION_KIND,worker_id:null,state:null,liveness:null,elapsed_ms:Math.max(0,now-deadlineMs),context:{run_id:state.run_id,deadline_at:state.global_deadline_at,worker_count:Object.keys(state.workers).length}}),candidates}function cooldownWindowFor(now,cooldownMs){let width=cooldownMs>0?cooldownMs:1;return String(Math.floor(now/width))}function shouldEmitEscalation(state,candidate,config,now){let cooldownWindow=cooldownWindowFor(now,config.escalation_cooldown_ms);return{emit:!state.escalations.some(record=>record.reason===candidate.reason&&(record.worker_id??null)===(candidate.worker_id??null)&&record.cooldown_window===cooldownWindow&&(record.outcome==="emitted"||record.outcome==="duplicate")),cooldown_window:cooldownWindow}}function recordEscalationResult(state,candidate,cooldownWindow,idempotencyKey,outcome2,now){let record={idempotency_key:idempotencyKey,worker_id:candidate.worker_id??null,reason:candidate.reason,kind:candidate.kind,cooldown_window:cooldownWindow,outcome:outcome2,recorded_at:new Date(now).toISOString()};return state.escalations.push(record),record}function formatElapsed(ms){let totalSeconds=Math.max(0,Math.floor(ms/1e3)),hours=Math.floor(totalSeconds/3600),minutes=Math.floor(totalSeconds%3600/60),seconds=totalSeconds%60;return hours>0?`${hours}h${minutes}m`:minutes>0?`${minutes}m`:`${seconds}s`}function formatEscalationForTerminal(runId,candidate){let worker=candidate.worker_id?` worker=${candidate.worker_id}`:"",stateBit=candidate.state?` state=${candidate.state}`:"",liveBit=candidate.liveness?` liveness=${candidate.liveness}`:"",elapsed=` elapsed=${formatElapsed(candidate.elapsed_ms)}`;return`[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`}var ESCALATION_KIND,init_supervisor_escalation=__esm({"src/conductor/supervisor-escalation.ts"(){"use strict";ESCALATION_KIND="escalation"}});function buildGateIdentity(gateName,configHash){let name=gateName.trim(),hash=typeof configHash=="string"?configHash.trim():"";return hash?`${name}@${hash.toLowerCase()}`:name}function makeMergeActionKey(repo,prNumber,headSha,gateIdentity){let r=normalizeRepoName(repo),pr=normalizePrNumber(prNumber),sha=normalizeSha(headSha),gate=(gateIdentity??"").trim();if(r===null||pr===null||sha===null||gate.length===0)throw new Error("invalid merge action key component");return`merge:${r}:${pr}:${sha}:${gate}`}var init_merge_identity=__esm({"src/conductor/merge-identity.ts"(){"use strict";init_git_ci_types()}});function isPlainObject10(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeEventHeadSha(value){if(typeof value!="string")return null;let trimmed=value.trim();return/^[0-9a-f]{7,40}$/i.test(trimmed)?trimmed.toLowerCase():null}function getRawEventDetails(event){let details=event.data?.details;return isPlainObject10(details)?details:null}function parseHeadObservation(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseMergeLifecycle(event){let details=getRawEventDetails(event);return{action_key:details&&typeof details.action_key=="string"&&details.action_key.trim().length>0?details.action_key.trim():null}}function parseGateMet(event){let details=getRawEventDetails(event);if(!details)return{head_sha:null,repo:null,pr_number:null,gate_name:null,config_hash:null,required_checks:[]};let gateName=typeof details.gate_name=="string"&&details.gate_name.trim().length>0?details.gate_name.trim():null,configHash=typeof details.config_hash=="string"&&details.config_hash.trim().length>0?details.config_hash.trim():null,ciCheckStatus=isPlainObject10(details.ci_check_status)?details.ci_check_status:null,requiredChecks=(Array.isArray(details.required_checks)?details.required_checks:ciCheckStatus&&Array.isArray(ciCheckStatus.required_checks)?ciCheckStatus.required_checks:[]).filter(c=>typeof c=="string"&&c.trim().length>0);return{head_sha:normalizeSha(details.head_sha),repo:normalizeRepoName(details.repo),pr_number:normalizePrNumber(details.pr_number),gate_name:gateName,config_hash:configHash,required_checks:requiredChecks}}function parseSpecReview(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseEmpty(){return EMPTY_DETAILS}function getEventDetails(event,expectedType){if(event.type!==expectedType)return null;let parser=EVENT_PARSERS[expectedType];return parser(event)}function getMergeIdentity(event){if(event.type!=="gate.met")return null;let details=getEventDetails(event,"gate.met");if(details===null)return null;let{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName}=details;if(repo===null||prNumber===null||headSha===null||gateName===null)return null;let gateIdentity=buildGateIdentity(gateName,details.config_hash),actionKey=makeMergeActionKey(repo,prNumber,headSha,gateIdentity);return{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName,config_hash:details.config_hash,required_checks:details.required_checks,gate_identity:gateIdentity,action_key:actionKey,gate_event:{id:typeof event.id=="string"?event.id:void 0,seq:typeof event.seq=="number"?event.seq:void 0,time:typeof event.time=="string"?event.time:void 0}}}var EMPTY_DETAILS,EVENT_PARSERS,init_event_accessors=__esm({"src/conductor/event-accessors.ts"(){"use strict";init_git_ci_types();init_merge_identity();EMPTY_DETAILS=Object.freeze({});EVENT_PARSERS={"run.started":parseEmpty,"run.heartbeat":parseEmpty,"run.stopped":parseEmpty,"agent.notification":parseEmpty,"tool.intent":parseEmpty,"worktree.changed":parseEmpty,"git.commit_created":parseEmpty,"git.pr_opened":parseHeadObservation,"ci.passed":parseHeadObservation,"ci.failed":parseHeadObservation,"gate.met":parseGateMet,"supervisor.assessment":parseEmpty,"message.sent":parseEmpty,"message.delivered":parseEmpty,"message.acked":parseEmpty,"merge.dry_run":parseMergeLifecycle,"merge.attempted":parseMergeLifecycle,"merge.succeeded":parseHeadObservation,"merge.failed":parseMergeLifecycle,"merge.conflict":parseHeadObservation,"merge.pending_approval":parseMergeLifecycle,"review.passed":parseHeadObservation,"review.changes_requested":parseHeadObservation,"spec_review.passed":parseSpecReview,"spec_review.changes_requested":parseSpecReview,"parse.triggered":parseEmpty,"parse.succeeded":parseEmpty,"parse.failed":parseEmpty}}});import{createHash as createHash7}from"node:crypto";function extractMergeActionIdentityFromGateEvent(event){return getMergeIdentity(event)}function makeMergeEventId(eventType,actionKey){let h=createHash7("sha256").update(`${eventType}:${actionKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}async function lookupMergeEventByActionKey(eventType,actionKey,deps){let db=await(deps.openDb??(()=>openReadonlyConductorDatabaseIfExists()))();if(!db)return!1;try{return db.prepare(`SELECT 1 FROM events
2547
2547
  WHERE type = ?
2548
2548
  AND json_extract(data_json, '$.details.action_key') = ?
2549
2549
  LIMIT 1`).get(eventType,actionKey)!==void 0}finally{db.close()}}async function hasTerminalMergeSucceeded(actionKey,deps={}){return lookupMergeEventByActionKey("merge.succeeded",actionKey,deps)}async function hasMergeDryRun(actionKey,deps={}){return lookupMergeEventByActionKey("merge.dry_run",actionKey,deps)}async function hasMergePendingApproval(actionKey,deps={}){return lookupMergeEventByActionKey("merge.pending_approval",actionKey,deps)}function isDuplicateConstraintError4(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitMergeLedgerEventIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,eventId=makeMergeEventId(input.type,input.action_key),event={id:eventId,source:"conductor-supervisor",type:input.type,run_id:input.run_id??null,worker_id:input.worker_id??null,producer:"conductor-merge",observed_via:"supervisor",data:{summary:input.summary??`${input.type} ${input.action_key}`,status:input.status,reason:input.reason??void 0,details:input.details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError4(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_merge_ledger=__esm({"src/conductor/merge-ledger.ts"(){"use strict";init_event_accessors();init_merge_identity();init_store()}});function buildMergeRequestFromGateEvent(identity){return{repo_name:identity.repo,pr_number:identity.pr_number,expected_head_sha:identity.head_sha,gate:{name:identity.gate_name,config_hash:identity.config_hash,required_checks:identity.required_checks},action_key:identity.action_key,gate_event:identity.gate_event}}function mapApiErrorReason(error){if(error instanceof ConductorBridgeApiError)switch(error.kind){case"timeout":return"api_timeout";case"network":return"api_network";case"unauthorized":return"api_unauthorized";case"server":case"http":return"api_unavailable";case"invalid-input":return"api_invalid"}return"api_network"}function resolveAttributionWorkerId(event,identity,resolve2){if(typeof event.worker_id=="string"&&event.worker_id.trim().length>0)return event.worker_id.trim();if(resolve2){let resolved=resolve2(event,identity);if(typeof resolved=="string"&&resolved.trim().length>0)return resolved.trim()}return null}async function processGateMetMerge(access2,event,deps={}){let extract=deps.extractIdentity??extractMergeActionIdentityFromGateEvent,checkTerminal=deps.hasTerminal??hasTerminalMergeSucceeded,checkDryRun=deps.hasDryRun??hasMergeDryRun,checkPendingApproval=deps.hasPendingApproval??hasMergePendingApproval,mergeFn=deps.merge??mergePullRequestForGate,identity=extract(event);if(!identity)return{processed:!1,reason:"ineligible"};let actionKey=identity.action_key,attributionWorkerId=resolveAttributionWorkerId(event,identity,deps.resolveWorkerIdForGateEvent);if(await checkTerminal(actionKey))return{processed:!1,reason:"already_succeeded"};let baseDetails={action_key:actionKey,repo:identity.repo,pr_number:identity.pr_number,expected_head_sha:identity.head_sha,gate:identity.gate_identity},response;try{response=await mergeFn(access2,buildMergeRequestFromGateEvent(identity))}catch(error){let reason=mapApiErrorReason(error);return await emitMergeLedgerEventIfNew({type:"merge.failed",action_key:actionKey,status:"failed",reason,details:baseDetails,run_id:event.run_id??null,worker_id:attributionWorkerId,summary:`merge.failed ${reason}`},{emitEvent:deps.emitEvent}),{processed:!0,outcome:"api_error",reason}}let emitted=[];for(let ledgerEvent of response.ledger_events??[]){if(ledgerEvent.type==="merge.dry_run"&&await checkDryRun(actionKey)){emitted.push({type:ledgerEvent.type,emitted:!1});continue}if(ledgerEvent.type==="merge.pending_approval"&&await checkPendingApproval(actionKey)){emitted.push({type:ledgerEvent.type,emitted:!1});continue}let result=await emitMergeLedgerEventIfNew({type:ledgerEvent.type,action_key:actionKey,status:ledgerEvent.status,reason:ledgerEvent.reason??null,details:ledgerEvent.details??baseDetails,run_id:event.run_id??null,worker_id:attributionWorkerId},{emitEvent:deps.emitEvent});emitted.push({type:ledgerEvent.type,emitted:result.emitted})}return{processed:!0,outcome:response.status,emitted}}var init_supervisor_merge=__esm({"src/conductor/supervisor-merge.ts"(){"use strict";init_bridge_api_client();init_merge_ledger()}});async function dispatchSupervisorNotification(epicRunId,candidate,assessment,idempotencyKey){let result=await resolveConductorBridgeApiAccess();if(!result.ok)return;let{access:access2}=result,url=buildConductorJiraUrl(access2.baseUrl,`/epic-runs/${encodeURIComponent(epicRunId)}/notifications`),payload={repo_name:access2.repoName,idempotency_key:idempotencyKey,summary:{reason:candidate.reason,kind:candidate.kind,worker_id:candidate.worker_id??null,elapsed_ms:candidate.elapsed_ms,ticket_key:candidate.context?.ticket_key??null,classification:assessment.classification}},headers={"X-API-Key":access2.apiKey,"Content-Type":"application/json"};try{await fetchConductorJsonPostWithTimeout(url,headers,JSON.stringify(payload),CONDUCTOR_FETCH_TIMEOUT_MS,fetch)}catch{}}var init_supervisor_notification=__esm({"src/conductor/supervisor-notification.ts"(){"use strict";init_bridge_api_client()}});var supervisor_runtime_exports={};__export(supervisor_runtime_exports,{runSupervisor:()=>runSupervisor});function deterministicAssessment(candidate){return{classification:"stuck",confidence:1,reason:candidate.reason}}function terminalStatus(state){return Object.values(state.workers).some(w=>w.state==="failed")?"failed":"complete"}async function processEscalations(state,config,deps){let now=deps.now(),candidates=findSupervisorEscalationCandidates(state,config,now);for(let candidate of candidates){let decision=shouldEmitEscalation(state,candidate,config,now);if(!decision.emit)continue;let idempotency={run_id:state.run_id,worker_id:candidate.worker_id,reason:candidate.reason,kind:candidate.kind,cooldown_window:decision.cooldown_window},idempotencyKey=makeSupervisorIdempotencyKey(idempotency),assessment=deterministicAssessment(candidate);state.latest_assessment=assessment;let outcome2="skipped";try{outcome2=(await deps.emitAssessment({run_id:state.run_id,worker_id:candidate.worker_id,assessment,details:{elapsed_ms:candidate.elapsed_ms,...candidate.context},idempotency})).emitted?"emitted":"duplicate"}catch{outcome2="skipped"}if(recordEscalationResult(state,candidate,decision.cooldown_window,idempotencyKey,outcome2,now),(outcome2==="emitted"||outcome2==="duplicate")&&candidate.worker_id)try{await sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,{sendMessage:deps.sendWorkerMessage})}catch{}if(outcome2==="emitted"&&(deps.log(formatEscalationForTerminal(state.run_id,candidate)),deps.dispatchNotification))try{await deps.dispatchNotification(state.run_id,candidate,assessment,idempotencyKey)}catch{}}}async function runSupervisor(options,deps={}){let runId=typeof options.run_id=="string"?options.run_id.trim():"";if(runId.length===0)throw new ConductorValidationError("Supervisor requires exactly one non-empty --run-id.");let config=options.config??resolveSupervisorConfig(options.overrides??{}),now=deps.now??(()=>Date.now()),log=deps.log??(m=>process.stdout.write(`${m}
@@ -6091,7 +6091,7 @@ If the call fails, fix what it reports and call it again.
6091
6091
  ## Return
6092
6092
 
6093
6093
  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.
6094
- `};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`) and GitHub Copilot CLI\n (`~/.copilot/mcp-config.json`).\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, GitHub\n Copilot CLI, Cursor, 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. Yes, I have received a token\n2. No, I need one\n```\n\n- Choose **2** 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 **1** 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, Copilot CLI, Codex, or Windsurf selection, a non-interactive\nrun, or a declined 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/` | The slash commands your editor runs | 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 and Copilot CLI *are* written automatically,\neven though their files are 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</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 **2** 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`, `copilot-cli`, `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<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.42", "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>VS Code / 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.42", "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.42", "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 CLI (~/.copilot/mcp-config.json)</strong></summary>\n\nCopilot CLI reads a single global file. The installer writes this one for you when\nyou select `copilot-cli`; the shape below is what it produces.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "local",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.42", "serve"],\n "tools": ["*"],\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.42", "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.42", "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\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-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review). For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every `/review-ticket` flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.\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 `/full-automation`, 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. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket, or debugging guidance when the ticket is a bug.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Critique Ticket**\n- **What it does:** Critiques a ticket against your project\'s standards and lists the deviations and improvements it found.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before anyone works it.\n- **How to use it:** `/critique-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. 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**6. 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**7. 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**8. Request PRD**\n- **What it does:** Generates a product requirements document for a ticket covering the problem, the goals, and the success metrics.\n- **When it\'s useful:** (Architecture | Refinement) When a piece of work needs its problem, goals, and success metrics written down before anyone designs a solution.\n- **How to use it:** `/create-doc BAPI-123 --doc-type prd`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain from a raw idea through tickets and reviews to implementation sessions.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 it creates tickets, spawns worktrees, and carries scheduling flags free text cannot).\n- **Flags:** `--require-approval` re-enable the approval gates; the chain runs end to end by default \xB7 `--max-children <n>` cap how many child tickets an epic decomposes into.\n\n**10. Update Ticket Description**\n- **What it does:** Rewrites a ticket\'s description with AI, using the ticket\'s own content and its reference material. A rewrite that changes more than 60% of the description is held for review instead of applied.\n- **When it\'s useful:** (Refinement) When a ticket has accumulated comments, attachments, or links and its description no longer reflects them.\n- **How to use it:** Ask your agent \u2014 "Update the description for BAPI-123."\n- **Flags:** None. Poll the ticket\'s state for the outcome; if the update was held for review, read the proposal instead of applying it blind.\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:** `/update-ticket BAPI-123` (command only \u2014 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.\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:** None.\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 Ticket**\n- **What it does:** Estimates the development effort for one ticket. Use Estimate Epic instead for a whole epic or a named group of tickets.\n- **When it\'s useful:** (Refinement) When you need a size for a single ticket before committing to it.\n- **How to use it:** Ask your agent \u2014 "Estimate BAPI-123."\n- **Flags:** Ask for a fresh estimate to regenerate rather than reuse a stored one.\n\n**9. 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<!-- 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) \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\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\n| `/check-ci PROJ-123` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\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 (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, 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:** list/inspect pipeline recipes, run/resume/list/delete pipeline runs (the engine under the orchestration commands), and resume a full-automation chain that stopped at an approval gate or was interrupted.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, architecture) 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\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). 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.** 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` | `sfcc_setup_status`, `check_permissions` only |\n| `sfcc` | the above + 8 OCAPI read tools + `sfcc_log_query` |\n| `sfcc-write` | the above diagnostics + the 9 destructive write tools |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable |\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: two always-on diagnostics, the `sfcc` profile\'s **read-only** surface (eight OCAPI reads plus `sfcc_log_query`), and 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** (always available, no profile needed)\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. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\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; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. 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, a supervisor peer tab, and the `check_messages` message-relay prompt). **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\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. 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-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\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### `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\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\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\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### `conduct-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.\n\n```\nnpx -y @bridge_gpt/mcp-server conduct-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`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor 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| `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), `sfcc` (+ the 8 heavy SFCC read tools and `sfcc_log_query` \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), `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 `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. 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## 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 **92 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`, `get_my_role`, `get_docs_dir`\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`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\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 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\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. | `/review-ticket 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` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\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:
6094
+ `};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`) and GitHub Copilot CLI\n (`~/.copilot/mcp-config.json`).\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, GitHub\n Copilot CLI, Cursor, 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. Yes, I have received a token\n2. No, I need one\n```\n\n- Choose **2** 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 **1** 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, Copilot CLI, Codex, or Windsurf selection, a non-interactive\nrun, or a declined 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/` | The slash commands your editor runs | 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 and Copilot CLI *are* written automatically,\neven though their files are 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</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 **2** 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`, `copilot-cli`, `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<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.44", "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>VS Code / 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.44", "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.44", "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 CLI (~/.copilot/mcp-config.json)</strong></summary>\n\nCopilot CLI reads a single global file. The installer writes this one for you when\nyou select `copilot-cli`; the shape below is what it produces.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "local",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.44", "serve"],\n "tools": ["*"],\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.44", "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.44", "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\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-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review). For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every `/review-ticket` flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.\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 `/full-automation`, 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. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket, or debugging guidance when the ticket is a bug.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Critique Ticket**\n- **What it does:** Critiques a ticket against your project\'s standards and lists the deviations and improvements it found.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before anyone works it.\n- **How to use it:** `/critique-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. 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**6. 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**7. 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**8. Request PRD**\n- **What it does:** Generates a product requirements document for a ticket covering the problem, the goals, and the success metrics.\n- **When it\'s useful:** (Architecture | Refinement) When a piece of work needs its problem, goals, and success metrics written down before anyone designs a solution.\n- **How to use it:** `/create-doc BAPI-123 --doc-type prd`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain from a raw idea through tickets and reviews to implementation sessions.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 it creates tickets, spawns worktrees, and carries scheduling flags free text cannot).\n- **Flags:** `--require-approval` re-enable the approval gates; the chain runs end to end by default \xB7 `--max-children <n>` cap how many child tickets an epic decomposes into.\n\n**10. Update Ticket Description**\n- **What it does:** Rewrites a ticket\'s description with AI, using the ticket\'s own content and its reference material. A rewrite that changes more than 60% of the description is held for review instead of applied.\n- **When it\'s useful:** (Refinement) When a ticket has accumulated comments, attachments, or links and its description no longer reflects them.\n- **How to use it:** Ask your agent \u2014 "Update the description for BAPI-123."\n- **Flags:** None. Poll the ticket\'s state for the outcome; if the update was held for review, read the proposal instead of applying it blind.\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:** `/update-ticket BAPI-123` (command only \u2014 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.\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:** None.\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 Ticket**\n- **What it does:** Estimates the development effort for one ticket. Use Estimate Epic instead for a whole epic or a named group of tickets.\n- **When it\'s useful:** (Refinement) When you need a size for a single ticket before committing to it.\n- **How to use it:** Ask your agent \u2014 "Estimate BAPI-123."\n- **Flags:** Ask for a fresh estimate to regenerate rather than reuse a stored one.\n\n**9. 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<!-- 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) \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\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\n| `/check-ci PROJ-123` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\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 (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, 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:** list/inspect pipeline recipes, run/resume/list/delete pipeline runs (the engine under the orchestration commands), and resume a full-automation chain that stopped at an approval gate or was interrupted.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, architecture) 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\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). 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.** 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` | `sfcc_setup_status`, `check_permissions` only |\n| `sfcc` | the above + 8 OCAPI read tools + `sfcc_log_query` |\n| `sfcc-write` | the above diagnostics + the 9 destructive write tools |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable |\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: two always-on diagnostics, the `sfcc` profile\'s **read-only** surface (eight OCAPI reads plus `sfcc_log_query`), and 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** (always available, no profile needed)\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. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\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; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. 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, a supervisor peer tab, and the `check_messages` message-relay prompt). **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\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. 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-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\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### `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\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\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\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### `conduct-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.\n\n```\nnpx -y @bridge_gpt/mcp-server conduct-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`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor 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| `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), `sfcc` (+ the 8 heavy SFCC read tools and `sfcc_log_query` \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), `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 `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. 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## 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 **92 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`, `get_my_role`, `get_docs_dir`\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`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\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 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\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. | `/review-ticket 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` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\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:
6095
6095
  ${errors.join(`
6096
6096
  `)}`);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}}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:
6097
6097
  ${unowned.join(`
@@ -7288,7 +7288,7 @@ Agents: scaffolded ${agentTotal} agent${agentTotal===1?"":"s"}`),agentWritten.si
7288
7288
  `);if(buffer.length>MAX_BUFFERED_ADVISORY_LINE_CHARS){buffer="",discardingOversizedLine=!0;return}buffer.length>0&&consumeAdvisoryLine(buffer)},setGitTelemetry(telemetry){git2=telemetry},setExitCode(code){exitCode=code},setAttemptStartSha(sha){let normalized=normalizeSha2(sha);normalized&&(attemptStartSha=normalized)},setAttemptEndSha(sha){let normalized=normalizeSha2(sha);normalized&&(attemptEndSha=normalized)},git(){return git2},snapshot(){let residue={...git2};return advisory.phase_hint&&(residue.phase_hint=advisory.phase_hint),advisory.last_stdout_at&&(residue.last_stdout_at=advisory.last_stdout_at),rateLimit!==void 0&&(residue.rate_limit=rateLimit),exitCode!==void 0&&(residue.exit_code=exitCode),attemptStartSha!==void 0&&(residue.attempt_start_sha=attemptStartSha),attemptEndSha!==void 0&&(residue.attempt_end_sha=attemptEndSha),residue}}}init_mcp_identity();var CLAUDE_ADAPTER_ID="claude-reference",CLAUDE_ADAPTER_STRATEGY_ID="claude-strict-mcp-v1",CLAUDE_ADAPTER_VERSION="1.0.0",CLAUDE_OAUTH_TOKEN_ENV="CLAUDE_CODE_OAUTH_TOKEN",CLAUDE_REQUIRED_MCP_SERVER_NAME=MCP_SERVER_NAME,CLAUDE_REDACTION_REPLACEMENT="[redacted]",VERSION_DETAIL_MAX=200;function normalizeClaudeVersion(output){return(output.stdout||output.stderr||"").trim().slice(0,VERSION_DETAIL_MAX)}function parseClaudeVersion(output){let version=normalizeClaudeVersion(output);return output.exitCode!==0||version.length===0?{version,fingerprint:null}:{version,fingerprint:version.replace(/\s+/g," ").trim()}}function isSystemInitEvent(value){if(!value||typeof value!="object"||Array.isArray(value))return!1;let obj=value;return obj.type==="system"&&obj.subtype==="init"}function parseClaudeInitEvent(line){let trimmed=typeof line=="string"?line.trim():"";if(trimmed.length===0)return{kind:"not-init"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"not-init"}}if(!isSystemInitEvent(parsed))return{kind:"not-init"};let servers=parsed.mcp_servers;if(!Array.isArray(servers))return{kind:"malformed-init"};let names=[];for(let entry of servers){if(!entry||typeof entry!="object"||Array.isArray(entry))return{kind:"malformed-init"};let name=entry.name;if(typeof name!="string"||name.trim().length===0)return{kind:"malformed-init"};names.push(name)}return{kind:"init",serverNames:normalizeMcpServerNames(names)}}function classifyClaudeAuthFailure(stdoutExcerpt){if(typeof stdoutExcerpt!="string"||stdoutExcerpt.length===0)return{notAuthenticated:!1,reasonCode:null};for(let rawLine of stdoutExcerpt.split(`
7289
7289
  `)){let trimmed=rawLine.trim();if(trimmed.length===0)continue;let parsed;try{parsed=JSON.parse(trimmed)}catch{continue}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))continue;let obj=parsed;if(obj.type!=="result"||obj.is_error!==!0)continue;let resultText=typeof obj.result=="string"?obj.result:"",notLoggedIn=obj.error==="authentication_failed"||resultText.includes("Not logged in")||resultText.includes("Please run /login"),invalidToken=obj.api_error_status===401||resultText.includes("OAuth access token is invalid");if(notLoggedIn)return{notAuthenticated:!0,reasonCode:"not-logged-in"};if(invalidToken)return{notAuthenticated:!0,reasonCode:"invalid-token"}}return{notAuthenticated:!1,reasonCode:null}}function resolveClaudeModelAlias(spec,payload){let p=payload&&typeof payload=="object"?payload:{},directRaw=p.model_alias;if(typeof directRaw=="string"&&directRaw.trim().length>0){let direct=directRaw.trim(),allowed=!spec.staticModelAliasAllowlist||spec.staticModelAliasAllowlist.includes(direct);if(isValidModelAlias(direct)&&allowed)return direct}let tier=isModelTier(p.model_tier)?p.model_tier:null;return resolveModelAlias(spec,tier,null)}function buildClaudeArgv(prompt,alias,mcpConfigPath,posture){let argv=["-p",prompt,"--output-format","stream-json","--verbose"];return alias&&argv.push("--model",alias),posture==="accept_edits"?argv.push("--permission-mode","acceptEdits"):argv.push("--dangerously-skip-permissions"),argv.push("--strict-mcp-config","--mcp-config",mcpConfigPath),argv}function buildClaudeWorkerEnv(parentEnv,options={}){let env=buildExecutorBaseWorkerEnv(parentEnv,options),token=parentEnv[CLAUDE_OAUTH_TOKEN_ENV];return typeof token=="string"&&(env[CLAUDE_OAUTH_TOKEN_ENV]=token),env}function createClaudeExecutorAdapter(spec,deps={}){let executable={executable:spec.command,versionArgv:["--version"],parseVersion:parseClaudeVersion},platform={supportedPlatforms:SUPPORTED_EXECUTOR_PLATFORMS,evaluate:evaluateExecutorPlatform},headlessInvocation={buildSpawnShape(input){let envOptions={};return input.effectiveBaseBranch!==void 0&&(envOptions.effectiveBaseBranch=input.effectiveBaseBranch),input.indexScope!==void 0&&(envOptions.indexScope=input.indexScope),{executable:spec.command,argv:buildClaudeArgv(input.prompt,input.modelAlias,input.mcpConfigPath,input.posture),env:buildClaudeWorkerEnv(input.parentEnv,envOptions)}},resolveModelAlias:payload=>resolveClaudeModelAlias(spec,payload)},mcpScoping={strategyId:"strict-mcp-config",requiredServerName:CLAUDE_REQUIRED_MCP_SERVER_NAME,supportsStrictConfig:!0},mcpInitParsing={strategyId:"claude-system-init",parseInitEvent:parseClaudeInitEvent},auth={managedAuthCarriers:[],operatorOwnedPassthroughs:[{passthroughId:"claude-code-oauth-token",envName:CLAUDE_OAUTH_TOKEN_ENV,billingClass:"subscription",ownership:"operator-owned",rule:"forward-when-present"}]},authFailureDetection={strategyId:"claude-stream-json-result",classify:classifyClaudeAuthFailure},denyEnforcement2={strategyId:"claude-settings-deny",async provisionWorktreeDenyLayer(input){let provision=deps.provisionDenyLayer??provisionExecutorDenyLayer,provisioningDeps=deps.denyProvisioningDeps;return provisioningDeps?provision(input.worktreePath,{baseBranch:input.baseBranch},provisioningDeps):{ok:!1,warning:"deny-layer provisioning deps were not supplied to the Claude adapter; continuing fail-open"}},async probeEnforcement(options={}){let result=await(deps.runDenyPreflight??runDenyEnforcementPreflight)(options.timeoutMs===void 0?{}:{timeoutMs:options.timeoutMs});return{enforced:result.enforced===!0,layer:String(result.layer),degraded:result.degraded===!0,detail:typeof result.detail=="string"?result.detail:"",warnings:Array.isArray(result.warnings)?[...result.warnings]:[]}}},redaction={strategyId:"claude-env-name-redaction",secretEnvNames:[CLAUDE_OAUTH_TOKEN_ENV],replacement:CLAUDE_REDACTION_REPLACEMENT},lifecycle={strategyId:"claude-no-lifecycle",declaration:{kind:"none"}},advisoryMcpInspection={strategyId:"claude-user-config-shadowing",async inspect(input){let inspect=deps.inspectClaudeUserConfig??inspectClaudeUserConfigForMcpShadowing,readFile19=deps.readFile;if(!readFile19)return{warnings:["Claude user-config MCP shadowing check did not run; no read boundary was supplied"]};try{let inspection=await inspect({claudeConfigPath:resolveClaudeUserConfigPath(input.homedir,input.platform),platform:input.platform,cwd:input.cwd,mainRepositoryPath:input.cwd,...input.worktreePath===void 0?{}:{worktreePath:input.worktreePath}},{readFile:readFile19});return{warnings:evaluateClaudeMcpShadowingPolicy(inspection).warnings}}catch{return{warnings:["Claude user-config MCP shadowing check did not complete; could not verify worker MCP integrity"]}}}};return{identity:{agentId:spec.name,adapterId:CLAUDE_ADAPTER_ID,adapterVersion:CLAUDE_ADAPTER_VERSION,strategyId:CLAUDE_ADAPTER_STRATEGY_ID},executable:supported(executable),platform:supported(platform),headlessInvocation:supported(headlessInvocation),mcpScoping:supported(mcpScoping),mcpInitParsing:supported(mcpInitParsing),auth:supported(auth),authFailureDetection:supported(authFailureDetection),denyEnforcement:supported(denyEnforcement2),redaction:supported(redaction),lifecycle:supported(lifecycle),advisoryMcpInspection:supported(advisoryMcpInspection)}}var ADAPTER_FACTORIES={"claude-strict-mcp-v1":(spec,deps)=>createClaudeExecutorAdapter(spec,deps.claude??{})};function listImplementedAdapterStrategies(){return Object.keys(ADAPTER_FACTORIES).sort()}function resolveExecutorAgentAdapter(agentName,deps={}){let spec=resolveAgentSpec(agentName);if(!spec)return{supported:!1,kind:"unknown-agent",message:`no agent named '${agentName}' is registered; cannot resolve an executor adapter`};let registration=spec.executorAdapter;if(!registration)return{supported:!1,kind:"no-adapter-registration",message:`agent '${spec.name}' declares no executor adapter registration, so it cannot spawn conductor workers. This is not a fallback condition: no other agent's adapter is substituted.`};let factory=ADAPTER_FACTORIES[registration.strategyId];if(!factory)return{supported:!1,kind:"unimplemented-strategy",message:`executor adapter strategy '${registration.strategyId}' (agent '${spec.name}') has no implementation. Implemented strategies: ${listImplementedAdapterStrategies().join(", ")}.`};let adapter=factory(spec,deps),identity=adapter.identity,mismatches=[];if(identity.agentId!==spec.name&&mismatches.push(`agentId '${identity.agentId}' != registry agent '${spec.name}'`),identity.adapterId!==registration.adapterId&&mismatches.push(`adapterId '${identity.adapterId}' != registered '${registration.adapterId}'`),identity.strategyId!==registration.strategyId&&mismatches.push(`strategyId '${identity.strategyId}' != registered '${registration.strategyId}'`),identity.adapterVersion!==registration.adapterVersion&&mismatches.push(`adapterVersion '${identity.adapterVersion}' != registered '${registration.adapterVersion}'`),mismatches.length>0)return{supported:!1,kind:"identity-mismatch",message:`executor adapter identity does not match its registry registration: ${mismatches.join("; ")}`};let validation=validateExecutorAdapterCapabilities(adapter);return validation.ok?{supported:!0,adapter,spec,validation}:{supported:!1,kind:"incomplete-capabilities",message:formatAdapterRefusal(identity,validation.refusals),refusals:validation.refusals}}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"]);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)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path53=>`${trimmedBase}/jira${path53}`;return{buildUrl:buildUrl2,buildApiUrl:path53=>`${trimmedBase}${path53}`,buildGetUrl:(path53,params)=>{let url=new URL(buildUrl2(path53));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=2500,TOOL_SURFACE_POLL_MIN_MS=12e3,TOOL_SURFACE_POLL_MAX_MS=18e3;function timeoutResult(){return{reason:"timeout",blockedTools:new Set}}function malformedResult(subtype){return{reason:"malformed",subtype,blockedTools:new Set}}function validateToolSurfacePayload(body){if(body===null||typeof body!="object"||Array.isArray(body))return malformedResult("invalid-shape");let p=body;if(typeof p.schema_version!="number"||!Number.isInteger(p.schema_version))return malformedResult("invalid-shape");if(!RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS.has(p.schema_version))return malformedResult("unsupported-schema");if(typeof p.complete!="boolean"||typeof p.evaluated_tool_count!="number"||!Number.isInteger(p.evaluated_tool_count)||p.evaluated_tool_count<0||typeof p.catalog_revision!="string"||!Array.isArray(p.blocked_tools)||!p.blocked_tools.every(t=>typeof t=="string"))return malformedResult("invalid-shape");if(!p.complete)return malformedResult("incomplete");if(p.catalog_revision.length===0)return malformedResult("invalid-shape");let blockedTools=new Set(p.blocked_tools);return{reason:"blocked",catalogRevision:p.catalog_revision,evaluatedToolCount:p.evaluated_tool_count,blockedTools}}async function probeToolSurface(options){let deadlineMs=options.deadlineMs??TOOL_SURFACE_PROBE_DEADLINE_MS,controller=new AbortController,onLifecycleAbort=()=>controller.abort();options.abortSignal&&(options.abortSignal.aborted?controller.abort():options.abortSignal.addEventListener("abort",onLifecycleAbort,{once:!0}));let timer,deadlinePromise=new Promise(resolve2=>{timer=setTimeout(()=>{controller.abort(),resolve2(timeoutResult())},deadlineMs)}),abortPromise=new Promise(resolve2=>{if(controller.signal.aborted){resolve2(timeoutResult());return}controller.signal.addEventListener("abort",()=>resolve2(timeoutResult()),{once:!0})}),workPromise=(async()=>{try{let headers=await options.resolveHeaders();if(controller.signal.aborted)return timeoutResult();let resp=await options.fetchFn(options.url,{method:"GET",headers,signal:controller.signal});if(!resp.ok)return malformedResult("non-2xx");let parsed;try{parsed=await resp.json()}catch{return controller.signal.aborted?timeoutResult():malformedResult("invalid-json")}return validateToolSurfacePayload(parsed)}catch{return controller.signal.aborted?timeoutResult():malformedResult("network")}})();try{return await Promise.race([workPromise,deadlinePromise,abortPromise])}finally{timer&&clearTimeout(timer),options.abortSignal&&options.abortSignal.removeEventListener("abort",onLifecycleAbort)}}var defaultScheduler={setTimeout:(callback,ms)=>setTimeout(callback,ms),clearTimeout:handle=>clearTimeout(handle),random:()=>Math.random()};function logDecision(logger,result,hiddenCount,hiddenNames){let revision=result.reason==="blocked"?result.catalogRevision:"n/a",subtype=result.reason==="malformed"?result.subtype:"n/a";logger(`tool-surface gating: reason=${result.reason} subtype=${subtype} hidden=${hiddenCount} revision=${revision} hidden_tools=[${hiddenNames.join(", ")}]`)}function createToolSurfaceGate(options){let{startupProbe,advertised,originalListHandler,freshProbe,notify,logger,lifecycleController}=options,scheduler=options.scheduler??defaultScheduler,advertisedNames=new Set(advertised.map(r=>r.name)),hiddenNames=new Set,lastServedVisible=null,catalogRevision=null,startupApplied=!1,timer,closed=!1;function deriveHidden(result){if(result.reason!=="blocked"||result.blockedTools.size===0)return new Set;let hidden=new Set;for(let id of result.blockedTools)advertisedNames.has(id)&&hidden.add(id);return hidden}function deriveVisible(hidden){let visible=new Set;for(let reg of advertised)reg.isEnabled()&&(hidden.has(reg.name)||visible.add(reg.name));return visible}function applyDecision(result){let nextHidden=deriveHidden(result);hiddenNames=nextHidden,logDecision(logger,result,nextHidden.size,Array.from(nextHidden)),result.reason==="blocked"&&result.catalogRevision!==catalogRevision&&(catalogRevision!==null&&logger(`tool-surface gating: catalog_revision ${catalogRevision} -> ${result.catalogRevision}`),catalogRevision=result.catalogRevision)}function projectList(original){let tools=original.tools.filter(tool=>!hiddenNames.has(tool.name));return{...original,tools}}let handleList=async(request,extra)=>{let startupResult=await startupProbe;startupApplied||(startupApplied=!0,applyDecision(startupResult));let original=await originalListHandler(request,extra),projected=projectList(original);return lastServedVisible=new Set(projected.tools.map(t=>t.name)),projected};async function pollOnce(){let result;try{result=await freshProbe()}catch{result=timeoutResult()}if(closed)return;let previousVisibleServed=lastServedVisible;applyDecision(result);let nextVisible=deriveVisible(hiddenNames);if(previousVisibleServed!==null&&!setsEqual(previousVisibleServed,nextVisible)){lastServedVisible=nextVisible;try{notify()}catch{logger("tool-surface gating: notification failed (suppressed)")}}}function scheduleNext(){if(closed)return;let span=TOOL_SURFACE_POLL_MAX_MS-TOOL_SURFACE_POLL_MIN_MS,delay=Math.round(TOOL_SURFACE_POLL_MIN_MS+scheduler.random()*span);timer=scheduler.setTimeout(()=>{pollOnce().finally(()=>{scheduleNext()})},delay),timer&&typeof timer.unref=="function"&&timer.unref()}function startPolling(){closed||scheduleNext()}function close(){closed||(closed=!0,timer&&(scheduler.clearTimeout(timer),timer=void 0),lifecycleController.signal.aborted||lifecycleController.abort())}return{handleList,startPolling,close}}function setsEqual(a,b){if(a.size!==b.size)return!1;for(let v of a)if(!b.has(v))return!1;return!0}var UPDATE_ADVISORY_META_KEY="bridge-api/update-advisory",ADVISORY_HOST_TOOL="ping";function decorateListResultWithUpdateAdvisory(original,advisory){if(!advisory)return original;try{let tools=original.tools.map(tool=>tool.name===ADVISORY_HOST_TOOL?{...tool,description:`${advisory}
7290
7290
 
7291
- ${tool.description??""}`.trimEnd()}:tool);return{...original,tools,_meta:{...original._meta??{},[UPDATE_ADVISORY_META_KEY]:advisory}}}catch{return original}}function createUpdateAdvisoryListHandler(inner,getAdvisory,onListServed){return async(request,extra)=>{let result=await inner(request,extra);try{return onListServed?.(),decorateListResultWithUpdateAdvisory(result,getAdvisory())}catch{return result}}}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_bridge_api_client();import path24 from"node:path";var CONDUCT_EPIC_CHECKPOINT_VERSION=1,CONDUCT_EPIC_DEFAULT_SOFT_SECONDS=3600,CONDUCT_EPIC_DEFAULT_HARD_SECONDS=10800,CONDUCT_EPIC_MAX_JOURNAL_LINES=50,CONDUCT_EPIC_TICKET_STATUSES=["pending","in_progress","merged","done","needs_human"],atomicWriteCounter=0;function resolveConductEpicStateDirectory(repoName,deps){let xdg=deps.env.XDG_CONFIG_HOME,bridgeDir=xdg&&xdg.trim().length>0?path24.join(xdg,"bridge"):path24.join(deps.homedir(),".config","bridge");return path24.join(bridgeDir,"conduct",repoName)}function resolveConductEpicCheckpointPath(repoName,epicKey,deps){return path24.join(resolveConductEpicStateDirectory(repoName,deps),`${epicKey}.json`)}function resolveConductEpicLockPath(checkpointPath){let dir=path24.dirname(checkpointPath),base=path24.basename(checkpointPath).replace(/\.json$/,"");return path24.join(dir,`${base}.lock`)}function resolveConductEpicBackupPath(checkpointPath){return`${checkpointPath}.prev`}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeConductEpicCheckpoint(value){return!isRecord(value)||!Array.isArray(value.tickets)?value:{...value,index_scope_id:"index_scope_id"in value?value.index_scope_id:null,index_scope_lease_epoch:"index_scope_lease_epoch"in value?value.index_scope_lease_epoch:null,tickets:value.tickets.map(ticket=>{if(!isRecord(ticket))return ticket;let normalized={...ticket};return"parse_requested_at"in normalized||(normalized.parse_requested_at=null),"parse_requested_for_sha"in normalized||(normalized.parse_requested_for_sha=null),normalized})}}function isText(value){return typeof value=="string"&&value.trim().length>0}function isCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}function isNullableText(value){return value===null||isText(value)}function fail(error){return{ok:!1,error}}function validateCounters(value,where){if(!isRecord(value))return fail(`${where}.counters must be an object`);for(let key of["sessions_spawned","plan_generations_observed","merge_attempts"])if(!isCount(value[key]))return fail(`${where}.counters.${key} must be a non-negative integer`);return Object.keys(value).length!==3?fail(`${where}.counters carries unexpected fields`):{ok:!0}}function validateTicket(value,index){let where=`tickets[${index}]`;if(!isRecord(value))return fail(`${where} must be an object`);if(!isText(value.key))return fail(`${where}.key must be a non-empty string`);if(typeof value.status!="string"||!CONDUCT_EPIC_TICKET_STATUSES.includes(value.status))return fail(`${where}.status must be one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`);if(!isNullableText(value.branch))return fail(`${where}.branch must be a non-empty string or null`);if(value.pr_number!==null&&!(isCount(value.pr_number)&&value.pr_number>0))return fail(`${where}.pr_number must be a positive integer or null`);if(!isNullableText(value.last_seen_head))return fail(`${where}.last_seen_head must be a non-empty string or null`);if(!isNullableText(value.last_state_change_at))return fail(`${where}.last_state_change_at must be a timestamp string or null`);if(!isNullableText(value.spawned_at))return fail(`${where}.spawned_at must be a timestamp string or null`);if(!isCount(value.respawns))return fail(`${where}.respawns must be a non-negative integer`);if(!isCount(value.conflict_attempts))return fail(`${where}.conflict_attempts must be a non-negative integer`);let counters=validateCounters(value.counters,where);if(!counters.ok)return counters;for(let key of["parse_requested_at","parse_requested_for_sha"])if(!isNullableText(value[key]))return fail(`${where}.${key} must be a non-empty string or null`);return!Array.isArray(value.journal)||value.journal.some(line=>typeof line!="string")?fail(`${where}.journal must be an array of strings`):value.journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?fail(`${where}.journal must hold at most ${CONDUCT_EPIC_MAX_JOURNAL_LINES} entries`):{ok:!0}}function validateCiLastPoll(value){return value===null?{ok:!0}:isRecord(value)?isText(value.head_sha)?!Array.isArray(value.required)||value.required.some(n=>typeof n!="string")?fail("ci_last_poll.required must be an array of strings"):typeof value.results_fingerprint!="string"?fail("ci_last_poll.results_fingerprint must be a string"):isText(value.at)?{ok:!0}:fail("ci_last_poll.at must be a timestamp string"):fail("ci_last_poll.head_sha must be a non-empty string"):fail("ci_last_poll must be an object or null")}function validateNeedsHuman(value){if(value===null)return{ok:!0};if(!isRecord(value))return fail("needs_human must be an object or null");for(let key of["reason","evidence","at"])if(typeof value[key]!="string")return fail(`needs_human.${key} must be a string`);return{ok:!0}}function validateLock(value){return isRecord(value)?isCount(value.owner_pid)?typeof value.host!="string"?fail("lock.host must be a string"):typeof value.acquired_at!="string"?fail("lock.acquired_at must be a string"):Object.keys(value).length!==3?fail("lock carries unexpected fields"):{ok:!0}:fail("lock.owner_pid must be a non-negative integer"):fail("lock must be an object")}function validateConductEpicCheckpoint(candidate){let value=normalizeConductEpicCheckpoint(candidate);if(!isRecord(value))return fail("checkpoint must be a JSON object");if(value.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return fail(`checkpoint version must be ${CONDUCT_EPIC_CHECKPOINT_VERSION}`);for(let key of["epic_key","repo_name","epic_branch","base_branch_original"])if(!isText(value[key]))return fail(`${key} must be a non-empty string`);for(let key of["created_at","updated_at"])if(!isText(value[key]))return fail(`${key} must be a timestamp string`);if(!isNullableText(value.index_scope_id))return fail("index_scope_id must be a non-empty string or null");if(value.index_scope_lease_epoch!==null&&(typeof value.index_scope_lease_epoch!="number"||!Number.isInteger(value.index_scope_lease_epoch)||value.index_scope_lease_epoch<0))return fail("index_scope_lease_epoch must be a non-negative integer or null");let deadlines=value.deadlines;if(!isRecord(deadlines))return fail("deadlines must be an object");if(!isCount(deadlines.soft_seconds))return fail("deadlines.soft_seconds must be a non-negative integer");if(!isCount(deadlines.hard_seconds))return fail("deadlines.hard_seconds must be a non-negative integer");if(!Array.isArray(value.tickets)||value.tickets.length===0)return fail("tickets must be a non-empty array");let seen=new Set;for(let i=0;i<value.tickets.length;i+=1){let result=validateTicket(value.tickets[i],i);if(!result.ok)return result;let key=value.tickets[i].key;if(seen.has(key))return fail(`tickets contains a duplicate key: ${key}`);seen.add(key)}let counters=value.counters;if(!isRecord(counters))return fail("counters must be an object");if(!isCount(counters.iterations))return fail("counters.iterations must be a non-negative integer");if(!isCount(counters.merges))return fail("counters.merges must be a non-negative integer");let needsHuman=validateNeedsHuman(value.needs_human);if(!needsHuman.ok)return needsHuman;let ciLastPoll=validateCiLastPoll(value.ci_last_poll);if(!ciLastPoll.ok)return ciLastPoll;let lock=validateLock(value.lock);return lock.ok?{ok:!0}:lock}function errorCode(err){let code=err?.code;return typeof code=="string"?code:void 0}async function readConductEpicCheckpoint(checkpointPath,fs7){let raw;try{raw=await fs7.readFile(checkpointPath)}catch(err){return errorCode(err)==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:`checkpoint at ${checkpointPath} could not be read`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"corrupt",error:`checkpoint at ${checkpointPath} is not valid JSON`}}if(isRecord(parsed)&&parsed.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return{kind:"unsupported-version",error:`checkpoint at ${checkpointPath} has version ${JSON.stringify(parsed.version)}; this CLI only understands version ${CONDUCT_EPIC_CHECKPOINT_VERSION}`};let validation=validateConductEpicCheckpoint(parsed);return validation.ok?{kind:"ok",checkpoint:normalizeConductEpicCheckpoint(parsed)}:{kind:"invalid",error:`checkpoint at ${checkpointPath} is invalid: ${validation.error}`}}function createInitialConductEpicCheckpoint(input){return{version:CONDUCT_EPIC_CHECKPOINT_VERSION,epic_key:input.epicKey,repo_name:input.repoName,epic_branch:input.epicBranch,base_branch_original:input.baseBranchOriginal,index_scope_id:input.indexScopeId??null,index_scope_lease_epoch:input.indexScopeLeaseEpoch??null,created_at:input.now,updated_at:input.now,deadlines:input.deadlines??{soft_seconds:CONDUCT_EPIC_DEFAULT_SOFT_SECONDS,hard_seconds:CONDUCT_EPIC_DEFAULT_HARD_SECONDS},tickets:input.ticketKeys.map(key=>({key,status:"pending",branch:null,pr_number:null,last_seen_head:null,last_state_change_at:null,spawned_at:null,respawns:0,conflict_attempts:0,counters:{sessions_spawned:0,plan_generations_observed:0,merge_attempts:0},parse_requested_at:null,parse_requested_for_sha:null,journal:[]})),counters:{iterations:0,merges:0},needs_human:null,ci_last_poll:null,lock:input.lock}}function appendTicketJournal(ticket,line){let journal=[...ticket.journal,line];return{...ticket,journal:journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?journal.slice(journal.length-CONDUCT_EPIC_MAX_JOURNAL_LINES):journal}}async function writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,fs7,options={}){let validation=validateConductEpicCheckpoint(checkpoint);if(!validation.ok)return{ok:!1,error:`refusing to write an invalid checkpoint: ${validation.error}`};let dir=path24.dirname(checkpointPath);try{await fs7.mkdir(dir,{recursive:!0}),options.skipChmod||await fs7.chmod(dir,448)}catch{return{ok:!1,error:`could not prepare the checkpoint directory ${dir}`}}let content=`${JSON.stringify(checkpoint,null,2)}
7291
+ ${tool.description??""}`.trimEnd()}:tool);return{...original,tools,_meta:{...original._meta??{},[UPDATE_ADVISORY_META_KEY]:advisory}}}catch{return original}}function createUpdateAdvisoryListHandler(inner,getAdvisory,onListServed){return async(request,extra)=>{let result=await inner(request,extra);try{return onListServed?.(),decorateListResultWithUpdateAdvisory(result,getAdvisory())}catch{return result}}}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_bridge_api_client();import path24 from"node:path";var CONDUCT_EPIC_CHECKPOINT_VERSION=1,CONDUCT_EPIC_DEFAULT_SOFT_SECONDS=3600,CONDUCT_EPIC_DEFAULT_HARD_SECONDS=10800,CONDUCT_EPIC_MAX_JOURNAL_LINES=50,CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING=6,CONDUCT_EPIC_TICKET_STATUSES=["pending","in_progress","merged","done","needs_human"],atomicWriteCounter=0;function resolveConductEpicStateDirectory(repoName,deps){let xdg=deps.env.XDG_CONFIG_HOME,bridgeDir=xdg&&xdg.trim().length>0?path24.join(xdg,"bridge"):path24.join(deps.homedir(),".config","bridge");return path24.join(bridgeDir,"conduct",repoName)}function resolveConductEpicCheckpointPath(repoName,epicKey,deps){return path24.join(resolveConductEpicStateDirectory(repoName,deps),`${epicKey}.json`)}function resolveConductEpicLockPath(checkpointPath){let dir=path24.dirname(checkpointPath),base=path24.basename(checkpointPath).replace(/\.json$/,"");return path24.join(dir,`${base}.lock`)}function resolveConductEpicBackupPath(checkpointPath){return`${checkpointPath}.prev`}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeConductEpicCheckpoint(value){return!isRecord(value)||!Array.isArray(value.tickets)?value:{...value,index_scope_id:"index_scope_id"in value?value.index_scope_id:null,index_scope_lease_epoch:"index_scope_lease_epoch"in value?value.index_scope_lease_epoch:null,tickets:value.tickets.map(ticket=>{if(!isRecord(ticket))return ticket;let normalized={...ticket};return"parse_requested_at"in normalized||(normalized.parse_requested_at=null),"parse_requested_for_sha"in normalized||(normalized.parse_requested_for_sha=null),"review_verdictless_observations"in normalized||(normalized.review_verdictless_observations=0),"review_verdictless_for_sha"in normalized||(normalized.review_verdictless_for_sha=null),normalized})}}function isText(value){return typeof value=="string"&&value.trim().length>0}function isCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}function isNullableText(value){return value===null||isText(value)}function fail(error){return{ok:!1,error}}function validateCounters(value,where){if(!isRecord(value))return fail(`${where}.counters must be an object`);for(let key of["sessions_spawned","plan_generations_observed","merge_attempts"])if(!isCount(value[key]))return fail(`${where}.counters.${key} must be a non-negative integer`);return Object.keys(value).length!==3?fail(`${where}.counters carries unexpected fields`):{ok:!0}}function validateTicket(value,index){let where=`tickets[${index}]`;if(!isRecord(value))return fail(`${where} must be an object`);if(!isText(value.key))return fail(`${where}.key must be a non-empty string`);if(typeof value.status!="string"||!CONDUCT_EPIC_TICKET_STATUSES.includes(value.status))return fail(`${where}.status must be one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`);if(!isNullableText(value.branch))return fail(`${where}.branch must be a non-empty string or null`);if(value.pr_number!==null&&!(isCount(value.pr_number)&&value.pr_number>0))return fail(`${where}.pr_number must be a positive integer or null`);if(!isNullableText(value.last_seen_head))return fail(`${where}.last_seen_head must be a non-empty string or null`);if(!isNullableText(value.last_state_change_at))return fail(`${where}.last_state_change_at must be a timestamp string or null`);if(!isNullableText(value.spawned_at))return fail(`${where}.spawned_at must be a timestamp string or null`);if(!isCount(value.respawns))return fail(`${where}.respawns must be a non-negative integer`);if(!isCount(value.conflict_attempts))return fail(`${where}.conflict_attempts must be a non-negative integer`);let counters=validateCounters(value.counters,where);if(!counters.ok)return counters;for(let key of["parse_requested_at","parse_requested_for_sha"])if(!isNullableText(value[key]))return fail(`${where}.${key} must be a non-empty string or null`);return isCount(value.review_verdictless_observations)?isNullableText(value.review_verdictless_for_sha)?!Array.isArray(value.journal)||value.journal.some(line=>typeof line!="string")?fail(`${where}.journal must be an array of strings`):value.journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?fail(`${where}.journal must hold at most ${CONDUCT_EPIC_MAX_JOURNAL_LINES} entries`):{ok:!0}:fail(`${where}.review_verdictless_for_sha must be a non-empty string or null`):fail(`${where}.review_verdictless_observations must be a non-negative integer`)}function validateCiLastPoll(value){return value===null?{ok:!0}:isRecord(value)?isText(value.head_sha)?!Array.isArray(value.required)||value.required.some(n=>typeof n!="string")?fail("ci_last_poll.required must be an array of strings"):typeof value.results_fingerprint!="string"?fail("ci_last_poll.results_fingerprint must be a string"):isText(value.at)?{ok:!0}:fail("ci_last_poll.at must be a timestamp string"):fail("ci_last_poll.head_sha must be a non-empty string"):fail("ci_last_poll must be an object or null")}function validateNeedsHuman(value){if(value===null)return{ok:!0};if(!isRecord(value))return fail("needs_human must be an object or null");for(let key of["reason","evidence","at"])if(typeof value[key]!="string")return fail(`needs_human.${key} must be a string`);return{ok:!0}}function validateLock(value){return isRecord(value)?isCount(value.owner_pid)?typeof value.host!="string"?fail("lock.host must be a string"):typeof value.acquired_at!="string"?fail("lock.acquired_at must be a string"):Object.keys(value).length!==3?fail("lock carries unexpected fields"):{ok:!0}:fail("lock.owner_pid must be a non-negative integer"):fail("lock must be an object")}function validateConductEpicCheckpoint(candidate){let value=normalizeConductEpicCheckpoint(candidate);if(!isRecord(value))return fail("checkpoint must be a JSON object");if(value.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return fail(`checkpoint version must be ${CONDUCT_EPIC_CHECKPOINT_VERSION}`);for(let key of["epic_key","repo_name","epic_branch","base_branch_original"])if(!isText(value[key]))return fail(`${key} must be a non-empty string`);for(let key of["created_at","updated_at"])if(!isText(value[key]))return fail(`${key} must be a timestamp string`);if(!isNullableText(value.index_scope_id))return fail("index_scope_id must be a non-empty string or null");if(value.index_scope_lease_epoch!==null&&(typeof value.index_scope_lease_epoch!="number"||!Number.isInteger(value.index_scope_lease_epoch)||value.index_scope_lease_epoch<0))return fail("index_scope_lease_epoch must be a non-negative integer or null");let deadlines=value.deadlines;if(!isRecord(deadlines))return fail("deadlines must be an object");if(!isCount(deadlines.soft_seconds))return fail("deadlines.soft_seconds must be a non-negative integer");if(!isCount(deadlines.hard_seconds))return fail("deadlines.hard_seconds must be a non-negative integer");if(!Array.isArray(value.tickets)||value.tickets.length===0)return fail("tickets must be a non-empty array");let seen=new Set;for(let i=0;i<value.tickets.length;i+=1){let result=validateTicket(value.tickets[i],i);if(!result.ok)return result;let key=value.tickets[i].key;if(seen.has(key))return fail(`tickets contains a duplicate key: ${key}`);seen.add(key)}let counters=value.counters;if(!isRecord(counters))return fail("counters must be an object");if(!isCount(counters.iterations))return fail("counters.iterations must be a non-negative integer");if(!isCount(counters.merges))return fail("counters.merges must be a non-negative integer");let needsHuman=validateNeedsHuman(value.needs_human);if(!needsHuman.ok)return needsHuman;let ciLastPoll=validateCiLastPoll(value.ci_last_poll);if(!ciLastPoll.ok)return ciLastPoll;let lock=validateLock(value.lock);return lock.ok?{ok:!0}:lock}function errorCode(err){let code=err?.code;return typeof code=="string"?code:void 0}async function readConductEpicCheckpoint(checkpointPath,fs7){let raw;try{raw=await fs7.readFile(checkpointPath)}catch(err){return errorCode(err)==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:`checkpoint at ${checkpointPath} could not be read`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"corrupt",error:`checkpoint at ${checkpointPath} is not valid JSON`}}if(isRecord(parsed)&&parsed.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return{kind:"unsupported-version",error:`checkpoint at ${checkpointPath} has version ${JSON.stringify(parsed.version)}; this CLI only understands version ${CONDUCT_EPIC_CHECKPOINT_VERSION}`};let validation=validateConductEpicCheckpoint(parsed);return validation.ok?{kind:"ok",checkpoint:normalizeConductEpicCheckpoint(parsed)}:{kind:"invalid",error:`checkpoint at ${checkpointPath} is invalid: ${validation.error}`}}function createInitialConductEpicCheckpoint(input){return{version:CONDUCT_EPIC_CHECKPOINT_VERSION,epic_key:input.epicKey,repo_name:input.repoName,epic_branch:input.epicBranch,base_branch_original:input.baseBranchOriginal,index_scope_id:input.indexScopeId??null,index_scope_lease_epoch:input.indexScopeLeaseEpoch??null,created_at:input.now,updated_at:input.now,deadlines:input.deadlines??{soft_seconds:CONDUCT_EPIC_DEFAULT_SOFT_SECONDS,hard_seconds:CONDUCT_EPIC_DEFAULT_HARD_SECONDS},tickets:input.ticketKeys.map(key=>({key,status:"pending",branch:null,pr_number:null,last_seen_head:null,last_state_change_at:null,spawned_at:null,respawns:0,conflict_attempts:0,counters:{sessions_spawned:0,plan_generations_observed:0,merge_attempts:0},parse_requested_at:null,parse_requested_for_sha:null,review_verdictless_observations:0,review_verdictless_for_sha:null,journal:[]})),counters:{iterations:0,merges:0},needs_human:null,ci_last_poll:null,lock:input.lock}}function appendTicketJournal(ticket,line){let journal=[...ticket.journal,line];return{...ticket,journal:journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?journal.slice(journal.length-CONDUCT_EPIC_MAX_JOURNAL_LINES):journal}}async function writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,fs7,options={}){let validation=validateConductEpicCheckpoint(checkpoint);if(!validation.ok)return{ok:!1,error:`refusing to write an invalid checkpoint: ${validation.error}`};let dir=path24.dirname(checkpointPath);try{await fs7.mkdir(dir,{recursive:!0}),options.skipChmod||await fs7.chmod(dir,448)}catch{return{ok:!1,error:`could not prepare the checkpoint directory ${dir}`}}let content=`${JSON.stringify(checkpoint,null,2)}
7292
7292
  `,tmpPath=path24.join(dir,`${path24.basename(checkpointPath)}.tmp-${process.pid}-${atomicWriteCounter++}`);try{await fs7.writeFile(tmpPath,content,{mode:384}),options.skipChmod||await fs7.chmod(tmpPath,384);let previous=null;try{previous=await fs7.readFile(checkpointPath)}catch{previous=null}if(previous!==null){let previousIsValid=!1;try{previousIsValid=validateConductEpicCheckpoint(JSON.parse(previous)).ok}catch{previousIsValid=!1}previousIsValid&&(await fs7.writeFile(resolveConductEpicBackupPath(checkpointPath),previous,{mode:384}),options.skipChmod||await fs7.chmod(resolveConductEpicBackupPath(checkpointPath),384))}return await fs7.rename(tmpPath,checkpointPath),{ok:!0,path:checkpointPath}}catch{return await fs7.unlink(tmpPath).catch(()=>{}),{ok:!1,error:`could not write the checkpoint at ${checkpointPath}`}}}function errorCode2(err){let code=err?.code;return typeof code=="string"?code:void 0}function parseConductEpicLock(raw){if(typeof raw!="string"||raw.length===0||raw.length>4096)return null;let value;try{value=JSON.parse(raw)}catch{return null}if(typeof value!="object"||value===null||Array.isArray(value))return null;let record=value,{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}=record;return version!==1||typeof ownerPid!="number"||!Number.isInteger(ownerPid)||ownerPid<=0||typeof host!="string"||host.length===0||typeof acquiredAt!="string"||acquiredAt.length===0?null:{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}}function isConductEpicLockOwnerAlive(pid){if(!Number.isInteger(pid)||pid<=0)return!0;try{return process.kill(pid,0),!0}catch(err){return errorCode2(err)!=="ESRCH"}}function serializeLock(request){let owner={version:1,owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt};return JSON.stringify(owner)}async function inspectConductEpicLock(lockPath,request,seams={}){let read=seams.readFile??defaultReadFile,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,raw;try{raw=await read(lockPath)}catch(err){return errorCode2(err)==="ENOENT"?{kind:"missing"}:{kind:"unknown",reason:"the lock file exists but could not be read"}}let owner=parseConductEpicLock(raw);return owner===null?{kind:"unknown",reason:"the lock file is malformed, oversized, or written by an unsupported version"}:owner.host!==request.host?{kind:"remote-host",owner}:owner.owner_pid===request.ownerPid?{kind:"owned",owner}:isAlive(owner.owner_pid)?{kind:"live-foreign",owner}:{kind:"dead-local",owner}}async function defaultReadFile(filePath){let{readFile:readFile19}=await import("node:fs/promises");return readFile19(filePath,"utf-8")}async function defaultWriteFileExclusive(filePath,data){let{open:open7}=await import("node:fs/promises"),handle=await open7(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}}async function defaultRemoveFile(filePath){let{rm:rm8}=await import("node:fs/promises");await rm8(filePath,{force:!0})}async function defaultMkdir(dirPath,options){let{mkdir:mkdir16}=await import("node:fs/promises");return mkdir16(dirPath,options)}async function acquireConductEpicLock(lockPath,request,seams={}){let writeExclusive=seams.writeFileExclusive??defaultWriteFileExclusive,read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,makeDir=seams.mkdir??defaultMkdir,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,refused=(reason,owner)=>({acquired:!1,lockPath,reason,owner}),{dirname:dirname2}=await import("node:path");try{await makeDir(dirname2(lockPath),{recursive:!0})}catch{}let payload=serializeLock(request);for(let attempt=0;attempt<3;attempt+=1){try{await writeExclusive(lockPath,payload);let owner2=parseConductEpicLock(payload);return owner2===null?refused("the lock payload could not be re-parsed",null):{acquired:!0,lockPath,owner:owner2,release:()=>releaseConductEpicLock(lockPath,owner2,{readFile:read,removeFile:remove})}}catch(err){if(errorCode2(err)!=="EEXIST")return refused("the lock file could not be created",null)}let raw;try{raw=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be read",null)}let owner=parseConductEpicLock(raw);if(owner===null)return refused("the lock file is malformed, oversized, or written by an unsupported version",null);if(owner.host!==request.host)return refused(`the lock is held by host ${owner.host}; liveness cannot be checked from here`,owner);if(owner.owner_pid===request.ownerPid)return{acquired:!0,lockPath,owner,release:()=>releaseConductEpicLock(lockPath,owner,{readFile:read,removeFile:remove})};if(isAlive(owner.owner_pid))return refused(`the lock is held by live process ${owner.owner_pid} on ${owner.host}`,owner);let confirmation;try{confirmation=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be re-read",null)}if(confirmation===raw)try{await remove(lockPath)}catch{return refused("a stale lock could not be removed",owner)}}return refused("the lock could not be acquired after 3 attempts",null)}async function releaseConductEpicLock(lockPath,owner,seams={}){let read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,raw;try{raw=await read(lockPath)}catch{return}let current=parseConductEpicLock(raw);if(current!==null&&!(current.owner_pid!==owner.owner_pid||current.host!==owner.host||current.acquired_at!==owner.acquired_at))try{await remove(lockPath)}catch{}}init_start_tickets_repo();init_launcher_config_inspection();init_mcp_identity();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();init_version_generated();init_start_tickets_prereqs();init_start_tickets_repo();import os9 from"node:os";init_agent_registry();var DEFAULT_EXECUTOR_AGENT_ID=DEFAULT_AGENT_NAME;function resolveExecutorAgentId(agentId){if(typeof agentId!="string")return DEFAULT_EXECUTOR_AGENT_ID;let trimmed=agentId.trim();return trimmed.length>0?trimmed:DEFAULT_EXECUTOR_AGENT_ID}init_mcp_server_invocation();import{execFile as execFile3,spawn}from"node:child_process";import{existsSync as existsSync3}from"node:fs";import{open,readFile as readFile6,writeFile as writeFile5,appendFile,mkdir as mkdir5,mkdtemp as mkdtemp3,chmod,rm as rm4,readdir as readdir3,lstat,stat as stat4,statfs}from"node:fs/promises";import os6 from"node:os";import{promisify}from"node:util";var execFileAsync=promisify(execFile3),MAX_COMMAND_BUFFER=10*1024*1024,EXECUTOR_HTTP_TIMEOUT_MS=3e4;function createDefaultExecutorDeps(){return{async runCommand(file,args,options){try{let{stdout,stderr}=await execFileAsync(file,args,{cwd:options?.cwd,timeout:options?.timeoutMs,maxBuffer:MAX_COMMAND_BUFFER,encoding:"utf8",shell:!1});return{stdout:stdout??"",stderr:stderr??"",exitCode:0}}catch(err){let e=err,exitCode=typeof e.code=="number"?e.code:1;return{stdout:e.stdout??"",stderr:e.stderr??"",exitCode}}},spawnProcess(file,args,options){let child=spawn(file,args,{cwd:options.cwd,env:options.env,stdio:["ignore","pipe","pipe"],shell:!1});return child.stdout?.setEncoding("utf8"),child.stderr?.setEncoding("utf8"),{pid:child.pid,stdout:child.stdout??null,stderr:child.stderr??null,wait(){return new Promise(resolve2=>{child.on("close",(code,signal)=>resolve2({exitCode:code,signal})),child.on("error",()=>resolve2({exitCode:null,signal:null}))})},kill(signal){try{child.kill(signal)}catch{}}}},readFile:filePath=>readFile6(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile5(filePath,data,"utf-8"),appendFile:(filePath,data)=>appendFile(filePath,data,"utf-8"),mkdir:(dirPath,opts)=>mkdir5(dirPath,opts),writeFileExclusive:async(filePath,data)=>{let handle=await open(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},removeFile:filePath=>rm4(filePath,{force:!0}),stat:filePath=>stat4(filePath).then(s=>({mode:s.mode})),statMtimeMs:filePath=>stat4(filePath).then(s=>s.mtimeMs).catch(()=>null),statfs:async path53=>{let s=await statfs(path53);return{bavail:Number(s.bavail),bsize:Number(s.bsize)}},mkdtemp:prefix=>mkdtemp3(prefix),chmod:(path53,mode)=>chmod(path53,mode),rmRecursive:path53=>rm4(path53,{recursive:!0,force:!0}),readdir:dirPath=>readdir3(dirPath),lstatPath:path53=>lstat(path53).then(s=>({isDirectory:s.isDirectory(),isSymbolicLink:s.isSymbolicLink(),mtimeMs:s.mtimeMs})).catch(()=>null),tmpdir:()=>os6.tmpdir(),sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),setTimer:(cb,ms)=>setTimeout(cb,ms),clearTimer:handle=>clearTimeout(handle),env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os6.homedir,async fetch(url,init){let g=globalThis,controller=new AbortController,timer=setTimeout(()=>controller.abort(),EXECUTOR_HTTP_TIMEOUT_MS);try{let res=await g.fetch(url,{method:init.method,headers:init.headers,body:init.body,signal:controller.signal}),body=await res.text();return{status:res.status,text:async()=>body}}finally{clearTimeout(timer)}},log:message=>console.error(message),errorLog:message=>console.error(message),mcpServerInvocation:resolveMcpShimInvocationForRuntime({moduleUrl:import.meta.url,nodeExecutable:"node",argv1:process.argv[1],fileExists:existsSync3})}}init_credential_store();function normalizeBaseUrlCandidate(raw){if(typeof raw!="string")return null;let normalized=raw.trim().replace(/\/+$/,"");return normalized.length>0?normalized:null}var REJECTION_TEXT={empty:"value is empty","not-absolute-http-url":"value is not an absolute URL","unsupported-scheme":"only http: and https: URLs are supported","missing-hostname":"URL has no hostname","embedded-credentials":"URL must not embed a username or password"};function describeBaseUrlRejection(flag,reason){return`Invalid ${flag}: ${REJECTION_TEXT[reason]}`}function validateHttpBaseUrl(raw){if(typeof raw!="string"||raw.trim().length===0)return{ok:!1,reason:"empty"};let parsed;try{parsed=new URL(raw.trim())}catch{return{ok:!1,reason:"not-absolute-http-url"}}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")return{ok:!1,reason:"unsupported-scheme"};if(parsed.hostname.length===0)return{ok:!1,reason:"missing-hostname"};if(parsed.username.length>0||parsed.password.length>0)return{ok:!1,reason:"embedded-credentials"};let normalized=normalizeBaseUrlCandidate(raw);return normalized===null?{ok:!1,reason:"empty"}:{ok:!0,baseUrl:normalized}}var DEFAULT_BAPI_BASE_URL="https://bridgegpt-api.com",EXECUTOR_BASE_URL_REQUIRED_MESSAGE=`no Bridge API base URL configured for the executor: pass --base-url <url> or set BAPI_BASE_URL (production is ${DEFAULT_BAPI_BASE_URL}). The executor never defaults to production.`;function resolveBaseUrl(env,explicitBaseUrl){let fromFlag=normalizeBaseUrlCandidate(explicitBaseUrl);if(fromFlag!==null)return{ok:!0,baseUrl:fromFlag};let fromEnv=normalizeBaseUrlCandidate(env?.BAPI_BASE_URL);return fromEnv!==null?{ok:!0,baseUrl:fromEnv}:{ok:!1}}async function resolveExecutorApiAccess(repoName,deps,explicitBaseUrl){let trimmed=typeof repoName=="string"?repoName.trim():"";if(trimmed.length===0)return{ok:!1,error:"invalid repo name: repo name is required"};let url=resolveBaseUrl(deps.env,explicitBaseUrl);if(!url.ok)return{ok:!1,error:EXECUTOR_BASE_URL_REQUIRED_MESSAGE};let storeDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat},result=await resolveBapiCredentials(trimmed,storeDeps);return result.ok?{ok:!0,apiKey:result.credentials.apiKey,baseUrl:url.baseUrl}:{ok:!1,error:`could not resolve Bridge API credentials for repo '${trimmed}' (${result.kind})`}}async function resolveAllExecutorApiAccess(repos,deps,explicitBaseUrl){let out=[];for(let repo of repos){let access2=await resolveExecutorApiAccess(repo,deps,explicitBaseUrl);access2.ok?out.push({ok:!0,repoName:repo,apiKey:access2.apiKey,baseUrl:access2.baseUrl}):out.push({ok:!1,repoName:repo,error:access2.error})}return out}function boundedDetail(text4){let trimmed=(text4??"").trim();return trimmed.length>200?`${trimmed.slice(0,200)}\u2026`:trimmed}function isOkBody(bodyText){try{let parsed=JSON.parse(bodyText);return!!parsed&&typeof parsed=="object"&&parsed.ok===!0}catch{return!1}}var HEARTBEAT_STOP_FIELD="stop_requested";function parseStopRequested(bodyText){try{let parsed=JSON.parse(bodyText);return!parsed||typeof parsed!="object"?!1:parsed[HEARTBEAT_STOP_FIELD]===!0}catch{return!1}}var RECONCILER_LIVENESS_HEADER="X-BAPI-Reconciler-Liveness",RECONCILER_LIVENESS_VALUES=["fresh","stale","never_seen","unknown"];function parseReconcilerLivenessHeader(headers){let raw=null;try{raw=headers.get(RECONCILER_LIVENESS_HEADER)}catch{return"unknown"}let value=(raw??"").trim().toLowerCase();return RECONCILER_LIVENESS_VALUES.includes(value)?value:"unknown"}function mapMutationResponse(status,bodyText,allowInvalidResult){return status===409?"stale_claim":status===503?"retry_later":status===422&&allowInvalidResult?"invalid_job_result":status>=200&&status<300&&isOkBody(bodyText)?"updated":"fatal_http_error"}function createExecutorHttpClient(config){let base=config.baseUrl.replace(/\/+$/,"")+"/executor/jobs";function keyForRepo(repoName){return config.apiKeyByRepo?.[repoName]??config.apiKey}function headersForRepo(repoName){return{"Content-Type":"application/json","X-API-Key":keyForRepo(repoName),"X-Bridge-MCP-Version":config.mcpVersion}}async function post(pathSuffix,body,repoName){let res=await config.fetch(`${base}${pathSuffix}`,{method:"POST",headers:headersForRepo(repoName),body:JSON.stringify(body)}),text4=await res.text();return{status:res.status,text:text4,headers:res.headers}}async function mutate(pathSuffix,body,allowInvalidResult,repoName){try{let{status,text:text4}=await post(pathSuffix,body,repoName);return mapMutationResponse(status,text4,allowInvalidResult)}catch{return"fatal_http_error"}}return{async claim(manifest){let status,text4,reconcilerLiveness="unknown";try{let res=await post("/claim",manifest,manifest.repo_name);status=res.status,text4=res.text,res.headers&&(reconcilerLiveness=parseReconcilerLivenessHeader(res.headers))}catch(err){let message=err instanceof Error?err.message:String(err);return{kind:"retryable",error:`claim network error: ${boundedDetail(message)}`}}if(status===204)return{kind:"none",reconcilerLiveness};if(status===200)try{let job=JSON.parse(text4);return!job||typeof job!="object"||typeof job.claim_token!="string"?{kind:"fatal",error:"claim returned a malformed job payload"}:{kind:"claimed",job,reconcilerLiveness}}catch{return{kind:"fatal",error:"claim returned invalid JSON"}}return status===401||status===403||status===422?{kind:"fatal",error:`claim rejected (HTTP ${status}): ${boundedDetail(text4)}`}:{kind:"retryable",error:`claim failed (HTTP ${status}): ${boundedDetail(text4)}`}},async heartbeat(job,payload){let status,text4;try{let res=await post(`/${job.id}/heartbeat`,{repo_name:job.repo_name,claim_token:job.claim_token,local_commit_count:payload.local_commit_count,last_commit_sha:payload.last_commit_sha,telemetry:payload.telemetry},job.repo_name);status=res.status,text4=res.text}catch{return{outcome:"fatal_http_error"}}let result={outcome:mapMutationResponse(status,text4,!1)};return parseStopRequested(text4)&&(result.stop_requested=!0),result},async processHeartbeat(request){try{let{status,text:text4}=await post("/process-heartbeat",request,request.repo_names[0]??"");return status>=200&&status<300&&isOkBody(text4)?"delivered":"failed"}catch{return"failed"}},complete(job,completion){return mutate(`/${job.id}/complete`,{repo_name:job.repo_name,claim_token:job.claim_token,job_type:completion.job_type,exit_code:completion.exit_code,classification:completion.classification,result:completion.result,artifacts:completion.artifacts,local_commit_count:completion.local_commit_count,last_commit_sha:completion.last_commit_sha,telemetry:completion.telemetry},!0,job.repo_name)},fail(job,failure){return mutate(`/${job.id}/fail`,{repo_name:job.repo_name,claim_token:job.claim_token,error_kind:failure.error_kind,error_message:failure.error_message,classification:failure.classification,local_commit_count:failure.local_commit_count,last_commit_sha:failure.last_commit_sha,telemetry:failure.telemetry},!1,job.repo_name)}}}init_types2();var DENY_PROBE_TIMEOUT_ENV_KEY="BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS",DENY_PROBE_TIMEOUT_MIN_MS=1e3,DENY_PROBE_TIMEOUT_MAX_MS=6e5;function resolveDenyProbeTimeoutMs(env={}){let raw=env[DENY_PROBE_TIMEOUT_ENV_KEY];if(typeof raw!="string"||raw.trim().length===0)return DEFAULT_PROBE_TIMEOUT_MS;let parsed=Number(raw.trim());return!Number.isFinite(parsed)||!Number.isInteger(parsed)?DEFAULT_PROBE_TIMEOUT_MS:parsed<DENY_PROBE_TIMEOUT_MIN_MS||parsed>DENY_PROBE_TIMEOUT_MAX_MS?DEFAULT_PROBE_TIMEOUT_MS:parsed}function isCacheableDenyResult(result){return result.enforced===!0&&result.layer!=="none"}function createDenyProbeCache(){let cached;return{get:()=>cached,set:result=>{isCacheableDenyResult(result)&&(cached=result)},reset:()=>{cached=void 0}}}function refusedPreflight(fatalFindings,warnings,adapter){return{ok:!1,fatalFindings,warnings,agentVersion:"",wtVersion:"",diskFreeGb:null,ghAuthed:!1,denyEnforced:!1,adapter}}async function collectExecutorPreflight(options,deps,seams={}){let fatalFindings=[],warnings=[],agentId=resolveExecutorAgentId(options.agentId),resolution=(seams.resolveAdapter??(id=>resolveExecutorAgentAdapter(id,{claude:{readFile:deps.readFile}})))(agentId);if(!resolution.supported)return refusedPreflight([resolution.message],warnings,inspectionForFailedResolution(agentId,resolution));let adapter=resolution.adapter,adapterInspection=await inspectExecutorAgentAdapter(adapter,deps,{probeExecutable:!0}),platformSupport=adapterInspection.inspection.platform;if(platformSupport&&!platformSupport.supported)return refusedPreflight([platformSupport.message],warnings,adapterInspection.inspection);let executableReport=adapterInspection.inspection.executable,agentVersion=executableReport?.version??"";if(!executableReport||!executableReport.probed||executableReport.failure!==null){let command=executableReport?.command??agentId;fatalFindings.push(`${command} (headless agent) is not available on PATH`)}adapterInspection.inspection.lifecycleKind===null&&fatalFindings.push("executor adapter declares no lifecycle behavior; refusing to claim (an explicit no-op lifecycle is required, absence is not treated as no-op)");let authCapability=adapter.auth;if(authCapability?.supported===!0)for(let carrier of authCapability.value.managedAuthCarriers)try{let validation=await carrier.validate(deps.env);validation.valid||fatalFindings.push(`declared auth carrier '${validation.carrierId}' failed validation: ${validation.detail}`)}catch{fatalFindings.push(`declared auth carrier '${carrier.carrierId}' could not be validated; refusing to claim`)}let normalizeVersion=(stdout,stderr)=>(stdout||stderr||"").trim().slice(0,200),wtVersion="";try{let wt=await deps.runCommand(options.worktrunkBinary,["--version"]);wt.exitCode===0?wtVersion=normalizeVersion(wt.stdout,wt.stderr):fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' is not available`)}catch{fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' could not be probed`)}try{(await deps.runCommand("git",["--version"])).exitCode!==0&&fatalFindings.push("git is not available on PATH")}catch{fatalFindings.push("git could not be probed")}let baseUrlResult=resolveBaseUrl(deps.env,options.baseUrl);if(!baseUrlResult.ok)fatalFindings.push(EXECUTOR_BASE_URL_REQUIRED_MESSAGE);else{let access2=await resolveAllExecutorApiAccess(options.repos,deps,baseUrlResult.baseUrl);for(let a of access2)a.ok||fatalFindings.push(`credential resolution failed for repo '${a.repoName}'`)}let diskFreeGb=null;if(!deps.statfs)fatalFindings.push("disk space could not be verified (statfs unavailable)");else try{let fs7=await deps.statfs(deps.homedir());diskFreeGb=fs7.bavail*fs7.bsize/1024**3}catch{fatalFindings.push("disk space could not be verified (statfs failed)")}let ghAuthed=!1;try{(await deps.runCommand("gh",["--version"])).exitCode===0&&(ghAuthed=(await deps.runCommand("gh",["auth","status"])).exitCode===0)}catch{ghAuthed=!1}ghAuthed||warnings.push("local `gh` is unavailable/unauthenticated; local merges would be degraded");let denyEnforced=!1;try{let cachedDeny=(seams.bypassDenyProbeCache===!0?void 0:seams.denyProbeCache)?.get(),deny;if(cachedDeny!==void 0)deny=cachedDeny;else if(seams.runDenyPreflight)deny=await seams.runDenyPreflight({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)}),seams.denyProbeCache?.set(deny);else{let denyCapability=adapter.denyEnforcement;if(denyCapability?.supported!==!0)throw new Error("adapter declares no deny-enforcement capability");let verdict=await denyCapability.value.probeEnforcement({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)});deny={enforced:verdict.enforced,layer:verdict.layer,degraded:verdict.degraded,warnings:verdict.warnings,detail:verdict.detail},seams.denyProbeCache?.set(deny)}denyEnforced=deny.enforced===!0,deny.enforced?deny.layer==="none"&&fatalFindings.push("deny-layer enforcement layer is 'none'; cannot verify \u2014 refusing to claim"):fatalFindings.push(`deny-layer enforcement not verified (enforced=false, layer=${deny.layer}); refusing to claim`)}catch{fatalFindings.push("deny-layer enforcement probe failed; refusing to claim")}let advisory=adapter.advisoryMcpInspection;if(advisory?.supported===!0)try{let result=await advisory.value.inspect({platform:deps.platform,homedir:deps.homedir(),cwd:deps.cwd});warnings.push(...result.warnings)}catch{warnings.push("advisory worker-MCP inspection did not complete; could not verify worker MCP integrity")}return warnings.push(...adapterInspection.inspection.warnings),{ok:fatalFindings.length===0,fatalFindings,warnings,agentVersion,wtVersion,diskFreeGb,ghAuthed,denyEnforced,adapter:adapterInspection.inspection}}function buildClaimManifest(report,options,freeSlots){return{repo_name:options.repoName,executor_id:options.executorId,repos:options.repos,agent_version:report.agentVersion,wt_version:report.wtVersion,max_concurrent:options.maxConcurrent,free_slots:freeSlots,disk_free_gb:report.diskFreeGb??0,...options.epicRunIds!==void 0?{epic_run_ids:options.epicRunIds}:{}}}import{rm as rm6}from"node:fs/promises";import os7 from"node:os";function normalizeHeartbeatResult(response){return typeof response=="string"?{outcome:response}:response}var PUSH_DETECT_INTERVAL_MS=5e3;async function runHeartbeatLoop(params){let{job,httpClient,options,deps,ownership,proc,observation}=params,pushDetectIntervalMs=params.pushDetectIntervalMs??PUSH_DETECT_INTERVAL_MS,remoteMarker,remoteBaselineEstablished=!1,sendHeartbeat=async()=>{let git2=await params.collectTelemetry();observation.setGitTelemetry(git2);let residue=observation.snapshot(),result=normalizeHeartbeatResult(await httpClient.heartbeat(job,{local_commit_count:git2.local_commit_count,last_commit_sha:git2.last_commit_sha,telemetry:residue}));return result.stop_requested===!0?(killOwnedProcess(proc,"server_stop"),{kind:"server_stop"}):result.outcome==="updated"?(ownership.lastSuccessfulHeartbeatAt=deps.now(),null):result.outcome==="stale_claim"?(killOwnedProcess(proc,"stale_claim"),ownership.abandoned=!0,ownership.abandonReason="stale_claim",{kind:"abandoned"}):deps.now()-ownership.lastSuccessfulHeartbeatAt>=options.deadmanMs?(killOwnedProcess(proc,"deadman"),ownership.abandoned=!0,ownership.abandonReason="deadman",{kind:"abandoned"}):null},waitIntervalWatchingForPush=async()=>{if(!params.collectRemoteMarker||pushDetectIntervalMs<=0)return await deps.sleep(options.heartbeatIntervalMs),null;let waited=0;for(;waited<options.heartbeatIntervalMs;){let slice=Math.min(pushDetectIntervalMs,options.heartbeatIntervalMs-waited);if(await deps.sleep(slice),waited+=slice,ownership.abandoned||params.isDone())return null;let sample;try{sample=await params.collectRemoteMarker()}catch{sample=void 0}if(sample===void 0)continue;if(!remoteBaselineEstablished){remoteBaselineEstablished=!0,remoteMarker=sample;continue}if(sample===remoteMarker)continue;remoteMarker=sample;let outcome2=await sendHeartbeat();if(outcome2)return outcome2;if(ownership.abandoned||params.isDone())return null}return null},first=!0;for(;!ownership.abandoned&&!params.isDone();){if(first){if(params.collectRemoteMarker)try{let sample=await params.collectRemoteMarker();sample!==void 0&&(remoteMarker=sample),remoteBaselineEstablished=!0}catch{remoteBaselineEstablished=!0}}else{let waitOutcome=await waitIntervalWatchingForPush();if(waitOutcome)return waitOutcome;if(ownership.abandoned||params.isDone())break}first=!1;let outcome2=await sendHeartbeat();if(outcome2)return outcome2}return ownership.abandoned?{kind:"abandoned"}:{kind:"done"}}var MissingVerdictArtifact="MissingVerdictArtifact",WorktreeLostBeforePush="WorktreeLostBeforePush",BranchMismatch="BranchMismatch",WorkerFinalizationMissingRemoteBranchAndPr="WorkerFinalizationMissingRemoteBranchAndPr",WorkerFinalizationPrBaseMismatch="WorkerFinalizationPrBaseMismatch",WorkerFinalizationPrNotAttached="WorkerFinalizationPrNotAttached",WorkerFinalizationSavedButUnfinalized="WorkerFinalizationSavedButUnfinalized";var PreSpawnVerification="ContractError.PreSpawnVerification";var McpSurfaceMismatch="ContractError.McpSurfaceMismatch",WorkerStartupFatal="ContractError.WorkerStartupFatal",RequiredMcpRegistration="ContractError.RequiredMcpRegistration",ClaudeNotAuthenticated="ContractError.ClaudeNotAuthenticated",WorktreeBusy="ContractError.WorktreeBusy",ExecutorAdapterUnavailable="ContractError.ExecutorAdapterUnavailable",StaleArtifactCleanupFailed="ContractError.StaleArtifactCleanupFailed",EMPTY_MCP_SERVER_NAME_MARKER="none",MCP_SURFACE_NAME_LIMIT=20;function formatMcpServerNameList(names){let normalized=normalizeMcpServerNames(names);if(normalized.length===0)return EMPTY_MCP_SERVER_NAME_MARKER;let shown=normalized.slice(0,MCP_SURFACE_NAME_LIMIT),suffix=normalized.length>shown.length?`, +${normalized.length-shown.length} more`:"";return`${shown.join(", ")}${suffix}`}function formatMcpSurfaceMismatch(expected,observed){return`worker MCP surface mismatch: the worker reported loading a different set of MCP servers than its worktree provisions, so it was terminated before doing work. Expected: ${formatMcpServerNameList(expected)}. Observed: ${formatMcpServerNameList(observed)}. An observed set of 'none' means the init event's server list could not be read at all. Check that the installed Claude CLI supports --strict-mcp-config and that nothing adds MCP servers outside the worktree registration.`}var WORKER_STARTUP_FATAL_MESSAGE="the worker exited before reporting its MCP surface: it produced no stream-json output at all and exited non-zero, which is a fatal startup failure rather than a failure during work. The known cause under strict MCP loading is an enterprise managed MCP policy (managed-mcp.json) rejecting --strict-mcp-config; also verify the installed Claude CLI supports the strict MCP flags. The executor does not retry without them, because falling back to unscoped loading would silently restore full inheritance of the operator's MCP servers.",CLAUDE_NOT_AUTHENTICATED_MESSAGE="the worker could not authenticate as the Claude CLI. Run `claude login` on this host; on a headless host with no interactive login available, export CLAUDE_CODE_OAUTH_TOKEN yourself before starting the executor.",WORKTREE_BUSY_MESSAGE="another executor job already holds this ticket's worktree, so no worker was started for this job. Two workers in one working tree corrupt each other's edits, index, and commits, so the second claim is refused rather than allowed in. The refusal is expected when two jobs exist for one ticket; the job that holds the worktree continues normally, and this one is safe to retry once it finishes. See the executor's stderr for the conflicting job id.",ERROR_MESSAGE_MAX_CHARS=300,ExecutorNamedError=class extends Error{errorKind;errorMessage;classification;constructor(errorKind,errorMessage,classification="crashed"){let bounded=String(errorMessage??"").slice(0,ERROR_MESSAGE_MAX_CHARS);super(`${errorKind}: ${bounded}`),this.name="ExecutorNamedError",this.errorKind=errorKind,this.errorMessage=bounded,this.classification=classification}};function isExecutorNamedError(value){return value instanceof ExecutorNamedError}function toExecutorFailure(error){return{error_kind:error.errorKind,error_message:error.errorMessage,classification:error.classification}}function secretFreeErrorMessage(error){let bounded=(error instanceof Error?error.message:String(error)).replace(/\b(token|password|passwd|pwd|secret|api[_-]?key|apikey|authorization|auth|bearer)\b\s*[:=]\s*\S+/gi,"$1=[REDACTED]").replace(/\bgh[posru]_[A-Za-z0-9]{16,}\b/g,"[REDACTED_TOKEN]").replace(/\b[A-Za-z0-9_-]{40,}\b/g,"[REDACTED_TOKEN]").slice(0,ERROR_MESSAGE_MAX_CHARS).trim();return bounded.length>0?`unexpected executor failure: ${bounded}`:"unexpected executor failure"}init_local_merge();init_bridge_api_client();function positiveIntOrNull(value){return typeof value=="number"&&Number.isInteger(value)&&value>0?value:null}function nonNegativeIntOrUndefined(value){return typeof value=="number"&&Number.isInteger(value)&&value>=0?value:void 0}function asString(value){return typeof value=="string"&&value.trim().length>0?value.trim():void 0}function readMergeJobPayloadFields(job){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},prNumber=positiveIntOrNull(payload.pr_number);if(prNumber===null)return{ok:!1,error:"merge job requires a positive integer pr_number"};let expectedHeadSha=asString(job.expected_head_sha)??asString(payload.expected_head_sha);if(!expectedHeadSha)return{ok:!1,error:"merge job requires an expected_head_sha (top-level or payload)"};let method=resolveLocalMergeMethod(payload.method),requiredChecks=Array.isArray(payload.required_checks)?payload.required_checks.filter(c=>typeof c=="string"):[],actionKey=asString(payload.action_key)??`merge:pr-${prNumber}:${expectedHeadSha}`,ciWaitTimeoutMs=nonNegativeIntOrUndefined(payload.ci_wait_timeout_ms),ciWaitPollIntervalMs=nonNegativeIntOrUndefined(payload.ci_wait_poll_interval_ms);return{ok:!0,fields:{prNumber,expectedHeadSha,method,requiredChecks,actionKey,ciWaitTimeoutMs,ciWaitPollIntervalMs}}}function buildConductorMergeRequestForExecutorJob(fields,repoName){return{repo_name:repoName,pr_number:fields.prNumber,expected_head_sha:fields.expectedHeadSha,gate:{name:"merge",required_checks:fields.requiredChecks},action_key:fields.actionKey}}async function buildConductorMergeAccessForExecutorJob(deps){let result=await resolveConductorBridgeApiAccess(deps);return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}}function extractMergeCommitSha(response){for(let ev of response.ledger_events)if(ev.type==="merge.succeeded"){let sha=ev.details?.merge_commit_sha;if(typeof sha=="string"&&sha.length>0)return sha}}function buildMergeJobResult(response,fields){let mergeCommitSha=extractMergeCommitSha(response),result={accepted:!0,pr_number:fields.prNumber,merge_method:fields.method,summary:`merged PR #${fields.prNumber} via ${fields.method}`};return mergeCommitSha&&(result.commit_sha=mergeCommitSha,result.head_sha=mergeCommitSha),result}var MERGE_RETRYABLE="MergeRetryable",MERGE_CONFLICT="MergeConflict",MERGE_FAILED="MergeFailed",RETRYABLE_MERGE_REASONS=new Set(["gh_pr_view_timeout","gh_pr_view_failed","gh_pr_view_unparseable","ci_poll_failed","ci_not_green","gh_merge_timeout","gh_merge_failed","merge_aborted"]);function classifyMergeFailureErrorKind(response){let hasConflictEvent=response.ledger_events.some(ev=>ev.type==="merge.conflict"),reason=asString(response.reason??void 0);return hasConflictEvent||reason==="gh_merge_conflict"?MERGE_CONFLICT:reason&&(reason.startsWith("ci_poll_")||RETRYABLE_MERGE_REASONS.has(reason))?MERGE_RETRYABLE:MERGE_FAILED}function buildMergeJobFailure(response){let reason=asString(response.reason??void 0)??response.status;return{error_kind:classifyMergeFailureErrorKind(response),error_message:secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),classification:"crashed"}}async function runExecutorMergeJob(job,seams){let resolution=readMergeJobPayloadFields(job);if(!resolution.ok)return{ok:!1,failure:{error_kind:"ContractError.MergePayload",error_message:resolution.error,classification:"crashed"}};let fields=resolution.fields,request=buildConductorMergeRequestForExecutorJob(fields,seams.access.repoName),executor=(seams.makeExecutor??makeLocalMergeExecutor)({method:fields.method,ciWaitTimeoutMs:fields.ciWaitTimeoutMs,ciWaitPollIntervalMs:fields.ciWaitPollIntervalMs},seams.localMergeDeps),response;try{response=await executor(seams.access,request)}catch(err){return{ok:!1,failure:{error_kind:"MergeFailed",error_message:secretFreeErrorMessage(err),classification:"crashed"}}}return response.status==="succeeded"?{ok:!0,result:buildMergeJobResult(response,fields)}:{ok:!1,failure:buildMergeJobFailure(response)}}function buildDefaultMergeLocalDeps(deps,signal){return{env:deps.env,signal}}init_mcp_provisioning();init_worktree_core();import path25 from"node:path";function pathApiForExecutorPlatform(platform){return platform==="win32"?path25.win32:path25.posix}function parseGitWorktreePorcelain(porcelain){let entries=[],current=null;for(let rawLine of porcelain.split(`
7293
7293
  `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9).trim()};else if(line.startsWith("branch ")&&current){let ref=line.slice(7).trim();current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}async function listGitWorktrees(runCommand,cwd){let result=await runCommand("git",["worktree","list","--porcelain"],{cwd});return result.exitCode!==0?[]:parseGitWorktreePorcelain(result.stdout)}function findWorktreeByBranch(entries,branch){return entries.find(e=>e.branch===branch)?.path}async function getCurrentBranch(runCommand,worktreePath){return(await runCommand("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:worktreePath})).stdout.trim()}async function assertWorktreeOnBranch(runCommand,worktreePath,expectedBranch){let actual=await getCurrentBranch(runCommand,worktreePath);if(actual!==expectedBranch)throw new ExecutorNamedError(BranchMismatch,`worktree '${worktreePath}' is on '${actual}', expected '${expectedBranch}'`)}async function remoteBranchExists(runCommand,cwd,branch){return(await runCommand("git",["rev-parse","--verify","--quiet",`origin/${branch}`],{cwd})).exitCode===0}var RESUME_WIP_COMMIT_MESSAGE="wip: auto-checkpoint before resume";function asString2(value){return typeof value=="string"&&value.trim().length>0?value.trim():void 0}function readResumePayloadFields(job){let payload=job.payload&&typeof job.payload=="object"?job.payload:{};return{promptTemplate:typeof payload.prompt_template=="string"?payload.prompt_template:null,worktreePath:asString2(payload.worktree_path),worktreeName:asString2(payload.worktree_name),baseBranch:asString2(payload.base_branch),expectedBranch:asString2(payload.expected_branch)}}function resolveResumeExpectedBranch(job,fields){let top=asString2(job.expected_branch);if(top)return top;if(fields.expectedBranch)return fields.expectedBranch;throw new ExecutorNamedError("ResumeMissingExpectedBranch",`resume job ${job.id} has no expected_branch (top-level or payload)`)}function resolveResumePromptTemplate(job,fields){let template=fields.promptTemplate;if(typeof template=="string"&&template.trim().length>0)return template;throw new ExecutorNamedError("ContractError.Prompt",`resume job ${job.id} has no usable payload.prompt_template`)}function resolveResumeBaseBranch(fields,options){return fields.baseBranch??options.baseBranch}async function resolveExistingResumeWorktreePath(fields,expectedBranch,seams){if(fields.worktreePath&&await seams.pathExists(fields.worktreePath))return fields.worktreePath;let entries=await(seams.listWorktrees??listGitWorktrees)(seams.runCommand,seams.cwd);return findWorktreeByBranch(entries,expectedBranch)??null}async function recreateResumeWorktreeFromPushedBranch(job,expectedBranch,seams){if(!await(seams.remoteBranchExists??remoteBranchExists)(seams.runCommand,seams.cwd,expectedBranch))throw new ExecutorNamedError(WorktreeLostBeforePush,`resume worktree for '${expectedBranch}' is gone and origin/${expectedBranch} does not exist`);let key=asString2(job.ticket_key)??expectedBranch,coreDeps={runCommand:seams.runCommand,platform:seams.platform,env:seams.env,cwd:seams.cwd},row=await(seams.createWorktree??createWorktreeForTicket)(coreDeps,key,{[key]:expectedBranch},seams.worktrunkBinary,`origin/${expectedBranch}`,!1);if(row.status==="created"&&typeof row.path=="string")return row.path;throw new ExecutorNamedError(WorktreeLostBeforePush,row.error??`failed to recreate worktree for '${expectedBranch}' from origin`)}async function autoCheckpointDirtyState(worktreePath,seams){return(await seams.runCommand("git",["status","--porcelain"],{cwd:worktreePath})).stdout.trim().length===0?{committed:!1}:(await seams.runCommand("git",["add","-A"],{cwd:worktreePath}),await seams.runCommand("git",["commit","-m",RESUME_WIP_COMMIT_MESSAGE],{cwd:worktreePath}),{committed:!0})}async function readResumeGitLog(worktreePath,baseBranch,seams){return(await seams.runCommand("git",["log","--oneline",`origin/${baseBranch}..HEAD`],{cwd:worktreePath})).stdout}async function readResumeGitDiff(worktreePath,baseBranch,seams){return(await seams.runCommand("git",["diff",`origin/${baseBranch}...HEAD`],{cwd:worktreePath})).stdout}function fillResumePromptPlaceholders(template,gitLog,gitDiff){return template.split("{{GIT_LOG}}").join(gitLog).split("{{GIT_DIFF}}").join(gitDiff)}async function prepareResumeSpawn(job,options,seams){let fields=readResumePayloadFields(job),expectedBranch=resolveResumeExpectedBranch(job,fields),baseBranch=resolveResumeBaseBranch(fields,options),promptTemplate=resolveResumePromptTemplate(job,fields),worktreePath=await resolveExistingResumeWorktreePath(fields,expectedBranch,seams);worktreePath===null&&(worktreePath=await recreateResumeWorktreeFromPushedBranch(job,expectedBranch,seams)),await assertWorktreeOnBranch(seams.runCommand,worktreePath,expectedBranch),await autoCheckpointDirtyState(worktreePath,seams);let gitLog=await readResumeGitLog(worktreePath,baseBranch,seams),gitDiff=await readResumeGitDiff(worktreePath,baseBranch,seams),prompt=fillResumePromptPlaceholders(promptTemplate,gitLog,gitDiff);return{worktreePath,branch:expectedBranch,prompt}}var PROMPT_CONTRACT_ERROR="ContractError.Prompt",SUPPORTED_PROMPT_SPEC_VERSION=1,GIT_LOG_MAX_CHARS=8e3,REVISE_REASONS_NONE="(none \u2014 this is the first review of the ticket spec)",RELATED_CONTEXT_NONE="(no related-ticket context was supplied with this job. This review is PARTIALLY BLIND to the surrounding tickets: do NOT conclude that the ticket has no related tickets, no dependencies, and no already-merged neighbors \u2014 that information was simply not provided.)",GIT_LOG_ARGS=["log","--oneline","--decorate","--max-count=50"],SUPPORTED_PLACEHOLDERS=new Set(["TICKET_SPEC","BASE_BRANCH","WORK_BRANCH","GIT_LOG","REVISE_REASONS","RELATED_CONTEXT","SPEC_CITATION_FRESHNESS_WARNING"]),SPEC_CITATION_FRESHNESS_NONE="(none \u2014 no dependency of this ticket merged after this ticket's specification snapshot was taken.)";function promptContractError(message){return new ExecutorNamedError(PROMPT_CONTRACT_ERROR,message)}function isNonBlankString(value){return typeof value=="string"&&value.trim().length>0}function extractTemplatePlaceholders(template){let seen=new Set,ordered=[],re=/\{\{([A-Z0-9_]+)\}\}/g,match;for(;(match=re.exec(template))!==null;){let name=match[1];seen.has(name)||(seen.add(name),ordered.push(name))}return ordered}function readPromptSpecPayload(value,expectedJobType){if(value===null||typeof value!="object")throw promptContractError(`job_type '${expectedJobType}' has no usable payload.prompt_spec`);let raw=value;if(raw.version!==SUPPORTED_PROMPT_SPEC_VERSION)throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has unsupported version (expected ${SUPPORTED_PROMPT_SPEC_VERSION})`);if(raw.job_type!==expectedJobType)throw promptContractError(`prompt_spec job_type mismatch: claimed job is '${expectedJobType}'`);if(!isNonBlankString(raw.system_prompt))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has a blank system_prompt`);if(!isNonBlankString(raw.user_prompt_template))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has a blank user_prompt_template`);if(!Array.isArray(raw.placeholders)||raw.placeholders.length===0||!raw.placeholders.every(p=>isNonBlankString(p)))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has an invalid placeholders list`);let declared=raw.placeholders,declaredSet=new Set;for(let name of declared){if(declaredSet.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' declares duplicate placeholder '${name}'`);declaredSet.add(name)}let systemPrompt=raw.system_prompt,userPromptTemplate=raw.user_prompt_template,used=new Set([...extractTemplatePlaceholders(systemPrompt),...extractTemplatePlaceholders(userPromptTemplate)]);for(let name of used)if(!declaredSet.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' uses undeclared placeholder '${name}'`);for(let name of declaredSet)if(!used.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' declares unused placeholder '${name}'`);return{version:SUPPORTED_PROMPT_SPEC_VERSION,job_type:expectedJobType,system_prompt:systemPrompt,user_prompt_template:userPromptTemplate,placeholders:declared}}function formatReviseReasons(reasons){if(!Array.isArray(reasons))return REVISE_REASONS_NONE;let bullets=reasons.filter(r=>isNonBlankString(r)).map(r=>`- ${r.trim()}`);return bullets.length>0?bullets.join(`
7294
7294
  `):REVISE_REASONS_NONE}async function resolveGitLog(context){return(await context.runCommand("git",GIT_LOG_ARGS,{cwd:context.worktreePath})).stdout.slice(0,GIT_LOG_MAX_CHARS)}async function buildPromptSpecPlaceholderValues(job,spec,context){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},values={};for(let name of spec.placeholders){if(!SUPPORTED_PLACEHOLDERS.has(name))throw promptContractError(`prompt_spec for job ${job.id} declares unsupported placeholder '${name}'`);switch(name){case"TICKET_SPEC":{if(!isNonBlankString(payload.ticket_spec))throw promptContractError(`job ${job.id} (${job.job_type}) is missing a usable ticket spec for {{TICKET_SPEC}}`);values.TICKET_SPEC=payload.ticket_spec.trim();break}case"BASE_BRANCH":{values.BASE_BRANCH=isNonBlankString(payload.base_branch)?payload.base_branch.trim():context.baseBranch;break}case"WORK_BRANCH":{if(!isNonBlankString(context.workBranch))throw promptContractError(`job ${job.id} (${job.job_type}) has no work branch for {{WORK_BRANCH}}`);values.WORK_BRANCH=context.workBranch.trim();break}case"GIT_LOG":{values.GIT_LOG=await resolveGitLog(context);break}case"REVISE_REASONS":{values.REVISE_REASONS=formatReviseReasons(payload.reasons);break}case"RELATED_CONTEXT":{values.RELATED_CONTEXT=isNonBlankString(payload.related_context)?payload.related_context:RELATED_CONTEXT_NONE;break}case"SPEC_CITATION_FRESHNESS_WARNING":{values.SPEC_CITATION_FRESHNESS_WARNING=isNonBlankString(payload.spec_citation_freshness_warning)?payload.spec_citation_freshness_warning:SPEC_CITATION_FRESHNESS_NONE;break}}}return values}function substitutePlaceholders(template,values){return template.replace(/\{\{([A-Z0-9_]+)\}\}/g,(marker,name)=>Object.prototype.hasOwnProperty.call(values,name)?values[name]:marker)}function buildPromptSpecSystemPrompt(spec,values){return substitutePlaceholders(spec.system_prompt,values)}function buildPromptSpecUserPrompt(spec,values){return substitutePlaceholders(spec.user_prompt_template,values)}async function renderPromptSpecPrompt(job,context){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},spec=readPromptSpecPayload(payload.prompt_spec,job.job_type),values=await buildPromptSpecPlaceholderValues(job,spec,context),systemPrompt=buildPromptSpecSystemPrompt(spec,values),userPrompt=buildPromptSpecUserPrompt(spec,values);return`${systemPrompt}
@@ -7360,9 +7360,9 @@ ${errorDetail(err)}
7360
7360
  No run was created and no automation-start charge occurred. Fix the named field in the policy file and re-run.`),1):(deps.errorLog(`Failed to create the epic run: ${errorDetail(err)}`),1)}if(deps.planeBinding)try{let manifestRead=await deps.planeBinding.readManifest(deps.cwd);if(manifestRead.kind==="valid"){let manifest=manifestRead.manifest;if(manifestHasLiveProcess(manifest,{isAlive:deps.planeBinding.isPlaneProcessAlive})){let bound2=await deps.planeBinding.bind(deps.cwd,manifest.planeId,result.epic_run_id);if(bound2.ok)say(bound2.alreadyBound?`Plane: already bound to run ${result.epic_run_id}`:`Plane: bound to run ${result.epic_run_id} \u2014 \`plane down\` will stop it automatically`);else{let msg=`Could not bind the local plane to run ${result.epic_run_id} (${bound2.message}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}}}}catch(err){let msg=`Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}let cutScopeId=null;if(needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null&&cutRunCommand!==void 0){let cut=await performExactIndexScopeCut(cutProtocolDeps(deps,cutRunCommand),access2,{featureBranch:effectiveFeatureBranch,baseBranch:scopeBaseBranch,candidateCommitSha:cutCommitSha,epicRunId:result.epic_run_id});if(!cut.ok){for(let line of cut.failures)deps.errorLog(line);return(cut.kind==="existing_ref_mismatch"||cut.kind==="confirm_failed")&&deps.errorLog(`Expected origin/${effectiveFeatureBranch} at the canonical indexed commit ${cut.expectedSha??cutCommitSha}`+(cut.observedSha?`, found ${cut.observedSha}.`:".")+" setup-epic never force-pushes: correct the local/origin branch, then re-run setup-epic."),deps.errorLog(`Epic run ${result.epic_run_id} exists but was NOT approved and will not dispatch; re-running setup-epic reuses it and re-drives the cut.`),emitRefusal(deps,opts,result)}cutScopeId=cut.lease.scope_id,say(cut.branchCreated?`Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`:`Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`)}try{let stored=await storeEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version,planBlob:plan,planHash:localHash},deps.fetch);result.plan_stored=!0;let serverHash=stored?.plan_hash;typeof serverHash=="string"&&(result.plan_hash=serverHash),say(`Plan: stored v${plan.plan_version}`)}catch(err){return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Plan v${plan.plan_version} is already stored with a DIFFERENT hash. The stored blob is immutable \u2014 bump plan_version in the sidecar and re-run.
7361
7361
  Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`),1)}effectiveFeatureBranch!==void 0&&say(`Branch: validating origin/${effectiveFeatureBranch} server-side (App read) and preparing the index scope\u2026`);let approval=await approveEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version},deps.fetch).catch(err=>(effectiveFeatureBranch!==void 0&&err instanceof ConductorBridgeApiError&&err.errorCode==="FEATURE_BRANCH_PROVISIONING"?deps.errorLog(`Failed to provision the feature branch '${effectiveFeatureBranch}' \u2014 child-ticket dispatch has NOT started. Correct repository access or the branch configuration, then re-run setup-epic.
7362
7362
  Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`),null));if(approval===null)return 1;if(approval.ok){result.plan_approved=!0,result.plan_hash=approval.plan_hash,result.status="active",say(`Plan: approved v${plan.plan_version}`);let prov=approval.featureBranchProvisioning;prov&&(result.feature_branch_provisioning=prov,prov.status==="created"?say(`Branch: ready on origin \u2014 created '${prov.feature_branch}' from '${prov.source_branch}' at ${prov.source_sha}`):say(`Branch: '${prov.feature_branch}' validated on origin at head ${prov.remote_head_sha} (cut ${prov.source_sha} from '${prov.source_branch}'; the remote ref was not moved or reset)`))}else{if(approval.reason==="multiple_active_runs")return deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`),1;{let msg="A later plan version is already approved \u2014 approval skipped.";result.warnings.push(msg),say(`Plan: [warn] ${msg}`)}}result.plan_hash&&result.plan_hash!==localHash&&result.warnings.push("Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative.");let exitCode=0;if(scopeBearing){let scopeId=cutScopeId??await resolveScopeIdForRun(access2,deps.fetch,result.epic_run_id);if(scopeId===null)result.index_scope={scope_id:null,cut_commit_sha:cutCommitSha,lifecycle_state:null,ready:!1,disposition:"not_polled"},say(`Scope: no index scope is associated with run ${result.epic_run_id}; the reconciler will not dispatch until one is ready. Re-run setup-epic.`),exitCode=1;else{say("Scope: waiting for index scope readiness (bounded)\u2026");let verdict=await pollIndexScopeLifecycle(cutProtocolDeps(deps,cutRunCommand??deps.runCommand??createExecFileRunCommand()),access2,scopeId,{onTransition:(state,status)=>say(describeScopeTransition(state,status))});verdict.kind==="ready"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!0,disposition:"ready"},say("Scope: ready \u2014 the reconciler may dispatch against the epic branch index.")):verdict.kind==="ready_mismatch"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:"ready_watermark_mismatch",recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} reports ready but its indexed commit (${verdict.status.indexed_commit_sha??"none"}) is not the cut commit (${verdict.status.cut_commit_sha??"none"}). Treating the scope as NOT ready; ticket dispatch remains blocked by the reconciler's freshness hold. Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`),exitCode=1):verdict.kind==="failed"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:verdict.reason,recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) \u2014 this is a recorded failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler will not dispatch against a failed scope.`),deps.errorLog(scopeFailureGuidance(verdict.reason,effectiveFeatureBranch??"",result.epic_run_id)),exitCode=1):(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.lastStatus?.cut_commit_sha??cutCommitSha,lifecycle_state:verdict.lastStatus?.lifecycle_state??null,ready:!1,disposition:"timeout"},deps.errorLog(`Index scope ${scopeId} did not become ready within the bounded wait (last observed state: ${verdict.lastState}). The run ${result.epic_run_id} is created and scope preparation continues asynchronously on the server; the reconciler will NOT dispatch tickets until the scope becomes ready. Check GET /jira/index-scope/status?repo_name=${access2.repoName}&scope_id=${scopeId}, or re-run setup-epic to wait again.`),exitCode=1)}}return opts.json?deps.log(JSON.stringify(result,null,2)):(say(""),validateLaneMissing&&say("[warn] This server has no POST /jira/epic-runs/plan/validate \u2014 the plan was NOT validated before the run was created, and the legacy create-then-store order was used. Check that BAPI_BASE_URL points at the intended deployment."),say(`Epic run ${result.epic_run_id} is ${result.status??"unknown"}.`),say("The server-side reconciler will pick it up within ~30s."),say("To execute claimed jobs on this machine, run:"),say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access2.repoName}`)),exitCode}init_base_ref();init_done_gate();init_bridge_api_client();init_pr_discovery();init_start_tickets();init_start_tickets_prereqs();init_start_tickets_repo();import{promises as nodeFs2}from"node:fs";import os15 from"node:os";import path31 from"node:path";init_github_mergeability();init_pr_discovery();var CONDUCT_EPIC_GH_PR_VIEW_FIELDS="number,state,headRefOid,mergeable,mergeStateStatus,baseRefName,updatedAt",CONDUCT_EPIC_GH_PR_LIST_LIMIT=20,CONDUCT_EPIC_PR_STATES=["OPEN","MERGED","CLOSED"];function isRecord3(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function text(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0?null:trimmed}function parseConductEpicPrState(value){if(!isRecord3(value))return null;let rawNumber=value.number,number=typeof rawNumber=="number"&&Number.isInteger(rawNumber)&&rawNumber>0?rawNumber:null,rawState=text(value.state),upper=rawState===null?null:rawState.toUpperCase(),state=CONDUCT_EPIC_PR_STATES.includes(upper??"")?upper:null,mergeability=parseGhPrMergeabilityFields(value);return{number,state,head_sha:text(value.headRefOid),base:text(value.baseRefName),mergeable:mergeability.mergeable,merge_state:mergeability.mergeStateStatus,updated_at:text(value.updatedAt)}}function selectConductEpicPrState(candidates){if(candidates.length===0)return null;let open7=candidates.find(pr=>pr.state==="OPEN");if(open7)return open7;let best=candidates[0];for(let candidate of candidates.slice(1)){let bestAt=Date.parse(best.updated_at??""),candidateAt=Date.parse(candidate.updated_at??""),bestUsable=Number.isFinite(bestAt);Number.isFinite(candidateAt)&&(!bestUsable||candidateAt>bestAt)&&(best=candidate)}return best}async function discoverConductEpicPrState(branch,options={}){let runGh=options.runGh??runGhCommand,result;try{result=await runGh(["pr","list","--head",branch,"--state","all","--json",CONDUCT_EPIC_GH_PR_VIEW_FIELDS,"--limit",String(CONDUCT_EPIC_GH_PR_LIST_LIMIT)],{cwd:options.cwd})}catch{return{kind:"error",reason:"the gh command could not be run"}}if(!result.ok)return{kind:"error",reason:"the gh command failed"};let raw=typeof result.stdout=="string"?result.stdout.trim():"";if(raw.length===0)return{kind:"error",reason:"gh returned no output for a --json query"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"error",reason:"gh returned output that is not valid JSON"}}if(!Array.isArray(parsed))return{kind:"error",reason:"gh returned a PR list in an unexpected shape"};if(parsed.length===0)return{kind:"none"};let records=[];for(let entry of parsed){let pr2=parseConductEpicPrState(entry);if(pr2===null)return{kind:"error",reason:"gh returned a PR record in an unexpected shape"};records.push(pr2)}let pr=selectConductEpicPrState(records);return pr===null?{kind:"error",reason:"gh returned a PR record in an unexpected shape"}:{kind:"ok",pr}}function parseGitWorktreePorcelain2(output){let entries=[],currentPath=null,currentBranch=null,flush=()=>{currentPath!==null&&currentBranch!==null&&entries.push({path:currentPath,branch:currentBranch}),currentPath=null,currentBranch=null};for(let rawLine of String(output??"").split(`
7363
- `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree ")){flush();let value=line.slice(9).trim();currentPath=value.length>0?value:null}else if(line.startsWith("branch ")&&currentPath!==null){let ref=line.slice(7).trim();if(ref.startsWith("refs/heads/")){let name=ref.slice(11);currentBranch=name.length>0?name:null}}}return flush(),entries}function discoverTicketWorktree(entries,ticketKey,storedBranch){if(storedBranch!==null&&storedBranch.length>0){let stored=entries.find(entry=>entry.branch===storedBranch);if(stored)return{branch:stored.branch,path:stored.path}}let canonical=`feature/${ticketKey}`,exact=entries.find(entry=>entry.branch===canonical);if(exact)return{branch:exact.branch,path:exact.path};let prefixed=entries.find(entry=>entry.branch.startsWith(`${canonical}-`));return prefixed?{branch:prefixed.branch,path:prefixed.path}:null}init_start_tickets();init_index_scope_contract();var CONDUCT_EPIC_AGENTS=["claude","cursor-agent"],CONDUCT_EPIC_DEFAULT_AGENT="claude";function resolveConductEpicAgent(agent){if(agent===void 0||agent.trim().length===0)return{ok:!0,agent:CONDUCT_EPIC_DEFAULT_AGENT};let trimmed=agent.trim();return CONDUCT_EPIC_AGENTS.includes(trimmed)?{ok:!0,agent:trimmed}:{ok:!1,error:`Unsupported agent '${trimmed}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`}}function buildConductEpicAgentCommand(input){let resolved=resolveConductEpicAgent(input.agent);return resolved.ok?input.worktreePath.trim().length===0?{ok:!1,error:"A worktree path is required to build the agent command."}:input.platform==="win32"?{ok:!0,command:`${input.indexScope?`$env:${INDEX_SCOPE_ENV_VAR} = ${powershellSquote(input.indexScope)}; `:""}Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`${input.indexScope?`export ${INDEX_SCOPE_ENV_VAR}='${shSquoteInner(input.indexScope)}' && `:""}cd '${shSquoteInner(input.worktreePath)}' && ${resolved.agent} '${shSquoteInner(input.prompt)}'`}:resolved}async function spawnConductEpicAgentTab(input,spawnTab){let built=buildConductEpicAgentCommand(input);if(!built.ok)return built;let result;try{result=await spawnTab(built.command,{key:input.ticketKey,worktreePath:input.worktreePath,title:`conduct-epic ${input.ticketKey}`})}catch{return{ok:!1,error:"The terminal tab could not be opened."}}return result.ok?{ok:!0,command:built.command}:{ok:!1,error:result.error}}init_mcp_identity();init_mcp_identity();var NPM_LATEST_ENDPOINT=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`,NPM_LATEST_TIMEOUT_MS=3e3;async function fetchLatestVersion(deps={}){let doFetch=deps.fetch??globalThis.fetch,timeoutSignal=deps.timeoutSignal??(ms=>AbortSignal.timeout(ms));try{let res=await doFetch(NPM_LATEST_ENDPOINT,{signal:timeoutSignal(NPM_LATEST_TIMEOUT_MS)});if(!res||!res.ok)return null;let data=await res.json();if(!data||typeof data!="object"||Array.isArray(data))return null;let version=data.version;if(typeof version!="string")return null;let trimmed=version.trim();return trimmed.length>0?trimmed:null}catch{return null}}init_index_scope_contract();var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn","recover","retire","reclaim"],TICKET_FIELDS=["status","branch","pr_number","spawned_at","parse_requested_at","parse_requested_for_sha","respawns","conflict_attempts","counters.sessions_spawned","counters.plan_generations_observed","counters.merge_attempts"],TOP_LEVEL_FIELDS=["needs_human","counters.iterations","counters.merges"];function createDefaultConductEpicDeps(){let runCommand=createExecFileRunCommand(),spawner=getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps=createDefaultStartTicketsDeps();return{runCommand,runGh:(args,options)=>runGhCommand(args,options??{}),spawnTab:(shellCommand,context)=>spawner(startTicketsDeps,detectTerminal(void 0,process.env),shellCommand,context),fetchImpl:globalThis.fetch,fs:{mkdir:(dirPath,options)=>nodeFs2.mkdir(dirPath,options),readFile:filePath=>nodeFs2.readFile(filePath,"utf-8"),writeFile:(filePath,data,options)=>nodeFs2.writeFile(filePath,data,{encoding:"utf-8",mode:options?.mode}),writeFileExclusive:async(filePath,data,options)=>{let handle=await nodeFs2.open(filePath,"wx",options?.mode);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(fromPath,toPath)=>nodeFs2.rename(fromPath,toPath),chmod:(filePath,mode)=>nodeFs2.chmod(filePath,mode),unlink:filePath=>nodeFs2.unlink(filePath),stat:filePath=>nodeFs2.stat(filePath)},now:()=>new Date,env:process.env,homedir:os15.homedir,hostname:os15.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveLatestPublishedVersion:()=>fetchLatestVersion({fetch:globalThis.fetch}),resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} conduct-epic <verb> [flags]`,"","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, then create epic/<EPIC> on origin at the commit the"," CANONICAL INDEX covers \u2014 not the base tip \u2014 seed and verify the epic's index"," scope at that commit, repoint the indexed branch, write the checkpoint, and"," take the lock. --base-branch selects the base whose history is fetched and"," recorded; the cut commit is the canonical indexed SHA and is reported"," separately. init fails closed when the repository has no successful parse."," Every preflight failure is printed in one pass; nothing is written unless"," all of them pass. --dry-run prints the validated plan and writes nothing.",""," status <EPIC> [--checkpoint-path <p>] --json"," Print one JSON object describing the in-flight ticket, its worktree, PR,"," CI, review, parse, deadline, and lock state. --json is required. A missing"," checkpoint exits 0 with checkpoint_exists:false. A failed probe leaves its"," sub-object null and is listed in probe_errors; it never fails the command."," `scopes` lists EVERY index scope this repository owns \u2014 expired and"," reclaiming ones included \u2014 so a crashed epic is visible without SQL.",""," checkpoint set <EPIC> --ticket <KEY> [--field <name> <value>]... [--journal <line>]"," [--checkpoint-path <p>]"," Apply ABSOLUTE field values (the caller computes n+1 from status).",` Ticket fields: ${TICKET_FIELDS.join(", ")}.`,` Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`," Repeat --field to write several in ONE atomic mutation."," parse_requested_at / parse_requested_for_sha are ACCEPTED for older"," checkpoints but no longer written: freshness is read from the scope.",""," finish <EPIC> [--checkpoint-path <p>] [--json]"," Restore the server's indexed base branch (idempotent), release the owned"," lock, and print the final summary. A second finish succeeds.","",` spawn <EPIC> --ticket <KEY> --prompt-file <path> [--agent ${CONDUCT_EPIC_AGENTS.join("|")}]`," [--checkpoint-path <p>] [--json]"," Open exactly ONE agent tab in the ticket's worktree running the prompt"," file's contents, then increment counters.sessions_spawned and append a"," journal line. Respawn and conflict budgets are the CALLER's job.",""," recover <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Take a NEW ownership generation for a crashed epic's index scope and"," record the returned fencing epoch locally. Use this instead of SQL when"," `status` shows a scope whose lease expired. Defaults to the epic's own"," scope; --scope targets another one (e.g. when the checkpoint is gone).",""," retire <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Start the scope's retention clock. Deletes NOTHING \u2014 the scope stays"," readable for post-mortem for the whole retention window. Idempotent."," `finish` does this for you; this verb is for retiring without finishing.",""," reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]"," Ask the server to schedule the scope's teardown: three Pinecone"," namespaces, six parse-table slices, three config rows, and a retained"," tombstone. Returns as soon as it is SCHEDULED; watch `status` for the"," result. --override-retention waives only the still-valid-lease and"," unelapsed-retention waits \u2014 an active parse, a held parse lock, a live"," automation run, or a live epic run still refuse. There is no raw"," deletion mode.","","Common:"," -h, --help Show this help","","State lives outside the repository, at ~/.config/bridge/conduct/<repo>/ ","(honoring XDG_CONFIG_HOME): <EPIC>.json, <EPIC>.json.prev, <EPIC>.lock, and","<EPIC>/prompts/. The directory is 0700 and files are 0600.","","Exit codes: 0 on success (including checkpoint_exists:false and an idempotent","finish); non-zero on any failure, with a one-line reason on stderr. With --json,","stdout is exactly one JSON object carrying ok."].join(`
7363
+ `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree ")){flush();let value=line.slice(9).trim();currentPath=value.length>0?value:null}else if(line.startsWith("branch ")&&currentPath!==null){let ref=line.slice(7).trim();if(ref.startsWith("refs/heads/")){let name=ref.slice(11);currentBranch=name.length>0?name:null}}}return flush(),entries}function discoverTicketWorktree(entries,ticketKey,storedBranch){if(storedBranch!==null&&storedBranch.length>0){let stored=entries.find(entry=>entry.branch===storedBranch);if(stored)return{branch:stored.branch,path:stored.path}}let canonical=`feature/${ticketKey}`,exact=entries.find(entry=>entry.branch===canonical);if(exact)return{branch:exact.branch,path:exact.path};let prefixed=entries.find(entry=>entry.branch.startsWith(`${canonical}-`));return prefixed?{branch:prefixed.branch,path:prefixed.path}:null}init_start_tickets();init_index_scope_contract();var CONDUCT_EPIC_AGENTS=["claude","cursor-agent"],CONDUCT_EPIC_DEFAULT_AGENT="claude";function resolveConductEpicAgent(agent){if(agent===void 0||agent.trim().length===0)return{ok:!0,agent:CONDUCT_EPIC_DEFAULT_AGENT};let trimmed=agent.trim();return CONDUCT_EPIC_AGENTS.includes(trimmed)?{ok:!0,agent:trimmed}:{ok:!1,error:`Unsupported agent '${trimmed}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`}}function buildConductEpicAgentCommand(input){let resolved=resolveConductEpicAgent(input.agent);return resolved.ok?input.worktreePath.trim().length===0?{ok:!1,error:"A worktree path is required to build the agent command."}:input.platform==="win32"?{ok:!0,command:`${input.indexScope?`$env:${INDEX_SCOPE_ENV_VAR} = ${powershellSquote(input.indexScope)}; `:""}Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`${input.indexScope?`export ${INDEX_SCOPE_ENV_VAR}='${shSquoteInner(input.indexScope)}' && `:""}cd '${shSquoteInner(input.worktreePath)}' && ${resolved.agent} '${shSquoteInner(input.prompt)}'`}:resolved}async function spawnConductEpicAgentTab(input,spawnTab){let built=buildConductEpicAgentCommand(input);if(!built.ok)return built;let result;try{result=await spawnTab(built.command,{key:input.ticketKey,worktreePath:input.worktreePath,title:`conduct-epic ${input.ticketKey}`})}catch{return{ok:!1,error:"The terminal tab could not be opened."}}return result.ok?{ok:!0,command:built.command}:{ok:!1,error:result.error}}init_mcp_identity();init_mcp_identity();var NPM_LATEST_ENDPOINT=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`,NPM_LATEST_TIMEOUT_MS=3e3;async function fetchLatestVersion(deps={}){let doFetch=deps.fetch??globalThis.fetch,timeoutSignal=deps.timeoutSignal??(ms=>AbortSignal.timeout(ms));try{let res=await doFetch(NPM_LATEST_ENDPOINT,{signal:timeoutSignal(NPM_LATEST_TIMEOUT_MS)});if(!res||!res.ok)return null;let data=await res.json();if(!data||typeof data!="object"||Array.isArray(data))return null;let version=data.version;if(typeof version!="string")return null;let trimmed=version.trim();return trimmed.length>0?trimmed:null}catch{return null}}init_index_scope_contract();var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn","recover","retire","reclaim"],TICKET_FIELDS=["status","branch","pr_number","spawned_at","parse_requested_at","parse_requested_for_sha","review_verdictless_observations","review_verdictless_for_sha","respawns","conflict_attempts","counters.sessions_spawned","counters.plan_generations_observed","counters.merge_attempts"],TOP_LEVEL_FIELDS=["needs_human","counters.iterations","counters.merges"];function createDefaultConductEpicDeps(){let runCommand=createExecFileRunCommand(),spawner=getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps=createDefaultStartTicketsDeps();return{runCommand,runGh:(args,options)=>runGhCommand(args,options??{}),spawnTab:(shellCommand,context)=>spawner(startTicketsDeps,detectTerminal(void 0,process.env),shellCommand,context),fetchImpl:globalThis.fetch,fs:{mkdir:(dirPath,options)=>nodeFs2.mkdir(dirPath,options),readFile:filePath=>nodeFs2.readFile(filePath,"utf-8"),writeFile:(filePath,data,options)=>nodeFs2.writeFile(filePath,data,{encoding:"utf-8",mode:options?.mode}),writeFileExclusive:async(filePath,data,options)=>{let handle=await nodeFs2.open(filePath,"wx",options?.mode);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(fromPath,toPath)=>nodeFs2.rename(fromPath,toPath),chmod:(filePath,mode)=>nodeFs2.chmod(filePath,mode),unlink:filePath=>nodeFs2.unlink(filePath),stat:filePath=>nodeFs2.stat(filePath)},now:()=>new Date,env:process.env,homedir:os15.homedir,hostname:os15.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveLatestPublishedVersion:()=>fetchLatestVersion({fetch:globalThis.fetch}),resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} conduct-epic <verb> [flags]`,"","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, then create epic/<EPIC> on origin at the commit the"," CANONICAL INDEX covers \u2014 not the base tip \u2014 seed and verify the epic's index"," scope at that commit, repoint the indexed branch, write the checkpoint, and"," take the lock. --base-branch selects the base whose history is fetched and"," recorded; the cut commit is the canonical indexed SHA and is reported"," separately. init fails closed when the repository has no successful parse."," Every preflight failure is printed in one pass; nothing is written unless"," all of them pass. --dry-run prints the validated plan and writes nothing.",""," status <EPIC> [--checkpoint-path <p>] --json"," Print one JSON object describing the in-flight ticket, its worktree, PR,"," CI, review, parse, deadline, and lock state. --json is required. A missing"," checkpoint exits 0 with checkpoint_exists:false. A failed probe leaves its"," sub-object null and is listed in probe_errors; it never fails the command."," `scopes` lists EVERY index scope this repository owns \u2014 expired and"," reclaiming ones included \u2014 so a crashed epic is visible without SQL.",""," checkpoint set <EPIC> --ticket <KEY> [--field <name> <value>]... [--journal <line>]"," [--checkpoint-path <p>]"," Apply ABSOLUTE field values (the caller computes n+1 from status).",` Ticket fields: ${TICKET_FIELDS.join(", ")}.`,` Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`," Repeat --field to write several in ONE atomic mutation."," parse_requested_at / parse_requested_for_sha are ACCEPTED for older"," checkpoints but no longer written: freshness is read from the scope.",""," finish <EPIC> [--checkpoint-path <p>] [--json]"," Restore the server's indexed base branch (idempotent), release the owned"," lock, and print the final summary. A second finish succeeds.","",` spawn <EPIC> --ticket <KEY> --prompt-file <path> [--agent ${CONDUCT_EPIC_AGENTS.join("|")}]`," [--checkpoint-path <p>] [--json]"," Open exactly ONE agent tab in the ticket's worktree running the prompt"," file's contents, then increment counters.sessions_spawned and append a"," journal line. Respawn and conflict budgets are the CALLER's job.",""," recover <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Take a NEW ownership generation for a crashed epic's index scope and"," record the returned fencing epoch locally. Use this instead of SQL when"," `status` shows a scope whose lease expired. Defaults to the epic's own"," scope; --scope targets another one (e.g. when the checkpoint is gone).",""," retire <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Start the scope's retention clock. Deletes NOTHING \u2014 the scope stays"," readable for post-mortem for the whole retention window. Idempotent."," `finish` does this for you; this verb is for retiring without finishing.",""," reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]"," Ask the server to schedule the scope's teardown: three Pinecone"," namespaces, six parse-table slices, three config rows, and a retained"," tombstone. Returns as soon as it is SCHEDULED; watch `status` for the"," result. --override-retention waives only the still-valid-lease and"," unelapsed-retention waits \u2014 an active parse, a held parse lock, a live"," automation run, or a live epic run still refuse. There is no raw"," deletion mode.","","Common:"," -h, --help Show this help","","State lives outside the repository, at ~/.config/bridge/conduct/<repo>/ ","(honoring XDG_CONFIG_HOME): <EPIC>.json, <EPIC>.json.prev, <EPIC>.lock, and","<EPIC>/prompts/. The directory is 0700 and files are 0600.","","Exit codes: 0 on success (including checkpoint_exists:false and an idempotent","finish); non-zero on any failure, with a one-line reason on stderr. With --json,","stdout is exactly one JSON object carrying ok."].join(`
7364
7364
  `)}function parseError(message){return{status:"error",message}}var VERB_FLAGS={init:["--tickets","--base-branch","--checkpoint-path","--dry-run","--json"],status:["--checkpoint-path","--json"],"checkpoint-set":["--ticket","--field","--journal","--checkpoint-path"],finish:["--checkpoint-path","--json"],spawn:["--ticket","--prompt-file","--agent","--checkpoint-path","--json"],recover:["--scope","--checkpoint-path","--json"],retire:["--scope","--checkpoint-path","--json"],reclaim:["--scope","--override-retention","--checkpoint-path","--json"]};function parseConductEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getConductEpicUsage()};if(argv.length===0)return parseError("A verb is required.");let verb,rest;if(argv[0]==="checkpoint"){if(argv[1]!=="set")return parseError("Unknown verb 'checkpoint "+(argv[1]??"")+"'. Expected 'checkpoint set'.");verb="checkpoint-set",rest=argv.slice(2)}else if(argv[0]==="init"||argv[0]==="status"||argv[0]==="finish"||argv[0]==="spawn"||argv[0]==="recover"||argv[0]==="retire"||argv[0]==="reclaim")verb=argv[0],rest=argv.slice(1);else return parseError(`Unknown verb '${argv[0]}'. Expected one of: ${CONDUCT_EPIC_VERBS.join(", ")}.`);let allowed=VERB_FLAGS[verb],options={verb,epicKey:"",tickets:[],fields:[],dryRun:!1,json:!1,overrideRetention:!1},seen=new Set,epicKey;for(let i=0;i<rest.length;i+=1){let arg=rest[i];if(!arg.startsWith("-")){if(epicKey!==void 0)return parseError(`Unexpected argument '${arg}'.`);epicKey=arg;continue}if(!allowed.includes(arg))return parseError(`Unknown flag '${arg}' for '${verbLabel(verb)}'.`);if(arg!=="--field"){if(seen.has(arg))return parseError(`Duplicate flag '${arg}'.`);seen.add(arg)}switch(arg){case"--dry-run":options.dryRun=!0;break;case"--json":options.json=!0;break;case"--override-retention":options.overrideRetention=!0;break;case"--field":{let name=rest[i+1],value=rest[i+2];if(name===void 0||value===void 0)return parseError("--field requires a name and a value.");options.fields.push({name,value}),i+=2;break}default:{let value=rest[i+1];if(value===void 0)return parseError(`${arg} requires a value.`);i+=1;let assigned=assignFlagValue(options,arg,value);if(assigned!==null)return parseError(assigned)}}}if(epicKey===void 0)return parseError(`'${verbLabel(verb)}' requires an <EPIC> key.`);if(!CONDUCT_EPIC_KEY_PATTERN.test(epicKey))return parseError(`Invalid epic key '${epicKey}'. Expected the form PROJ-123.`);options.epicKey=epicKey;let missing=requiredFlagError(options);return missing!==null?parseError(missing):{status:"ok",options}}function verbLabel(verb){return verb==="checkpoint-set"?"checkpoint set":verb}function assignFlagValue(options,flag,value){switch(flag){case"--tickets":{let keys=value.split(",").map(k=>k.trim()).filter(k=>k.length>0);if(keys.length===0)return"--tickets requires at least one ticket key.";for(let key of keys)if(!CONDUCT_EPIC_KEY_PATTERN.test(key))return`Invalid ticket key '${key}' in --tickets. Expected the form PROJ-123.`;return new Set(keys).size!==keys.length?"--tickets must not repeat a ticket key.":(options.tickets=keys,null)}case"--ticket":return CONDUCT_EPIC_KEY_PATTERN.test(value)?(options.ticket=value,null):`Invalid ticket key '${value}'. Expected the form PROJ-123.`;case"--base-branch":{let reason=validateBranchName(value);return reason?`Invalid --base-branch value: ${reason}`:(options.baseBranch=value,null)}case"--checkpoint-path":return value.trim().length===0?"--checkpoint-path requires a path.":(options.checkpointPath=value,null);case"--prompt-file":return value.trim().length===0?"--prompt-file requires a path.":(options.promptFile=value,null);case"--agent":return CONDUCT_EPIC_AGENTS.includes(value)?(options.agent=value,null):`Unsupported agent '${value}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`;case"--journal":return options.journal=value,null;case"--scope":return/^[0-9a-f]{32}$/.test(value)?(options.scope=value,null):`Invalid --scope value '${value}'. Expected a 32-character index-scope id.`;default:return`Unknown flag '${flag}'.`}}function requiredFlagError(options){if(options.verb==="init"&&options.tickets.length===0)return"init requires --tickets K1,K2,...";if(options.verb==="status"&&!options.json)return"status requires --json.";if(options.verb==="checkpoint-set"){if(options.ticket===void 0)return"checkpoint set requires --ticket <KEY>.";if(options.fields.length===0&&options.journal===void 0)return"checkpoint set requires at least one --field or --journal."}if(options.verb==="spawn"){if(options.ticket===void 0)return"spawn requires --ticket <KEY>.";if(options.promptFile===void 0)return"spawn requires --prompt-file <path>."}return null}function emitSuccess(deps,json,payload,humanLines=[]){if(json){for(let line of humanLines)deps.errorLog(line);deps.log(JSON.stringify(payload,null,2))}else for(let line of humanLines)deps.log(line);return 0}function emitFailure(deps,json,reasons,payload={}){for(let reason of reasons)deps.errorLog(reason);return json&&deps.log(JSON.stringify({ok:!1,...payload,failures:reasons},null,2)),1}function epicBranchFor(epicKey){return`epic/${epicKey}`}function resolveCheckpointPath(deps,repoName,epicKey,override){return override!==void 0?path31.resolve(override):resolveConductEpicCheckpointPath(repoName,epicKey,{env:deps.env,homedir:deps.homedir})}function buildConductEpicLockSeams(deps){return{readFile:filePath=>deps.fs.readFile(filePath),removeFile:filePath=>deps.fs.unlink(filePath),mkdir:(dirPath,options)=>deps.fs.mkdir(dirPath,options),isProcessAlive:deps.isProcessAlive,writeFileExclusive:(filePath,data)=>deps.fs.writeFileExclusive(filePath,data,{mode:384})}}function lockRequest(deps){return{ownerPid:deps.pid,host:deps.hostname(),acquiredAt:deps.now().toISOString()}}async function resolveRepoNameForPath(deps){try{let result=await deps.resolveRepoName({env:deps.env,cwd:deps.cwd,readFile:filePath=>deps.fs.readFile(filePath)});if(result.ok)return result.repoName}catch{}return"unknown"}async function resolveAccess(deps){let result=await deps.resolveAccess({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:filePath=>deps.fs.readFile(filePath),stat:filePath=>deps.fs.stat(filePath)});return result.ok?{ok:!0,access:result.access}:{ok:!1,error:`Bridge credentials could not be resolved: ${result.error}`}}function git(deps,args){return runGit(cutProtocolDeps2(deps),args)}function cutProtocolDeps2(deps){return{runCommand:deps.runCommand,cwd:deps.cwd,fetchImpl:deps.fetchImpl,errorLog:deps.errorLog}}function isRecord4(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function elapsedSeconds(from,now){if(from===null)return null;let start=Date.parse(from);return Number.isNaN(start)?null:Math.max(0,Math.floor((now.getTime()-start)/1e3))}function inFlightTicket(checkpoint){return checkpoint.tickets.find(ticket=>ticket.status!=="done")??null}var PUBLISHED_IDENTITY_TIMEOUT_MS=12e4,PUBLISHED_IDENTITY_PATTERN=/^commit: ([0-9a-f]{12})(-dirty)?$/,PUBLISHED_IDENTITY_UNKNOWN="unknown";function describePublishedIdentityReason(reason){switch(reason){case"registry_unreadable":return"the npm registry could not be read";case"launch_failed":return"the published package could not be launched";case"unreadable_output":return"the published package reported no readable build identity";case"identity_unknown":return"the published build reports an unknown build commit"}}async function readPublishedBuildIdentity(deps){let resolveVersion=deps.resolveLatestPublishedVersion??(()=>fetchLatestVersion({fetch:deps.fetchImpl})),version;try{version=await resolveVersion()}catch{return{kind:"unavailable",reason:"registry_unreadable"}}if(typeof version!="string"||version.trim().length===0)return{kind:"unavailable",reason:"registry_unreadable"};let resolvedVersion=version.trim(),probe;try{probe=await deps.runCommand("npx",["-y",`${MCP_PACKAGE_NAME}@${resolvedVersion}`,"--version"],{cwd:deps.cwd,timeoutMs:PUBLISHED_IDENTITY_TIMEOUT_MS})}catch{return{kind:"unavailable",reason:"launch_failed"}}if(!probe||probe.exitCode!==0)return{kind:"unavailable",reason:"launch_failed"};let lines=String(probe.stdout??"").split(`
7365
- `).map(line=>line.trim()).filter(line=>line.length>0);if(lines[0]!==resolvedVersion)return{kind:"unavailable",reason:"unreadable_output"};let commitLine=lines.slice(1).find(line=>line.startsWith("commit:"));if(commitLine===void 0)return{kind:"unavailable",reason:"unreadable_output"};if(commitLine===`commit: ${PUBLISHED_IDENTITY_UNKNOWN}`)return{kind:"unavailable",reason:"identity_unknown"};let match=PUBLISHED_IDENTITY_PATTERN.exec(commitLine);return match===null?{kind:"unavailable",reason:"unreadable_output"}:{kind:"known",version:resolvedVersion,commit:match[1],dirty:match[2]!==void 0}}async function evaluatePublishGate(deps,expectedCommitSha){if(expectedCommitSha===null)return{failures:[],advisories:["advisory: the publish gate was not evaluated because no canonical indexed commit is available to check against."]};let expected=normalizeCommitSha(expectedCommitSha);if(expected===null)return{failures:["The publish gate cannot be evaluated: the expected commit is not a full 40-character SHA. Refusing rather than comparing an arbitrary prefix."],advisories:[]};let identity=await readPublishedBuildIdentity(deps);if(identity.kind==="unavailable")return{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 ${describePublishedIdentityReason(identity.reason)}. Initialization is continuing; the published ${MCP_PACKAGE_NAME} build was NOT confirmed to contain ${expected}.`]};if(identity.dirty)return{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} reports build commit ${identity.commit}-dirty. A dirty build carries content that no commit represents, so it cannot be verified to contain ${expected}. Publish a build from a clean checkout.`],advisories:[]};if((await git(deps,["rev-parse","--verify","--quiet",`${identity.commit}^{commit}`])).exitCode!==0)return{failures:[],advisories:[`advisory: the published ${MCP_PACKAGE_NAME}@${identity.version} build commit ${identity.commit} is not present in this checkout, so the publish gate could not be verified. Initialization is continuing; fetch origin and confirm that build contains ${expected}.`]};let contains=await git(deps,["merge-base","--is-ancestor",expected,identity.commit]);return contains.exitCode===0?{failures:[],advisories:[]}:contains.exitCode===1?{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} was built from ${identity.commit}, which does not contain ${expected} \u2014 the canonical indexed commit this epic is cut at. Publish a build containing that commit before initializing.`],advisories:[]}:{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 the ancestry of published build commit ${identity.commit} could not be determined locally. Initialization is continuing.`]}}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],advisories=[],epicBranch=epicBranchFor(options.epicKey),pendingSupervisorConfig=null;try{(await deps.runGh(["auth","status"],{cwd:deps.cwd})).ok||failures.push("gh is not authenticated. Run `gh auth login`.")}catch{failures.push("gh could not be run. Install the GitHub CLI and run `gh auth login`.")}let worktrunk=resolveWorktrunkBinary(deps.platform,deps.env);try{(await deps.runCommand(worktrunk,["--version"],{cwd:deps.cwd})).exitCode!==0&&failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}catch{failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}let accessResult=await resolveAccess(deps),access2=accessResult.ok?accessResult.access:null;accessResult.ok||failures.push(accessResult.error);let baseBranch=options.baseBranch??null,baseSha=null,cutCommitSha=null,epicBranchAlreadyAtCut=!1;if(access2!==null){let readiness=await getConductorReadiness(access2,deps.fetchImpl);if(!readiness.ok)failures.push(`Conductor readiness could not be read: ${readiness.error}`);else if(!readiness.value.supervisor.auto_merge_enabled){let current=await getEffectiveSupervisorConfig(access2,deps.fetchImpl);if(!current.ok)failures.push(`auto_merge_enabled is not set and the effective supervisor config could not be read: ${current.error}`);else{let body={teardown_enabled:current.value.teardown_enabled,auto_rereview_enabled:current.value.auto_rereview_enabled,max_remediation_attempts:current.value.max_remediation_attempts,max_remediation_no_progress_attempts:current.value.max_remediation_no_progress_attempts,worker_liveness_window_seconds:current.value.worker_liveness_window_seconds,auto_merge_enabled:!0,merge_approval_required:current.value.merge_approval_required};options.dryRun?announcements.push(`announced: would enable auto_merge_enabled on the ${access2.repoName} project defaults (--dry-run: not sent).`):pendingSupervisorConfig=body}}let resolved=await resolveCiChecks(access2,void 0,deps.fetchImpl),resolvedRequired=requiredCheckNamesFromResolve(resolved.ok?resolved.value:null);readiness.ok&&readiness.value.supervisor.required_checks_empty&&(resolved.ok?resolvedRequired.length===0&&failures.push("No required CI checks are configured and resolve-ci-checks returned none. A done gate with an empty required set passes vacuously."):failures.push(`No required CI checks are configured and resolve-ci-checks failed: ${resolved.error}`));let runState=await getEpicRunState(access2,options.epicKey,deps.fetchImpl);if(runState.ok){let status=runState.value.epic_run?.status;status!=="done"&&status!=="abandoned"&&failures.push(`A server-side epic run for ${options.epicKey} is ${String(status)}. conduct-epic will not drive an epic the v2 reconciler is already conducting.`)}else runState.status!==404&&failures.push(`The epic-run state for ${options.epicKey} could not be read: ${runState.error}`);let parseStatus=await getParseStatus(access2,deps.fetchImpl);if(!parseStatus.ok)failures.push(`The canonical parse status could not be read: ${parseStatus.error}`);else if(parseStatus.value.status!=="succeeded")failures.push(`The canonical index for ${access2.repoName} has no successful parse (status: ${String(parseStatus.value.status)}). Parse the repository first.`);else{let indexed=normalizeCommitSha(parseStatus.value.indexed_commit_sha);indexed===null?failures.push(`The canonical index for ${access2.repoName} published no commit for its last successful parse, so there is no commit to cut at. Parse the repository first.`):cutCommitSha=indexed}if(baseBranch===null){let configured=await getConfigFieldBaseBranch(access2,deps.fetchImpl);configured.ok&&configured.value.base_branch!==null&&(baseBranch=configured.value.base_branch)}}baseBranch===null&&(baseBranch="main");let branchReason=validateBranchName(baseBranch);if(branchReason)failures.push(`The resolved base branch is unusable: ${branchReason}`);else{(await git(deps,["fetch","origin",baseBranch])).exitCode!==0&&failures.push(`git fetch origin ${baseBranch} failed.`);let tip=await git(deps,["rev-parse",`refs/remotes/origin/${baseBranch}`]);baseSha=tip.exitCode===0?firstOutputLine(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`),cutCommitSha!==null&&(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(await git(deps,["fetch","origin",cutCommitSha]),(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(failures.push(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2?.repoName??"the repository"}.`),cutCommitSha=null));let publishGate=await evaluatePublishGate(deps,cutCommitSha);failures.push(...publishGate.failures),advisories.push(...publishGate.advisories);let existing=await git(deps,["ls-remote","--heads","origin",`refs/heads/${epicBranch}`]);if(existing.exitCode!==0)failures.push(`git ls-remote could not read origin/${epicBranch}.`);else{let existingSha=lsRemoteSha(existing);existingSha!==null&&(cutCommitSha!==null&&existingSha===cutCommitSha?epicBranchAlreadyAtCut=!0:failures.push(`origin/${epicBranch} already exists at ${existingSha}, which is not the canonical indexed commit${cutCommitSha?` ${cutCommitSha}`:""}. Delete it or finish the previous run before re-initializing.`))}}let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));return lockState.kind==="live-foreign"?failures.push(`The epic lock is held by live process ${lockState.owner.owner_pid} on ${lockState.owner.host}.`):lockState.kind==="remote-host"?failures.push(`The epic lock was taken on host ${lockState.owner.host} and cannot be recovered here.`):lockState.kind==="unknown"&&failures.push(`The epic lock is unusable: ${lockState.reason}`),{failures,announcements,advisories,access:access2,baseBranch,baseSha,cutCommitSha,epicBranchAlreadyAtCut,pendingSupervisorConfig}}function requiredCheckNamesFromResolve(value){if(!isRecord4(value))return[];let detail=value.detail;if(!isRecord4(detail))return[];let checks=detail.checks;if(!Array.isArray(checks))return[];let names=[];for(let check of checks){if(!isRecord4(check)||check.required!==!0)continue;let name=check.name;typeof name=="string"&&name.trim().length>0&&names.push(name.trim())}return names}async function driveIndexScopeBootstrap(deps,access2,scopeId,options){let scheduled=await bootstrapIndexScope(access2,{scopeId},deps.fetchImpl);if(!scheduled.ok)return{ok:!1,failures:[`The index scope could not be seeded: ${scheduled.error}`]};let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),lastState="unknown";for(let poll=0;poll<SCOPE_BOOTSTRAP_MAX_POLLS;poll+=1){await sleep3(SCOPE_BOOTSTRAP_POLL_INTERVAL_MS);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl);if(!status.ok){lastState=`unreadable (${status.error})`;continue}if(lastState=status.value.lifecycle_state,status.value.lifecycle_state==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{ok:!0,failures:[]}:{ok:!1,failures:[`Index scope ${scopeId} reports ready but its indexed commit (${status.value.indexed_commit_sha??"none"}) is not the cut commit (${status.value.cut_commit_sha??"none"}).`]};if(status.value.lifecycle_state==="failed"){let reason=status.value.last_error??"unknown";return reason==="canonical_index_advanced"?{ok:!1,failures:[`The canonical index advanced before the seed could run, so the scope was not seeded. Delete origin/${epicBranchFor(options.epicKey)} and re-run init to cut at the newer commit.`]}:{ok:!1,failures:[`Index scope ${scopeId} failed verification (${reason}). The epic branch and its recorded cut are intact; re-run init to re-drive verification.`]}}}return{ok:!1,failures:[`Index scope ${scopeId} did not become ready within the bootstrap window (last observed state: ${lastState}). Re-run init to resume verification.`]}}async function runConductEpicInit(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),epicBranch=epicBranchFor(options.epicKey);if((await readConductEpicCheckpoint(checkpointPath,deps.fs)).kind!=="missing")return emitFailure(deps,options.json,[`${options.epicKey} is already initialized: a checkpoint exists at ${checkpointPath}. Nothing was changed.`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let preflight=await collectConductEpicInitPreflight(deps,options);for(let line of preflight.advisories)deps.errorLog(line);if(preflight.failures.length>0){for(let line of preflight.announcements)deps.errorLog(line);return emitFailure(deps,options.json,preflight.failures,{epic_key:options.epicKey,checkpoint_path:checkpointPath})}let access2=preflight.access;if(access2===null||preflight.baseBranch===null||preflight.baseSha===null||preflight.cutCommitSha===null)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let cutCommitSha=preflight.cutCommitSha,announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`cut: ${cutCommitSha} (canonical indexed commit)`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtCut?" (already at the cut commit)":""}`,`tickets: ${options.tickets.join(", ")}`,`checkpoint: ${checkpointPath}`,...announcements];if(options.dryRun)return emitSuccess(deps,options.json,{ok:!0,dry_run:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, cut, seeded, repointed, or written):",...describePlan()]);if(preflight.pendingSupervisorConfig!==null){let put=await putSupervisorConfigDefaults(access2,preflight.pendingSupervisorConfig,deps.fetchImpl);if(!put.ok)return emitFailure(deps,options.json,[`auto_merge_enabled could not be enabled: ${put.error}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});announcements.push(`announced: auto_merge_enabled was OFF and has been enabled on the ${access2.repoName} project defaults.`)}let cutOutcome=await performExactIndexScopeCut(cutProtocolDeps2(deps),access2,{featureBranch:epicBranch,baseBranch:preflight.baseBranch,candidateCommitSha:cutCommitSha});if(!cutOutcome.ok)return emitFailure(deps,options.json,cutOutcome.failures,cutOutcome.kind==="begin_refused"?{epic_key:options.epicKey,checkpoint_path:checkpointPath}:{epic_key:options.epicKey});let cut=cutOutcome.lease,scopeReady=await driveIndexScopeBootstrap(deps,access2,cut.scope_id,options);if(!scopeReady.ok)return emitFailure(deps,options.json,scopeReady.failures,{epic_key:options.epicKey,scope_id:cut.scope_id,checkpoint_path:checkpointPath});announcements.push(`announced: index scope ${cut.scope_id} is ready at ${cut.cut_commit_sha}.`);let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,indexScopeId:cut.scope_id,ticketKeys:options.tickets,now:deps.now().toISOString(),lock:{owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt}}),written=await writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error],{epic_key:options.epicKey});let lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),request,buildConductEpicLockSeams(deps));return lock.acquired?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,scope_id:cut.scope_id,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),announcements},["Initialized:",...describePlan()]):emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath})}async function runConductEpicStatus(deps,options){let accessProbe=await resolveAccess(deps),repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,checkpoint_path:checkpointPath,checkpoint_exists:!1});if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let checkpoint=read.checkpoint,now=deps.now(),probeErrors=[],ticket=inFlightTicket(checkpoint),allDone=ticket===null,discoveredBranch=ticket?.branch??null,worktreePath=null,worktreeExists=!1;if(ticket!==null)try{let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)probeErrors.push({probe:"worktree",reason:"git worktree list failed"});else{let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticket.key,ticket.branch);if(found!==null){discoveredBranch=found.branch,worktreePath=found.path;try{await deps.fs.stat(found.path),worktreeExists=!0}catch{worktreeExists=!1}}}}catch{probeErrors.push({probe:"worktree",reason:"git worktree list could not be run"})}let branchHead=null,workerCommits=0;if(discoveredBranch!==null){try{let remote=await git(deps,["ls-remote","--heads","origin",discoveredBranch]);remote.exitCode!==0?probeErrors.push({probe:"branch_head",reason:"git ls-remote failed"}):branchHead=lsRemoteSha(remote)}catch{probeErrors.push({probe:"branch_head",reason:"git ls-remote could not be run"})}if(branchHead!==null)try{await git(deps,["fetch","origin"]);let counted=await git(deps,["rev-list","--count",`origin/${checkpoint.epic_branch}..origin/${discoveredBranch}`]);if(counted.exitCode===0){let parsedCount=Number.parseInt(firstOutputLine(counted)??"",10);workerCommits=Number.isSafeInteger(parsedCount)&&parsedCount>=0?parsedCount:0}else probeErrors.push({probe:"worker_commits",reason:"git rev-list failed"})}catch{probeErrors.push({probe:"worker_commits",reason:"git rev-list could not be run"})}}let pr=null;if(discoveredBranch!==null){let probe=await discoverConductEpicPrState(discoveredBranch,{runGh:deps.runGh,cwd:deps.cwd});probe.kind==="ok"?pr=probe.pr:probe.kind==="error"&&probeErrors.push({probe:"pr",reason:probe.reason})}let mergedExternally=pr?.state==="MERGED"&&ticket!==null&&ticket.status!=="merged"&&ticket.status!=="done",access2=accessProbe.ok?accessProbe.access:null;accessProbe.ok||probeErrors.push({probe:"credentials",reason:accessProbe.error});let doneGateRequired=null,reviewOptedIn=!1,reviewSource=null;if(access2!==null){let setup=await getEffectiveSupervisorSetup(access2,deps.fetchImpl);if(!setup.ok)probeErrors.push({probe:"supervisor_setup",reason:setup.error});else{let gate=parseDoneGateConfig(setup.value.done_gate_config);for(let condition of gate.conditions)condition.type==="required_ci_checks_green"?doneGateRequired=[...condition.required_checks]:condition.type==="review_state"&&(reviewOptedIn=!0,reviewSource=condition.source)}}let ci=access2===null||pr?.head_sha==null?null:await collectCiFacts(deps,access2,pr.head_sha,doneGateRequired,checkpoint.ci_last_poll,probeErrors),review={opted_in:reviewOptedIn,source:reviewSource,available:null,verdict:null,head_sha:null};if(access2!==null&&reviewOptedIn&&pr?.number!=null){let status=await getPrReviewStatus(access2,pr.number,deps.fetchImpl);status.ok?review=normalizeReviewStatus(status.value,reviewOptedIn,reviewSource):(probeErrors.push({probe:"review",reason:status.error}),review={opted_in:!0,source:reviewSource,available:null,verdict:null,head_sha:null})}let parse=null;if(access2!==null){let parseStatus=await getParseStatus(access2,deps.fetchImpl);parseStatus.ok?parse=normalizeParseStatus(parseStatus.value):probeErrors.push({probe:"parse",reason:parseStatus.error})}let scope=null,declaredScopeId=typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0?checkpoint.index_scope_id:null;if(access2!==null&&declaredScopeId!==null){let scopeStatus=await getIndexScopeStatus(access2,declaredScopeId,deps.fetchImpl);scopeStatus.ok?scope={scope_id:scopeStatus.value.scope_id,lifecycle_state:scopeStatus.value.lifecycle_state,freshness_status:scopeStatus.value.freshness_status??"unavailable",blocked_reason:scopeStatus.value.blocked_reason,required_commit_sha:scopeStatus.value.required_commit_sha,indexed_commit_sha:scopeStatus.value.indexed_commit_sha,last_error:scopeStatus.value.last_error}:(probeErrors.push({probe:"scope",reason:scopeStatus.error}),scope={scope_id:declaredScopeId,lifecycle_state:null,freshness_status:"unavailable",blocked_reason:null,required_commit_sha:null,indexed_commit_sha:null,last_error:null})}let scopes=[],retentionSeconds=null,nextLeaseEpoch=checkpoint.index_scope_lease_epoch;if(access2!==null){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);if(listing.ok?(scopes=listing.value.scopes,retentionSeconds=listing.value.retention_seconds):probeErrors.push({probe:"scopes",reason:listing.error}),declaredScopeId!==null){let entry=scopes.find(scope2=>scope2.scope_id===declaredScopeId),epoch=entry?.lease_epoch??checkpoint.index_scope_lease_epoch;if(epoch!==null&&entry?.recoverable!==!1){let beat=await heartbeatIndexScope(access2,{scopeId:declaredScopeId,leaseEpoch:epoch},deps.fetchImpl);beat.ok?nextLeaseEpoch=beat.value.lease_epoch:probeErrors.push({probe:"scope_heartbeat",reason:beat.error})}}}let lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps)),lock={held_by_me:lockState.kind==="owned",owner_pid:"owner"in lockState?lockState.owner.owner_pid:null,host:"owner"in lockState?lockState.owner.host:null,alive:lockState.kind==="owned"||lockState.kind==="live-foreign"?!0:lockState.kind==="dead-local"?!1:null},lastSeenHead=ticket?.last_seen_head??null,lastStateChangeAt=ticket?.last_state_change_at??null,leaseEpochChanged=nextLeaseEpoch!==checkpoint.index_scope_lease_epoch;if(ticket!==null){let next={...checkpoint,tickets:[...checkpoint.tickets]},index=next.tickets.findIndex(entry=>entry.key===ticket.key),dirty=!1,updatedTicket={...next.tickets[index]};if(updatedTicket.branch===null&&discoveredBranch!==null&&(updatedTicket.branch=discoveredBranch,dirty=!0),branchHead!==null&&branchHead!==updatedTicket.last_seen_head&&(updatedTicket.last_state_change_at=now.toISOString(),updatedTicket.last_seen_head=branchHead,lastSeenHead=branchHead,lastStateChangeAt=updatedTicket.last_state_change_at,dirty=!0),next.tickets[index]=updatedTicket,ci?.ci_last_poll&&(next.ci_last_poll=ci.ci_last_poll,dirty=!0),leaseEpochChanged&&(next.index_scope_lease_epoch=nextLeaseEpoch,dirty=!0),dirty){next.updated_at=now.toISOString();let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});written.ok||probeErrors.push({probe:"checkpoint_write",reason:written.error})}}let payload={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,checkpoint_path:checkpointPath,checkpoint_exists:!0,all_done:allDone,ticket:ticket===null?null:projectConductEpicTicketFacts(ticket,discoveredBranch),worktree_path:worktreePath,worktree_exists:worktreeExists,branch_head:branchHead,worker_commits_since_spawn:workerCommits,last_seen_head:lastSeenHead,last_state_change_at:lastStateChangeAt,stale_for_seconds:elapsedSeconds(lastStateChangeAt,now),pr,merged_externally:!!mergedExternally,ci:ci?.ci??null,review,parse,deadlines:{soft_seconds:checkpoint.deadlines.soft_seconds,hard_seconds:checkpoint.deadlines.hard_seconds,elapsed_since_spawn_seconds:elapsedSeconds(ticket?.spawned_at??null,now)},scope,scope_lease_epoch:nextLeaseEpoch,retention_seconds:retentionSeconds,scopes,lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload,[...renderScopeFreshnessLines(scope),...renderStrandedScopeLines(scopes,declaredScopeId)])}function renderStrandedScopeLines(scopes,ownScopeId){let stranded=scopes.filter(scope=>scope.scope_id!==ownScopeId&&scope.lifecycle_state!=="reclaimed"&&!scope.lease_valid);if(stranded.length===0)return[];let lines=[`${stranded.length} index scope(s) in this repository have no live lease:`];for(let scope of stranded){let action=scope.recoverable?"recoverable \u2014 `conduct-epic recover <EPIC> --scope "+scope.scope_id+"`":scope.retention_elapsed?"past retention \u2014 the sweep will reclaim it":`retained until ${scope.retention_deadline??"an unknown deadline"}`;lines.push(` ${scope.scope_id} ${scope.lifecycle_state??"unknown"} branch=${scope.feature_branch??"unknown"} ${action}`),scope.blockers.length>0&&lines.push(` blocked by: ${scope.blockers.join(", ")}`)}return lines}function renderScopeFreshnessLines(scope){if(scope===null)return[];let headline={fresh:"Index is fresh for this epic.",pending:"Waiting for index refresh.",blocked:"Index refresh is BLOCKED \u2014 this advance will not be indexed.",failed:"Index generation FAILED for this scope.",unavailable:"Index freshness is unavailable \u2014 treat as not fresh."},refusal={advance_blocked_base_merge:"the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",advance_blocked_unexpected_parent:"the merge commit does not descend directly from the head this scope pinned, so it is not a worker merge",advance_blocked_history_changed:"the pinned head is gone from the branch's history \u2014 a force-push or rewrite",advance_blocked_unverifiable:"the advance could not be verified at all, and doubt blocks rather than indexes"},lines=[headline[scope.freshness_status]??"Index freshness is unknown \u2014 treat as not fresh.",` lifecycle: ${scope.lifecycle_state??"unknown"}`,` Required commit: ${scope.required_commit_sha??"none"}`,` Indexed commit: ${scope.indexed_commit_sha??"none"}`];return scope.blocked_reason!==null?(lines.push(` Reason: ${scope.blocked_reason} \u2014 ${refusal[scope.blocked_reason]??"the server refused this branch advance"}`),lines.push(" A human must resolve the branch before the epic can continue.")):scope.freshness_status==="failed"&&scope.last_error!==null&&lines.push(` Failure category: ${scope.last_error}`),lines}function projectConductEpicTicketFacts(ticket,discoveredBranch){return{key:ticket.key,status:ticket.status,branch:discoveredBranch,pr_number:ticket.pr_number,spawned_at:ticket.spawned_at,parse_requested_at:ticket.parse_requested_at,parse_requested_for_sha:ticket.parse_requested_for_sha,respawns:ticket.respawns,conflict_attempts:ticket.conflict_attempts,counters:{...ticket.counters},journal:[...ticket.journal]}}async function collectCiFacts(deps,access2,headSha,doneGateRequired,previous,probeErrors){let resolvedValue=null,resolveCalled=!1,ensureResolved=async()=>{if(resolveCalled)return;resolveCalled=!0;let resolved=await resolveCiChecks(access2,headSha,deps.fetchImpl);resolved.ok?resolvedValue=resolved.value:probeErrors.push({probe:"ci_resolve",reason:resolved.error})},polled=await pollCiChecks(access2,headSha,deps.fetchImpl);if(!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1){if(await ensureResolved(),polled=await pollCiChecks(access2,headSha,deps.fetchImpl),!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1)return probeErrors.push({probe:"ci",reason:"CI checks are unavailable after resolve-ci-checks and a second poll"}),null}let detail=isRecord4(polled.value)?polled.value.detail:null,rawChecks=isRecord4(detail)&&Array.isArray(detail.checks)?detail.checks:[],checks=rawChecks.filter(isRecord4).map(check=>({name:typeof check.name=="string"?check.name:"",status:check.status??null,conclusion:check.conclusion??null,required:check.required!==!1})),required=doneGateRequired;required===null&&(await ensureResolved(),required=requiredCheckNamesFromResolve(resolvedValue));let requiredSorted=[...new Set(required)].sort(),complete=requiredSorted.length>0&&requiredSorted.every(name=>{let raw=rawChecks.filter(isRecord4).find(check=>check.name===name);return raw!==void 0&&raw.complete===!0&&raw.passed===!0}),fingerprint=JSON.stringify(checks.filter(check=>requiredSorted.includes(check.name)).map(check=>[check.name,check.status??null,check.conclusion??null]).sort((a,b)=>String(a[0]).localeCompare(String(b[0])))),stable=previous!==null&&previous.head_sha===headSha&&previous.results_fingerprint===fingerprint&&previous.required.length===requiredSorted.length&&previous.required.every((name,i)=>name===requiredSorted[i]);return{ci:{required:requiredSorted,complete,stable_across_two_polls:stable,head_sha:headSha,checks},ci_last_poll:{head_sha:headSha,required:requiredSorted,results_fingerprint:fingerprint,at:deps.now().toISOString()}}}function normalizeReviewStatus(value,optedIn,source){let available=isRecord4(value)&&value.available===!0,detail=isRecord4(value)&&isRecord4(value.detail)?value.detail:null,verdict=null;if(detail!==null){let sticky=detail.sticky_verdict,native=detail.review_decision;typeof sticky=="string"&&sticky.trim().length>0?verdict=normalizeVerdict(sticky):typeof native=="string"&&native.trim().length>0?verdict=normalizeVerdict(native):verdict="unknown"}return{opted_in:optedIn,source,available:isRecord4(value)?!!value.available:null,verdict:available||verdict!==null?verdict:null,head_sha:detail!==null&&typeof detail.head_sha=="string"?detail.head_sha:null}}function normalizeVerdict(raw){let value=raw.trim().toLowerCase();return value==="approved"?"approved":value==="changes_requested"?"changes_requested":"unknown"}function normalizeParseStatus(value){let known=["idle","queued","in_progress","succeeded","failed"],raw=isRecord4(value)?value.status:null,status=typeof raw=="string"&&known.includes(raw)?raw:null,optionalText=key=>{let field=isRecord4(value)?value[key]:null;if(typeof field!="string")return null;let trimmed=field.trim();return trimmed.length===0?null:trimmed};return{status,terminal:status==="succeeded"||status==="failed",started_at:optionalText("started_at"),finished_at:optionalText("finished_at")}}async function runConductEpicCheckpointSet(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets (${checkpoint.tickets.map(t=>t.key).join(", ")}).`]);let next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))},now=deps.now().toISOString(),statusBefore=next.tickets[index].status;for(let assignment of options.fields){let applied=applyFieldAssignment(next,index,assignment);if(applied!==null)return emitFailure(deps,options.json,[applied])}next.tickets[index].status!==statusBefore&&(next.tickets[index].last_state_change_at=now),options.journal!==void 0&&(next.tickets[index]=appendTicketJournal(next.tickets[index],options.journal)),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error]);let changed=options.fields.map(f=>f.name);return options.journal!==void 0&&changed.push("journal"),emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,updated_fields:changed,checkpoint_path:checkpointPath},[`Updated ${ticketKey}: ${changed.join(", ")}`])}finally{await releaseAcquired(lock)}}async function releaseAcquired(lock){await lock.release()}function applyFieldAssignment(checkpoint,ticketIndex,assignment){let{name,value}=assignment,ticket=checkpoint.tickets[ticketIndex];switch(name){case"status":return CONDUCT_EPIC_TICKET_STATUSES.includes(value)?(ticket.status=value,null):`Invalid status '${value}'. Expected one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`;case"branch":{if(value==="null")return ticket.branch=null,null;let reason=validateBranchName(value);return reason?`Invalid branch: ${reason}`:(ticket.branch=value,null)}case"pr_number":{if(value==="null")return ticket.pr_number=null,null;let parsed=parseIntegerField(value);return parsed===null||parsed<=0?"pr_number must be a positive integer or null.":(ticket.pr_number=parsed,null)}case"spawned_at":return value==="null"?(ticket.spawned_at=null,null):value.trim().length===0?"spawned_at must be a non-empty timestamp or null.":(ticket.spawned_at=value,null);case"parse_requested_at":case"parse_requested_for_sha":return value==="null"?(ticket[name]=null,null):value.trim().length===0?`${name} must be a non-empty string or null.`:(ticket[name]=value,null);case"respawns":case"conflict_attempts":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(ticket[name]=parsed,null)}case"counters.sessions_spawned":case"counters.plan_generations_observed":case"counters.merge_attempts":{let parsed=parseIntegerField(value);if(parsed===null)return`${name} must be a non-negative integer.`;let key=name.slice(9);return ticket.counters[key]=parsed,null}case"counters.iterations":case"counters.merges":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(checkpoint.counters[name.slice(9)]=parsed,null)}case"needs_human":{if(value==="null")return checkpoint.needs_human=null,null;let parsed;try{parsed=JSON.parse(value)}catch{return"needs_human must be JSON null or an object with reason, evidence, and at."}return parsed===null?(checkpoint.needs_human=null,null):!isRecord4(parsed)||typeof parsed.reason!="string"||typeof parsed.evidence!="string"||typeof parsed.at!="string"?"needs_human must be JSON null or an object with string reason, evidence, and at.":(checkpoint.needs_human={reason:parsed.reason,evidence:parsed.evidence,at:parsed.at},null)}default:return`Unknown field '${name}'. Ticket fields: ${TICKET_FIELDS.join(", ")}. Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`}}function parseIntegerField(value){if(!/^\d+$/.test(value.trim()))return null;let parsed=Number.parseInt(value.trim(),10);return Number.isSafeInteger(parsed)&&parsed>=0?parsed:null}async function runConductEpicSpawn(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets.`]);let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)return emitFailure(deps,options.json,["git worktree list failed; the ticket worktree could not be resolved."]);let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticketKey,checkpoint.tickets[index].branch);if(found===null)return emitFailure(deps,options.json,[`No worktree was found for ${ticketKey}. Expected a worktree on the ticket's branch, feature/${ticketKey}, or feature/${ticketKey}-<slug>.`]);try{await deps.fs.stat(found.path)}catch{return emitFailure(deps,options.json,[`The worktree path for ${ticketKey} is not accessible.`])}let prompt;try{prompt=await deps.fs.readFile(options.promptFile)}catch{return emitFailure(deps,options.json,[`The prompt file '${options.promptFile}' could not be read.`])}let indexScope;try{indexScope=validateOptionalIndexScope(checkpoint.index_scope_id)}catch{return emitFailure(deps,options.json,[INDEX_SCOPE_CONFIGURATION_ERROR])}let spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform,...indexScope===void 0?{}:{indexScope}},deps.spawnTab);if(!spawned.ok)return emitFailure(deps,options.json,[spawned.error]);let now=deps.now().toISOString(),next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))};next.tickets[index].branch=found.branch,next.tickets[index].counters.sessions_spawned+=1,next.tickets[index]=appendTicketJournal(next.tickets[index],`${now} spawned ${options.agent??"claude"} in ${found.branch}`),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});return written.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,branch:found.branch,worktree_path:found.path,sessions_spawned:next.tickets[index].counters.sessions_spawned},[`Spawned one agent tab for ${ticketKey} in ${found.path}`]):emitFailure(deps,options.json,[written.error])}finally{await releaseAcquired(lock)}}async function runConductEpicFinish(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let access2=accessProbe.access,checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);let scopeRetired=null,scopeRetirementError=null;if(typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0){let retirement=await retireScopeWithEpoch(deps,options,access2,{scopeId:checkpoint.index_scope_id,checkpoint,leaseEpoch:checkpoint.index_scope_lease_epoch});scopeRetired=retirement.ok,retirement.ok||(scopeRetirementError=retirement.reason)}await releaseAcquired(lock),scopeRetirementError!==null&&deps.errorLog(`The index scope was not retired: ${scopeRetirementError}. Retry with \`conduct-epic retire ${checkpoint.epic_key}\`.`);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,scope_retired:scopeRetired,counters:{...checkpoint.counters},needs_human:checkpoint.needs_human,tickets:checkpoint.tickets.map(ticket=>({key:ticket.key,status:ticket.status,pr_number:ticket.pr_number,counters:{...ticket.counters}}))},humanLines=[`Finished ${checkpoint.epic_key} (${checkpoint.epic_branch})`,`index scope: ${scopeRetired===null?"none declared":scopeRetired?"retired (retention clock started; nothing deleted)":"NOT retired \u2014 see the error above"}`,`iterations: ${checkpoint.counters.iterations} merges: ${checkpoint.counters.merges}`,...checkpoint.tickets.map(ticket=>` ${ticket.key} ${ticket.status} PR ${ticket.pr_number??"-"} spawned ${ticket.counters.sessions_spawned} plans ${ticket.counters.plan_generations_observed} merges ${ticket.counters.merge_attempts}`),`needs_human: ${checkpoint.needs_human===null?"none":checkpoint.needs_human.reason}`];return emitSuccess(deps,options.json,summary,humanLines)}async function resolveLifecycleScope(deps,options,checkpointPath){if(options.scope!==void 0)return{ok:!0,scopeId:options.scope,checkpoint:null,leaseEpoch:null};let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return{ok:!1,reason:`No checkpoint exists at ${checkpointPath}. Pass --scope <id> to target a scope directly (see \`conduct-epic status\`).`};if(read.kind!=="ok")return{ok:!1,reason:read.error};let scopeId=read.checkpoint.index_scope_id;return typeof scopeId!="string"||scopeId.length===0?{ok:!1,reason:`${options.epicKey} declares no index scope.`}:{ok:!0,scopeId,checkpoint:read.checkpoint,leaseEpoch:read.checkpoint.index_scope_lease_epoch}}async function persistScopeLeaseEpoch(deps,checkpointPath,checkpoint,leaseEpoch){if(checkpoint===null||leaseEpoch===null||checkpoint.index_scope_lease_epoch===leaseEpoch)return;let next={...checkpoint,index_scope_lease_epoch:leaseEpoch,updated_at:deps.now().toISOString()};await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"})}async function runConductEpicRecover(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let recovered=await recoverIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);return recovered.ok?(await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,recovered.value.lease_epoch),emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:recovered.value.scope_id,lifecycle_state:recovered.value.lifecycle_state,lease_epoch:recovered.value.lease_epoch,lease_expires_at:recovered.value.lease_expires_at},[`Recovered index scope ${recovered.value.scope_id}.`,` lifecycle: ${recovered.value.lifecycle_state??"unknown"}`,` lease epoch: ${recovered.value.lease_epoch??"unknown"} (previous owners are now fenced)`,` lease expires: ${recovered.value.lease_expires_at??"unknown"}`])):emitFailure(deps,options.json,[`The index scope could not be recovered: ${recovered.error}`])}async function runConductEpicRetire(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let outcome2=await retireScopeWithEpoch(deps,options,accessProbe.access,target);return outcome2.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:outcome2.state.scope_id,lifecycle_state:outcome2.state.lifecycle_state,lease_epoch:outcome2.state.lease_epoch,already_retired:outcome2.state.already_retired},[outcome2.state.already_retired?`Index scope ${outcome2.state.scope_id} was already retired; retention clock unchanged.`:`Retired index scope ${outcome2.state.scope_id}. Nothing was deleted.`," The scope stays readable for post-mortem for the whole retention window."]):emitFailure(deps,options.json,[outcome2.reason])}async function retireScopeWithEpoch(deps,options,access2,target){let epoch=target.leaseEpoch;if(epoch===null){let current=await lookupScopeEpoch(deps,access2,target.scopeId);if(current===null)return{ok:!1,reason:`The current fencing epoch for scope ${target.scopeId} could not be read.`};epoch=current}let retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl);if(!retired.ok){let current=await lookupScopeEpoch(deps,access2,target.scopeId);current!==null&&current!==epoch&&(epoch=current,retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl))}if(!retired.ok)return{ok:!1,reason:`The index scope could not be retired: ${retired.error}`};let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath);return await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,retired.value.lease_epoch),{ok:!0,state:retired.value}}async function lookupScopeEpoch(deps,access2,scopeId){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);return listing.ok?listing.value.scopes.find(scope=>scope.scope_id===scopeId)?.lease_epoch??null:null}async function runConductEpicReclaim(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let scheduled=await reclaimIndexScope(accessProbe.access,{scopeId:target.scopeId,overrideRetention:options.overrideRetention},deps.fetchImpl);if(!scheduled.ok){let blockers=scheduled.blockers??[],reasons=[`The index scope could not be reclaimed: ${scheduled.error}`];return blockers.length>0&&reasons.push(` blocked by: ${blockers.join(", ")}`),emitFailure(deps,options.json,reasons,{epic_key:options.epicKey,scope_id:target.scopeId,blockers})}return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:scheduled.value.scope_id??target.scopeId,scheduled:scheduled.value.scheduled},[`Scheduled reclamation of index scope ${scheduled.value.scope_id??target.scopeId}.`," This is SCHEDULED, not done \u2014 the teardown waits out Pinecone's"," consistency window. Run `conduct-epic status --json` to see it reach"," `reclaimed`."])}async function runConductEpicCli(argv,overrides={}){let deps={...createDefaultConductEpicDeps(),...overrides},parsed=parseConductEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),1;let options=parsed.options;switch(options.verb){case"init":return runConductEpicInit(deps,options);case"status":return runConductEpicStatus(deps,options);case"checkpoint-set":return runConductEpicCheckpointSet(deps,options);case"spawn":return runConductEpicSpawn(deps,options);case"finish":return runConductEpicFinish(deps,options);case"recover":return runConductEpicRecover(deps,options);case"retire":return runConductEpicRetire(deps,options);case"reclaim":return runConductEpicReclaim(deps,options)}}init_bridge_api_client();init_mcp_identity();var EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/;var DRIVE_EPIC_PREFERRED_CONDUCTOR="v2",V2_READINESS_REQUIREMENTS=[{id:"supervisor_setup",describe:"supervisor setup stored for this repository",satisfied:r=>r.supervisor.setup_present},{id:"supervisor_config",describe:"supervisor configuration stored for this repository",satisfied:r=>r.supervisor.config_present},{id:"github_credentials",describe:"GitHub App credentials that resolve completely",satisfied:r=>r.github.credentials_complete},{id:"reconciler_live",describe:"a reconciler that is ticking and not stale",satisfied:r=>r.reconciler.liveness_readable&&!r.reconciler.stale},{id:"executor_live",describe:"an executor provisioned and reporting ready",satisfied:r=>r.executor.liveness_readable&&r.executor.ready===!0}];function getDriveEpicUsage(){return["Usage: mcp-server drive-epic [options] <EPIC>","","Drives one epic with the conductor this project can actually run. Reads","conductor readiness from Bridge API and routes to exactly one path \u2014 it","never asks you to choose.","","Arguments:"," <EPIC> Jira epic key, matches [A-Z]+-[0-9]+ (e.g. BAPI-885)","","Options:"," --plan-file <path> Plan DAG sidecar. When supplied and the v2 path is"," selected, drive-epic runs that bootstrap directly"," instead of printing the command to run."," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," -h, --help Show this help"].join(`
7365
+ `).map(line=>line.trim()).filter(line=>line.length>0);if(lines[0]!==resolvedVersion)return{kind:"unavailable",reason:"unreadable_output"};let commitLine=lines.slice(1).find(line=>line.startsWith("commit:"));if(commitLine===void 0)return{kind:"unavailable",reason:"unreadable_output"};if(commitLine===`commit: ${PUBLISHED_IDENTITY_UNKNOWN}`)return{kind:"unavailable",reason:"identity_unknown"};let match=PUBLISHED_IDENTITY_PATTERN.exec(commitLine);return match===null?{kind:"unavailable",reason:"unreadable_output"}:{kind:"known",version:resolvedVersion,commit:match[1],dirty:match[2]!==void 0}}async function evaluatePublishGate(deps,expectedCommitSha){if(expectedCommitSha===null)return{failures:[],advisories:["advisory: the publish gate was not evaluated because no canonical indexed commit is available to check against."]};let expected=normalizeCommitSha(expectedCommitSha);if(expected===null)return{failures:["The publish gate cannot be evaluated: the expected commit is not a full 40-character SHA. Refusing rather than comparing an arbitrary prefix."],advisories:[]};let identity=await readPublishedBuildIdentity(deps);if(identity.kind==="unavailable")return{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 ${describePublishedIdentityReason(identity.reason)}. Initialization is continuing; the published ${MCP_PACKAGE_NAME} build was NOT confirmed to contain ${expected}.`]};if(identity.dirty)return{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} reports build commit ${identity.commit}-dirty. A dirty build carries content that no commit represents, so it cannot be verified to contain ${expected}. Publish a build from a clean checkout.`],advisories:[]};if((await git(deps,["rev-parse","--verify","--quiet",`${identity.commit}^{commit}`])).exitCode!==0)return{failures:[],advisories:[`advisory: the published ${MCP_PACKAGE_NAME}@${identity.version} build commit ${identity.commit} is not present in this checkout, so the publish gate could not be verified. Initialization is continuing; fetch origin and confirm that build contains ${expected}.`]};let contains=await git(deps,["merge-base","--is-ancestor",expected,identity.commit]);return contains.exitCode===0?{failures:[],advisories:[]}:contains.exitCode===1?{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} was built from ${identity.commit}, which does not contain ${expected} \u2014 the canonical indexed commit this epic is cut at. Publish a build containing that commit before initializing.`],advisories:[]}:{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 the ancestry of published build commit ${identity.commit} could not be determined locally. Initialization is continuing.`]}}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],advisories=[],epicBranch=epicBranchFor(options.epicKey),pendingSupervisorConfig=null;try{(await deps.runGh(["auth","status"],{cwd:deps.cwd})).ok||failures.push("gh is not authenticated. Run `gh auth login`.")}catch{failures.push("gh could not be run. Install the GitHub CLI and run `gh auth login`.")}let worktrunk=resolveWorktrunkBinary(deps.platform,deps.env);try{(await deps.runCommand(worktrunk,["--version"],{cwd:deps.cwd})).exitCode!==0&&failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}catch{failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}let accessResult=await resolveAccess(deps),access2=accessResult.ok?accessResult.access:null;accessResult.ok||failures.push(accessResult.error);let baseBranch=options.baseBranch??null,baseSha=null,cutCommitSha=null,epicBranchAlreadyAtCut=!1;if(access2!==null){let readiness=await getConductorReadiness(access2,deps.fetchImpl);if(!readiness.ok)failures.push(`Conductor readiness could not be read: ${readiness.error}`);else if(!readiness.value.supervisor.auto_merge_enabled){let current=await getEffectiveSupervisorConfig(access2,deps.fetchImpl);if(!current.ok)failures.push(`auto_merge_enabled is not set and the effective supervisor config could not be read: ${current.error}`);else{let body={teardown_enabled:current.value.teardown_enabled,auto_rereview_enabled:current.value.auto_rereview_enabled,max_remediation_attempts:current.value.max_remediation_attempts,max_remediation_no_progress_attempts:current.value.max_remediation_no_progress_attempts,worker_liveness_window_seconds:current.value.worker_liveness_window_seconds,auto_merge_enabled:!0,merge_approval_required:current.value.merge_approval_required};options.dryRun?announcements.push(`announced: would enable auto_merge_enabled on the ${access2.repoName} project defaults (--dry-run: not sent).`):pendingSupervisorConfig=body}}let resolved=await resolveCiChecks(access2,void 0,deps.fetchImpl),resolvedRequired=requiredCheckNamesFromResolve(resolved.ok?resolved.value:null);readiness.ok&&readiness.value.supervisor.required_checks_empty&&(resolved.ok?resolvedRequired.length===0&&failures.push("No required CI checks are configured and resolve-ci-checks returned none. A done gate with an empty required set passes vacuously."):failures.push(`No required CI checks are configured and resolve-ci-checks failed: ${resolved.error}`));let runState=await getEpicRunState(access2,options.epicKey,deps.fetchImpl);if(runState.ok){let status=runState.value.epic_run?.status;status!=="done"&&status!=="abandoned"&&failures.push(`A server-side epic run for ${options.epicKey} is ${String(status)}. conduct-epic will not drive an epic the v2 reconciler is already conducting.`)}else runState.status!==404&&failures.push(`The epic-run state for ${options.epicKey} could not be read: ${runState.error}`);let parseStatus=await getParseStatus(access2,deps.fetchImpl);if(!parseStatus.ok)failures.push(`The canonical parse status could not be read: ${parseStatus.error}`);else if(parseStatus.value.status!=="succeeded")failures.push(`The canonical index for ${access2.repoName} has no successful parse (status: ${String(parseStatus.value.status)}). Parse the repository first.`);else{let indexed=normalizeCommitSha(parseStatus.value.indexed_commit_sha);indexed===null?failures.push(`The canonical index for ${access2.repoName} published no commit for its last successful parse, so there is no commit to cut at. Parse the repository first.`):cutCommitSha=indexed}if(baseBranch===null){let configured=await getConfigFieldBaseBranch(access2,deps.fetchImpl);configured.ok&&configured.value.base_branch!==null&&(baseBranch=configured.value.base_branch)}}baseBranch===null&&(baseBranch="main");let branchReason=validateBranchName(baseBranch);if(branchReason)failures.push(`The resolved base branch is unusable: ${branchReason}`);else{(await git(deps,["fetch","origin",baseBranch])).exitCode!==0&&failures.push(`git fetch origin ${baseBranch} failed.`);let tip=await git(deps,["rev-parse",`refs/remotes/origin/${baseBranch}`]);baseSha=tip.exitCode===0?firstOutputLine(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`),cutCommitSha!==null&&(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(await git(deps,["fetch","origin",cutCommitSha]),(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(failures.push(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2?.repoName??"the repository"}.`),cutCommitSha=null));let publishGate=await evaluatePublishGate(deps,cutCommitSha);failures.push(...publishGate.failures),advisories.push(...publishGate.advisories);let existing=await git(deps,["ls-remote","--heads","origin",`refs/heads/${epicBranch}`]);if(existing.exitCode!==0)failures.push(`git ls-remote could not read origin/${epicBranch}.`);else{let existingSha=lsRemoteSha(existing);existingSha!==null&&(cutCommitSha!==null&&existingSha===cutCommitSha?epicBranchAlreadyAtCut=!0:failures.push(`origin/${epicBranch} already exists at ${existingSha}, which is not the canonical indexed commit${cutCommitSha?` ${cutCommitSha}`:""}. Delete it or finish the previous run before re-initializing.`))}}let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));return lockState.kind==="live-foreign"?failures.push(`The epic lock is held by live process ${lockState.owner.owner_pid} on ${lockState.owner.host}.`):lockState.kind==="remote-host"?failures.push(`The epic lock was taken on host ${lockState.owner.host} and cannot be recovered here.`):lockState.kind==="unknown"&&failures.push(`The epic lock is unusable: ${lockState.reason}`),{failures,announcements,advisories,access:access2,baseBranch,baseSha,cutCommitSha,epicBranchAlreadyAtCut,pendingSupervisorConfig}}function requiredCheckNamesFromResolve(value){if(!isRecord4(value))return[];let detail=value.detail;if(!isRecord4(detail))return[];let checks=detail.checks;if(!Array.isArray(checks))return[];let names=[];for(let check of checks){if(!isRecord4(check)||check.required!==!0)continue;let name=check.name;typeof name=="string"&&name.trim().length>0&&names.push(name.trim())}return names}async function driveIndexScopeBootstrap(deps,access2,scopeId,options){let scheduled=await bootstrapIndexScope(access2,{scopeId},deps.fetchImpl);if(!scheduled.ok)return{ok:!1,failures:[`The index scope could not be seeded: ${scheduled.error}`]};let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),lastState="unknown";for(let poll=0;poll<SCOPE_BOOTSTRAP_MAX_POLLS;poll+=1){await sleep3(SCOPE_BOOTSTRAP_POLL_INTERVAL_MS);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl);if(!status.ok){lastState=`unreadable (${status.error})`;continue}if(lastState=status.value.lifecycle_state,status.value.lifecycle_state==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{ok:!0,failures:[]}:{ok:!1,failures:[`Index scope ${scopeId} reports ready but its indexed commit (${status.value.indexed_commit_sha??"none"}) is not the cut commit (${status.value.cut_commit_sha??"none"}).`]};if(status.value.lifecycle_state==="failed"){let reason=status.value.last_error??"unknown";return reason==="canonical_index_advanced"?{ok:!1,failures:[`The canonical index advanced before the seed could run, so the scope was not seeded. Delete origin/${epicBranchFor(options.epicKey)} and re-run init to cut at the newer commit.`]}:{ok:!1,failures:[`Index scope ${scopeId} failed verification (${reason}). The epic branch and its recorded cut are intact; re-run init to re-drive verification.`]}}}return{ok:!1,failures:[`Index scope ${scopeId} did not become ready within the bootstrap window (last observed state: ${lastState}). Re-run init to resume verification.`]}}async function runConductEpicInit(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),epicBranch=epicBranchFor(options.epicKey);if((await readConductEpicCheckpoint(checkpointPath,deps.fs)).kind!=="missing")return emitFailure(deps,options.json,[`${options.epicKey} is already initialized: a checkpoint exists at ${checkpointPath}. Nothing was changed.`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let preflight=await collectConductEpicInitPreflight(deps,options);for(let line of preflight.advisories)deps.errorLog(line);if(preflight.failures.length>0){for(let line of preflight.announcements)deps.errorLog(line);return emitFailure(deps,options.json,preflight.failures,{epic_key:options.epicKey,checkpoint_path:checkpointPath})}let access2=preflight.access;if(access2===null||preflight.baseBranch===null||preflight.baseSha===null||preflight.cutCommitSha===null)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let cutCommitSha=preflight.cutCommitSha,announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`cut: ${cutCommitSha} (canonical indexed commit)`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtCut?" (already at the cut commit)":""}`,`tickets: ${options.tickets.join(", ")}`,`checkpoint: ${checkpointPath}`,...announcements];if(options.dryRun)return emitSuccess(deps,options.json,{ok:!0,dry_run:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, cut, seeded, repointed, or written):",...describePlan()]);if(preflight.pendingSupervisorConfig!==null){let put=await putSupervisorConfigDefaults(access2,preflight.pendingSupervisorConfig,deps.fetchImpl);if(!put.ok)return emitFailure(deps,options.json,[`auto_merge_enabled could not be enabled: ${put.error}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});announcements.push(`announced: auto_merge_enabled was OFF and has been enabled on the ${access2.repoName} project defaults.`)}let cutOutcome=await performExactIndexScopeCut(cutProtocolDeps2(deps),access2,{featureBranch:epicBranch,baseBranch:preflight.baseBranch,candidateCommitSha:cutCommitSha});if(!cutOutcome.ok)return emitFailure(deps,options.json,cutOutcome.failures,cutOutcome.kind==="begin_refused"?{epic_key:options.epicKey,checkpoint_path:checkpointPath}:{epic_key:options.epicKey});let cut=cutOutcome.lease,scopeReady=await driveIndexScopeBootstrap(deps,access2,cut.scope_id,options);if(!scopeReady.ok)return emitFailure(deps,options.json,scopeReady.failures,{epic_key:options.epicKey,scope_id:cut.scope_id,checkpoint_path:checkpointPath});announcements.push(`announced: index scope ${cut.scope_id} is ready at ${cut.cut_commit_sha}.`);let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,indexScopeId:cut.scope_id,ticketKeys:options.tickets,now:deps.now().toISOString(),lock:{owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt}}),written=await writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error],{epic_key:options.epicKey});let lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),request,buildConductEpicLockSeams(deps));return lock.acquired?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,scope_id:cut.scope_id,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),announcements},["Initialized:",...describePlan()]):emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath})}async function runConductEpicStatus(deps,options){let accessProbe=await resolveAccess(deps),repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,checkpoint_path:checkpointPath,checkpoint_exists:!1});if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let checkpoint=read.checkpoint,now=deps.now(),probeErrors=[],ticket=inFlightTicket(checkpoint),allDone=ticket===null,discoveredBranch=ticket?.branch??null,worktreePath=null,worktreeExists=!1;if(ticket!==null)try{let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)probeErrors.push({probe:"worktree",reason:"git worktree list failed"});else{let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticket.key,ticket.branch);if(found!==null){discoveredBranch=found.branch,worktreePath=found.path;try{await deps.fs.stat(found.path),worktreeExists=!0}catch{worktreeExists=!1}}}}catch{probeErrors.push({probe:"worktree",reason:"git worktree list could not be run"})}let branchHead=null,workerCommits=0;if(discoveredBranch!==null){try{let remote=await git(deps,["ls-remote","--heads","origin",discoveredBranch]);remote.exitCode!==0?probeErrors.push({probe:"branch_head",reason:"git ls-remote failed"}):branchHead=lsRemoteSha(remote)}catch{probeErrors.push({probe:"branch_head",reason:"git ls-remote could not be run"})}if(branchHead!==null)try{await git(deps,["fetch","origin"]);let counted=await git(deps,["rev-list","--count",`origin/${checkpoint.epic_branch}..origin/${discoveredBranch}`]);if(counted.exitCode===0){let parsedCount=Number.parseInt(firstOutputLine(counted)??"",10);workerCommits=Number.isSafeInteger(parsedCount)&&parsedCount>=0?parsedCount:0}else probeErrors.push({probe:"worker_commits",reason:"git rev-list failed"})}catch{probeErrors.push({probe:"worker_commits",reason:"git rev-list could not be run"})}}let pr=null;if(discoveredBranch!==null){let probe=await discoverConductEpicPrState(discoveredBranch,{runGh:deps.runGh,cwd:deps.cwd});probe.kind==="ok"?pr=probe.pr:probe.kind==="error"&&probeErrors.push({probe:"pr",reason:probe.reason})}let mergedExternally=pr?.state==="MERGED"&&ticket!==null&&ticket.status!=="merged"&&ticket.status!=="done",access2=accessProbe.ok?accessProbe.access:null;accessProbe.ok||probeErrors.push({probe:"credentials",reason:accessProbe.error});let doneGateRequired=null,reviewOptedIn=!1,reviewSource=null,reviewDisposition=null,reviewConfigInvalid=!1;if(access2!==null){let setup=await getEffectiveSupervisorSetup(access2,deps.fetchImpl);if(!setup.ok)probeErrors.push({probe:"supervisor_setup",reason:setup.error});else{let gate=parseDoneGateConfig(setup.value.done_gate_config);(gate.reason==="malformed"||gate.reason.startsWith("invalid:"))&&(reviewConfigInvalid=!0,reviewOptedIn=!0,reviewSource=null,reviewDisposition=null);for(let condition of gate.conditions)condition.type==="required_ci_checks_green"?doneGateRequired=[...condition.required_checks]:condition.type==="review_state"&&(reviewOptedIn=!0,reviewSource=condition.source,reviewDisposition=condition.verdictless_disposition??null)}}let ci=access2===null||pr?.head_sha==null?null:await collectCiFacts(deps,access2,pr.head_sha,doneGateRequired,checkpoint.ci_last_poll,probeErrors),reviewPolicyFacts={verdictless_disposition:reviewDisposition,verdictless_ceiling:CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING,config_invalid:reviewConfigInvalid},review={opted_in:reviewOptedIn,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts};if(reviewConfigInvalid)review={opted_in:!0,source:null,available:!1,verdict:null,head_sha:null,...reviewPolicyFacts};else if(access2!==null&&reviewOptedIn&&pr?.number!=null){let status=await getPrReviewStatus(access2,pr.number,deps.fetchImpl);status.ok?review={...normalizeReviewStatus(status.value,reviewOptedIn,reviewSource),...reviewPolicyFacts}:(probeErrors.push({probe:"review",reason:status.error}),review={opted_in:!0,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts})}let parse=null;if(access2!==null){let parseStatus=await getParseStatus(access2,deps.fetchImpl);parseStatus.ok?parse=normalizeParseStatus(parseStatus.value):probeErrors.push({probe:"parse",reason:parseStatus.error})}let scope=null,declaredScopeId=typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0?checkpoint.index_scope_id:null;if(access2!==null&&declaredScopeId!==null){let scopeStatus=await getIndexScopeStatus(access2,declaredScopeId,deps.fetchImpl);scopeStatus.ok?scope={scope_id:scopeStatus.value.scope_id,lifecycle_state:scopeStatus.value.lifecycle_state,freshness_status:scopeStatus.value.freshness_status??"unavailable",blocked_reason:scopeStatus.value.blocked_reason,required_commit_sha:scopeStatus.value.required_commit_sha,indexed_commit_sha:scopeStatus.value.indexed_commit_sha,last_error:scopeStatus.value.last_error}:(probeErrors.push({probe:"scope",reason:scopeStatus.error}),scope={scope_id:declaredScopeId,lifecycle_state:null,freshness_status:"unavailable",blocked_reason:null,required_commit_sha:null,indexed_commit_sha:null,last_error:null})}let scopes=[],retentionSeconds=null,nextLeaseEpoch=checkpoint.index_scope_lease_epoch;if(access2!==null){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);if(listing.ok?(scopes=listing.value.scopes,retentionSeconds=listing.value.retention_seconds):probeErrors.push({probe:"scopes",reason:listing.error}),declaredScopeId!==null){let entry=scopes.find(scope2=>scope2.scope_id===declaredScopeId),epoch=entry?.lease_epoch??checkpoint.index_scope_lease_epoch;if(epoch!==null&&entry?.recoverable!==!1){let beat=await heartbeatIndexScope(access2,{scopeId:declaredScopeId,leaseEpoch:epoch},deps.fetchImpl);beat.ok?nextLeaseEpoch=beat.value.lease_epoch:probeErrors.push({probe:"scope_heartbeat",reason:beat.error})}}}let lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps)),lock={held_by_me:lockState.kind==="owned",owner_pid:"owner"in lockState?lockState.owner.owner_pid:null,host:"owner"in lockState?lockState.owner.host:null,alive:lockState.kind==="owned"||lockState.kind==="live-foreign"?!0:lockState.kind==="dead-local"?!1:null},lastSeenHead=ticket?.last_seen_head??null,lastStateChangeAt=ticket?.last_state_change_at??null,leaseEpochChanged=nextLeaseEpoch!==checkpoint.index_scope_lease_epoch;if(ticket!==null){let next={...checkpoint,tickets:[...checkpoint.tickets]},index=next.tickets.findIndex(entry=>entry.key===ticket.key),dirty=!1,updatedTicket={...next.tickets[index]};if(updatedTicket.branch===null&&discoveredBranch!==null&&(updatedTicket.branch=discoveredBranch,dirty=!0),branchHead!==null&&branchHead!==updatedTicket.last_seen_head&&(updatedTicket.last_state_change_at=now.toISOString(),updatedTicket.last_seen_head=branchHead,lastSeenHead=branchHead,lastStateChangeAt=updatedTicket.last_state_change_at,dirty=!0),next.tickets[index]=updatedTicket,ci?.ci_last_poll&&(next.ci_last_poll=ci.ci_last_poll,dirty=!0),leaseEpochChanged&&(next.index_scope_lease_epoch=nextLeaseEpoch,dirty=!0),dirty){next.updated_at=now.toISOString();let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});written.ok||probeErrors.push({probe:"checkpoint_write",reason:written.error})}}let payload={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,checkpoint_path:checkpointPath,checkpoint_exists:!0,all_done:allDone,ticket:ticket===null?null:projectConductEpicTicketFacts(ticket,discoveredBranch),worktree_path:worktreePath,worktree_exists:worktreeExists,branch_head:branchHead,worker_commits_since_spawn:workerCommits,last_seen_head:lastSeenHead,last_state_change_at:lastStateChangeAt,stale_for_seconds:elapsedSeconds(lastStateChangeAt,now),pr,merged_externally:!!mergedExternally,ci:ci?.ci??null,review,parse,deadlines:{soft_seconds:checkpoint.deadlines.soft_seconds,hard_seconds:checkpoint.deadlines.hard_seconds,elapsed_since_spawn_seconds:elapsedSeconds(ticket?.spawned_at??null,now)},scope,scope_lease_epoch:nextLeaseEpoch,retention_seconds:retentionSeconds,scopes,lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload,[...renderScopeFreshnessLines(scope),...renderStrandedScopeLines(scopes,declaredScopeId)])}function renderStrandedScopeLines(scopes,ownScopeId){let stranded=scopes.filter(scope=>scope.scope_id!==ownScopeId&&scope.lifecycle_state!=="reclaimed"&&!scope.lease_valid);if(stranded.length===0)return[];let lines=[`${stranded.length} index scope(s) in this repository have no live lease:`];for(let scope of stranded){let action=scope.recoverable?"recoverable \u2014 `conduct-epic recover <EPIC> --scope "+scope.scope_id+"`":scope.retention_elapsed?"past retention \u2014 the sweep will reclaim it":`retained until ${scope.retention_deadline??"an unknown deadline"}`;lines.push(` ${scope.scope_id} ${scope.lifecycle_state??"unknown"} branch=${scope.feature_branch??"unknown"} ${action}`),scope.blockers.length>0&&lines.push(` blocked by: ${scope.blockers.join(", ")}`)}return lines}function renderScopeFreshnessLines(scope){if(scope===null)return[];let headline={fresh:"Index is fresh for this epic.",pending:"Waiting for index refresh.",blocked:"Index refresh is BLOCKED \u2014 this advance will not be indexed.",failed:"Index generation FAILED for this scope.",unavailable:"Index freshness is unavailable \u2014 treat as not fresh."},refusal={advance_blocked_base_merge:"the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",advance_blocked_unexpected_parent:"the merge commit does not descend directly from the head this scope pinned, so it is not a worker merge",advance_blocked_history_changed:"the pinned head is gone from the branch's history \u2014 a force-push or rewrite",advance_blocked_unverifiable:"the advance could not be verified at all, and doubt blocks rather than indexes"},lines=[headline[scope.freshness_status]??"Index freshness is unknown \u2014 treat as not fresh.",` lifecycle: ${scope.lifecycle_state??"unknown"}`,` Required commit: ${scope.required_commit_sha??"none"}`,` Indexed commit: ${scope.indexed_commit_sha??"none"}`];return scope.blocked_reason!==null?(lines.push(` Reason: ${scope.blocked_reason} \u2014 ${refusal[scope.blocked_reason]??"the server refused this branch advance"}`),lines.push(" A human must resolve the branch before the epic can continue.")):scope.freshness_status==="failed"&&scope.last_error!==null&&lines.push(` Failure category: ${scope.last_error}`),lines}function projectConductEpicTicketFacts(ticket,discoveredBranch){return{key:ticket.key,status:ticket.status,branch:discoveredBranch,pr_number:ticket.pr_number,spawned_at:ticket.spawned_at,parse_requested_at:ticket.parse_requested_at,parse_requested_for_sha:ticket.parse_requested_for_sha,review_verdictless_observations:ticket.review_verdictless_observations,review_verdictless_for_sha:ticket.review_verdictless_for_sha,respawns:ticket.respawns,conflict_attempts:ticket.conflict_attempts,counters:{...ticket.counters},journal:[...ticket.journal]}}async function collectCiFacts(deps,access2,headSha,doneGateRequired,previous,probeErrors){let resolvedValue=null,resolveCalled=!1,ensureResolved=async()=>{if(resolveCalled)return;resolveCalled=!0;let resolved=await resolveCiChecks(access2,headSha,deps.fetchImpl);resolved.ok?resolvedValue=resolved.value:probeErrors.push({probe:"ci_resolve",reason:resolved.error})},polled=await pollCiChecks(access2,headSha,deps.fetchImpl);if(!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1){if(await ensureResolved(),polled=await pollCiChecks(access2,headSha,deps.fetchImpl),!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1)return probeErrors.push({probe:"ci",reason:"CI checks are unavailable after resolve-ci-checks and a second poll"}),null}let detail=isRecord4(polled.value)?polled.value.detail:null,rawChecks=isRecord4(detail)&&Array.isArray(detail.checks)?detail.checks:[],checks=rawChecks.filter(isRecord4).map(check=>({name:typeof check.name=="string"?check.name:"",status:check.status??null,conclusion:check.conclusion??null,required:check.required!==!1})),required=doneGateRequired;required===null&&(await ensureResolved(),required=requiredCheckNamesFromResolve(resolvedValue));let requiredSorted=[...new Set(required)].sort(),complete=requiredSorted.length>0&&requiredSorted.every(name=>{let raw=rawChecks.filter(isRecord4).find(check=>check.name===name);return raw!==void 0&&raw.complete===!0&&raw.passed===!0}),fingerprint=JSON.stringify(checks.filter(check=>requiredSorted.includes(check.name)).map(check=>[check.name,check.status??null,check.conclusion??null]).sort((a,b)=>String(a[0]).localeCompare(String(b[0])))),stable=previous!==null&&previous.head_sha===headSha&&previous.results_fingerprint===fingerprint&&previous.required.length===requiredSorted.length&&previous.required.every((name,i)=>name===requiredSorted[i]);return{ci:{required:requiredSorted,complete,stable_across_two_polls:stable,head_sha:headSha,checks},ci_last_poll:{head_sha:headSha,required:requiredSorted,results_fingerprint:fingerprint,at:deps.now().toISOString()}}}function normalizeReviewStatus(value,optedIn,source){let available=isRecord4(value)&&value.available===!0,detail=isRecord4(value)&&isRecord4(value.detail)?value.detail:null,verdict=null;if(detail!==null){let sticky=detail.sticky_verdict,native=detail.review_decision;typeof sticky=="string"&&sticky.trim().length>0?verdict=normalizeVerdict(sticky):typeof native=="string"&&native.trim().length>0?verdict=normalizeVerdict(native):verdict="unknown"}return{opted_in:optedIn,source,available:isRecord4(value)?!!value.available:null,verdict:available||verdict!==null?verdict:null,head_sha:detail!==null&&typeof detail.head_sha=="string"?detail.head_sha:null}}function normalizeVerdict(raw){let value=raw.trim().toLowerCase();return value==="approved"?"approved":value==="changes_requested"?"changes_requested":"unknown"}function normalizeParseStatus(value){let known=["idle","queued","in_progress","succeeded","failed"],raw=isRecord4(value)?value.status:null,status=typeof raw=="string"&&known.includes(raw)?raw:null,optionalText=key=>{let field=isRecord4(value)?value[key]:null;if(typeof field!="string")return null;let trimmed=field.trim();return trimmed.length===0?null:trimmed};return{status,terminal:status==="succeeded"||status==="failed",started_at:optionalText("started_at"),finished_at:optionalText("finished_at")}}async function runConductEpicCheckpointSet(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets (${checkpoint.tickets.map(t=>t.key).join(", ")}).`]);let next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))},now=deps.now().toISOString(),statusBefore=next.tickets[index].status;for(let assignment of options.fields){let applied=applyFieldAssignment(next,index,assignment);if(applied!==null)return emitFailure(deps,options.json,[applied])}next.tickets[index].status!==statusBefore&&(next.tickets[index].last_state_change_at=now),options.journal!==void 0&&(next.tickets[index]=appendTicketJournal(next.tickets[index],options.journal)),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error]);let changed=options.fields.map(f=>f.name);return options.journal!==void 0&&changed.push("journal"),emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,updated_fields:changed,checkpoint_path:checkpointPath},[`Updated ${ticketKey}: ${changed.join(", ")}`])}finally{await releaseAcquired(lock)}}async function releaseAcquired(lock){await lock.release()}function applyFieldAssignment(checkpoint,ticketIndex,assignment){let{name,value}=assignment,ticket=checkpoint.tickets[ticketIndex];switch(name){case"status":return CONDUCT_EPIC_TICKET_STATUSES.includes(value)?(ticket.status=value,null):`Invalid status '${value}'. Expected one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`;case"branch":{if(value==="null")return ticket.branch=null,null;let reason=validateBranchName(value);return reason?`Invalid branch: ${reason}`:(ticket.branch=value,null)}case"pr_number":{if(value==="null")return ticket.pr_number=null,null;let parsed=parseIntegerField(value);return parsed===null||parsed<=0?"pr_number must be a positive integer or null.":(ticket.pr_number=parsed,null)}case"spawned_at":return value==="null"?(ticket.spawned_at=null,null):value.trim().length===0?"spawned_at must be a non-empty timestamp or null.":(ticket.spawned_at=value,null);case"parse_requested_at":case"parse_requested_for_sha":case"review_verdictless_for_sha":return value==="null"?(ticket[name]=null,null):value.trim().length===0?`${name} must be a non-empty string or null.`:(ticket[name]=value,null);case"respawns":case"conflict_attempts":case"review_verdictless_observations":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(ticket[name]=parsed,null)}case"counters.sessions_spawned":case"counters.plan_generations_observed":case"counters.merge_attempts":{let parsed=parseIntegerField(value);if(parsed===null)return`${name} must be a non-negative integer.`;let key=name.slice(9);return ticket.counters[key]=parsed,null}case"counters.iterations":case"counters.merges":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(checkpoint.counters[name.slice(9)]=parsed,null)}case"needs_human":{if(value==="null")return checkpoint.needs_human=null,null;let parsed;try{parsed=JSON.parse(value)}catch{return"needs_human must be JSON null or an object with reason, evidence, and at."}return parsed===null?(checkpoint.needs_human=null,null):!isRecord4(parsed)||typeof parsed.reason!="string"||typeof parsed.evidence!="string"||typeof parsed.at!="string"?"needs_human must be JSON null or an object with string reason, evidence, and at.":(checkpoint.needs_human={reason:parsed.reason,evidence:parsed.evidence,at:parsed.at},null)}default:return`Unknown field '${name}'. Ticket fields: ${TICKET_FIELDS.join(", ")}. Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`}}function parseIntegerField(value){if(!/^\d+$/.test(value.trim()))return null;let parsed=Number.parseInt(value.trim(),10);return Number.isSafeInteger(parsed)&&parsed>=0?parsed:null}async function runConductEpicSpawn(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets.`]);let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)return emitFailure(deps,options.json,["git worktree list failed; the ticket worktree could not be resolved."]);let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticketKey,checkpoint.tickets[index].branch);if(found===null)return emitFailure(deps,options.json,[`No worktree was found for ${ticketKey}. Expected a worktree on the ticket's branch, feature/${ticketKey}, or feature/${ticketKey}-<slug>.`]);try{await deps.fs.stat(found.path)}catch{return emitFailure(deps,options.json,[`The worktree path for ${ticketKey} is not accessible.`])}let prompt;try{prompt=await deps.fs.readFile(options.promptFile)}catch{return emitFailure(deps,options.json,[`The prompt file '${options.promptFile}' could not be read.`])}let indexScope;try{indexScope=validateOptionalIndexScope(checkpoint.index_scope_id)}catch{return emitFailure(deps,options.json,[INDEX_SCOPE_CONFIGURATION_ERROR])}let spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform,...indexScope===void 0?{}:{indexScope}},deps.spawnTab);if(!spawned.ok)return emitFailure(deps,options.json,[spawned.error]);let now=deps.now().toISOString(),next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))};next.tickets[index].branch=found.branch,next.tickets[index].counters.sessions_spawned+=1,next.tickets[index]=appendTicketJournal(next.tickets[index],`${now} spawned ${options.agent??"claude"} in ${found.branch}`),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});return written.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,branch:found.branch,worktree_path:found.path,sessions_spawned:next.tickets[index].counters.sessions_spawned},[`Spawned one agent tab for ${ticketKey} in ${found.path}`]):emitFailure(deps,options.json,[written.error])}finally{await releaseAcquired(lock)}}async function runConductEpicFinish(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let access2=accessProbe.access,checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);let scopeRetired=null,scopeRetirementError=null;if(typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0){let retirement=await retireScopeWithEpoch(deps,options,access2,{scopeId:checkpoint.index_scope_id,checkpoint,leaseEpoch:checkpoint.index_scope_lease_epoch});scopeRetired=retirement.ok,retirement.ok||(scopeRetirementError=retirement.reason)}await releaseAcquired(lock),scopeRetirementError!==null&&deps.errorLog(`The index scope was not retired: ${scopeRetirementError}. Retry with \`conduct-epic retire ${checkpoint.epic_key}\`.`);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,scope_retired:scopeRetired,counters:{...checkpoint.counters},needs_human:checkpoint.needs_human,tickets:checkpoint.tickets.map(ticket=>({key:ticket.key,status:ticket.status,pr_number:ticket.pr_number,counters:{...ticket.counters}}))},humanLines=[`Finished ${checkpoint.epic_key} (${checkpoint.epic_branch})`,`index scope: ${scopeRetired===null?"none declared":scopeRetired?"retired (retention clock started; nothing deleted)":"NOT retired \u2014 see the error above"}`,`iterations: ${checkpoint.counters.iterations} merges: ${checkpoint.counters.merges}`,...checkpoint.tickets.map(ticket=>` ${ticket.key} ${ticket.status} PR ${ticket.pr_number??"-"} spawned ${ticket.counters.sessions_spawned} plans ${ticket.counters.plan_generations_observed} merges ${ticket.counters.merge_attempts}`),`needs_human: ${checkpoint.needs_human===null?"none":checkpoint.needs_human.reason}`];return emitSuccess(deps,options.json,summary,humanLines)}async function resolveLifecycleScope(deps,options,checkpointPath){if(options.scope!==void 0)return{ok:!0,scopeId:options.scope,checkpoint:null,leaseEpoch:null};let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return{ok:!1,reason:`No checkpoint exists at ${checkpointPath}. Pass --scope <id> to target a scope directly (see \`conduct-epic status\`).`};if(read.kind!=="ok")return{ok:!1,reason:read.error};let scopeId=read.checkpoint.index_scope_id;return typeof scopeId!="string"||scopeId.length===0?{ok:!1,reason:`${options.epicKey} declares no index scope.`}:{ok:!0,scopeId,checkpoint:read.checkpoint,leaseEpoch:read.checkpoint.index_scope_lease_epoch}}async function persistScopeLeaseEpoch(deps,checkpointPath,checkpoint,leaseEpoch){if(checkpoint===null||leaseEpoch===null||checkpoint.index_scope_lease_epoch===leaseEpoch)return;let next={...checkpoint,index_scope_lease_epoch:leaseEpoch,updated_at:deps.now().toISOString()};await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"})}async function runConductEpicRecover(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let recovered=await recoverIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);return recovered.ok?(await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,recovered.value.lease_epoch),emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:recovered.value.scope_id,lifecycle_state:recovered.value.lifecycle_state,lease_epoch:recovered.value.lease_epoch,lease_expires_at:recovered.value.lease_expires_at},[`Recovered index scope ${recovered.value.scope_id}.`,` lifecycle: ${recovered.value.lifecycle_state??"unknown"}`,` lease epoch: ${recovered.value.lease_epoch??"unknown"} (previous owners are now fenced)`,` lease expires: ${recovered.value.lease_expires_at??"unknown"}`])):emitFailure(deps,options.json,[`The index scope could not be recovered: ${recovered.error}`])}async function runConductEpicRetire(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let outcome2=await retireScopeWithEpoch(deps,options,accessProbe.access,target);return outcome2.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:outcome2.state.scope_id,lifecycle_state:outcome2.state.lifecycle_state,lease_epoch:outcome2.state.lease_epoch,already_retired:outcome2.state.already_retired},[outcome2.state.already_retired?`Index scope ${outcome2.state.scope_id} was already retired; retention clock unchanged.`:`Retired index scope ${outcome2.state.scope_id}. Nothing was deleted.`," The scope stays readable for post-mortem for the whole retention window."]):emitFailure(deps,options.json,[outcome2.reason])}async function retireScopeWithEpoch(deps,options,access2,target){let epoch=target.leaseEpoch;if(epoch===null){let current=await lookupScopeEpoch(deps,access2,target.scopeId);if(current===null)return{ok:!1,reason:`The current fencing epoch for scope ${target.scopeId} could not be read.`};epoch=current}let retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl);if(!retired.ok){let current=await lookupScopeEpoch(deps,access2,target.scopeId);current!==null&&current!==epoch&&(epoch=current,retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl))}if(!retired.ok)return{ok:!1,reason:`The index scope could not be retired: ${retired.error}`};let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath);return await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,retired.value.lease_epoch),{ok:!0,state:retired.value}}async function lookupScopeEpoch(deps,access2,scopeId){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);return listing.ok?listing.value.scopes.find(scope=>scope.scope_id===scopeId)?.lease_epoch??null:null}async function runConductEpicReclaim(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let scheduled=await reclaimIndexScope(accessProbe.access,{scopeId:target.scopeId,overrideRetention:options.overrideRetention},deps.fetchImpl);if(!scheduled.ok){let blockers=scheduled.blockers??[],reasons=[`The index scope could not be reclaimed: ${scheduled.error}`];return blockers.length>0&&reasons.push(` blocked by: ${blockers.join(", ")}`),emitFailure(deps,options.json,reasons,{epic_key:options.epicKey,scope_id:target.scopeId,blockers})}return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:scheduled.value.scope_id??target.scopeId,scheduled:scheduled.value.scheduled},[`Scheduled reclamation of index scope ${scheduled.value.scope_id??target.scopeId}.`," This is SCHEDULED, not done \u2014 the teardown waits out Pinecone's"," consistency window. Run `conduct-epic status --json` to see it reach"," `reclaimed`."])}async function runConductEpicCli(argv,overrides={}){let deps={...createDefaultConductEpicDeps(),...overrides},parsed=parseConductEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),1;let options=parsed.options;switch(options.verb){case"init":return runConductEpicInit(deps,options);case"status":return runConductEpicStatus(deps,options);case"checkpoint-set":return runConductEpicCheckpointSet(deps,options);case"spawn":return runConductEpicSpawn(deps,options);case"finish":return runConductEpicFinish(deps,options);case"recover":return runConductEpicRecover(deps,options);case"retire":return runConductEpicRetire(deps,options);case"reclaim":return runConductEpicReclaim(deps,options)}}init_bridge_api_client();init_mcp_identity();var EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/;var DRIVE_EPIC_PREFERRED_CONDUCTOR="v2",V2_READINESS_REQUIREMENTS=[{id:"supervisor_setup",describe:"supervisor setup stored for this repository",satisfied:r=>r.supervisor.setup_present},{id:"supervisor_config",describe:"supervisor configuration stored for this repository",satisfied:r=>r.supervisor.config_present},{id:"github_credentials",describe:"GitHub App credentials that resolve completely",satisfied:r=>r.github.credentials_complete},{id:"reconciler_live",describe:"a reconciler that is ticking and not stale",satisfied:r=>r.reconciler.liveness_readable&&!r.reconciler.stale},{id:"executor_live",describe:"an executor provisioned and reporting ready",satisfied:r=>r.executor.liveness_readable&&r.executor.ready===!0}];function getDriveEpicUsage(){return["Usage: mcp-server drive-epic [options] <EPIC>","","Drives one epic with the conductor this project can actually run. Reads","conductor readiness from Bridge API and routes to exactly one path \u2014 it","never asks you to choose.","","Arguments:"," <EPIC> Jira epic key, matches [A-Z]+-[0-9]+ (e.g. BAPI-885)","","Options:"," --plan-file <path> Plan DAG sidecar. When supplied and the v2 path is"," selected, drive-epic runs that bootstrap directly"," instead of printing the command to run."," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," -h, --help Show this help"].join(`
7366
7366
  `)}function parseDriveEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getDriveEpicUsage()};let positionals=[],planFile,repo;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--plan-file"||arg==="--repo"){let value=argv[i+1];if(value===void 0||value.startsWith("-"))return{status:"error",message:`${arg} requires a value.`};arg==="--plan-file"?planFile=value:repo=value,i++;continue}let eq=arg.match(/^(--plan-file|--repo)=(.*)$/);if(eq){let value=eq[2];if(value.trim().length===0)return{status:"error",message:`${eq[1]} requires a value.`};eq[1]==="--plan-file"?planFile=value:repo=value;continue}if(arg.startsWith("-"))return{status:"error",message:`Unsupported flag: ${arg}`};positionals.push(arg)}if(positionals.length===0)return{status:"error",message:"Missing required epic key."};if(positionals.length>1)return{status:"error",message:`Expected exactly one epic key, got ${positionals.length}: ${positionals.join(", ")}`};let epicKey=positionals[0];return EPIC_KEY_PATTERN.test(epicKey)?{status:"ok",options:{epicKey,...planFile?{planFile}:{},...repo?{repo}:{}}}:{status:"error",message:`Malformed epic key '${epicKey}'. Expected e.g. BAPI-885.`}}function selectConductor(readiness){let missing=V2_READINESS_REQUIREMENTS.filter(req=>!req.satisfied(readiness));return missing.length===0?{kind:"selected",conductor:DRIVE_EPIC_PREFERRED_CONDUCTOR,reason:"conductor readiness is green"}:{kind:"selected",conductor:"pilot",reason:`the engine path still needs ${missing.map(m=>m.describe).join("; ")}`}}function renderConductorHandoff(selection,epicKey){if(selection.conductor==="v2"){let invocation2=`npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;return{conductor:"v2",invocation:invocation2,lines:[`${epicKey}: ${selection.reason}, but no plan file was supplied.`,"","The engine path needs a plan DAG, and nothing derives one from an epic key:","the dependency edges and per-ticket touched_files it carries do not exist","anywhere else. Produce one, then re-run this command with it.","",` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`," sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json."," If the epic's tickets already exist, run emit-conductor-bundle"," finalize instead \u2014 it resolves the placeholder keys in an existing"," sidecar and attaches touched_files.","",` 2. ${invocation2}`,"",`That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,"something you invoke directly."]}}let invocation=`/loop 5m /conduct-epic ${epicKey}`;return{conductor:"pilot",invocation,lines:[`${epicKey}: ${selection.reason}. Drive it from an interactive session with:`,"",` ${invocation}`]}}var CONDUCTOR_INVOCATION_TOKENS=["setup-epic","conduct-epic"],SETUP_EPIC_SUBCOMMAND=CONDUCTOR_INVOCATION_TOKENS[0];function assertSingleConductorInvocation(text4){let named=CONDUCTOR_INVOCATION_TOKENS.filter(token=>text4.includes(token));if(named.length>1)throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). Exactly one path may ever be presented.`)}function createDefaultDriveEpicDeps(){return{resolveAccess:async repo=>{let result=await resolveConductorBridgeApiAccess(repo?{repoName:repo}:{});return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}},readReadiness:access2=>fetchConductorReadiness(access2,globalThis.fetch),runSetupEpic:argv=>runSetupEpicCli(argv),log:message=>console.log(message),errorLog:message=>console.error(message)}}async function runDriveEpicCli(argv,overrides={}){let deps={...createDefaultDriveEpicDeps(),...overrides},parsed=parseDriveEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),deps.errorLog(""),deps.errorLog(getDriveEpicUsage()),1;let{epicKey,planFile,repo}=parsed.options;try{let accessResult=await deps.resolveAccess(repo);if(!accessResult.ok)return escalate(deps,epicKey,`conductor readiness could not be read: ${accessResult.error}`);let readiness;try{readiness=await deps.readReadiness(accessResult.access)}catch(err){return escalate(deps,epicKey,`conductor readiness could not be read: ${safeDiagnosticMessage(err,"readiness request failed")}`)}let selection=selectConductor(readiness);if(selection.conductor==="v2"&&planFile){let setupArgv=["--epic-key",epicKey,"--plan-file",planFile,...repo?["--repo",repo]:[]];return await deps.runSetupEpic(setupArgv)}let text4=renderConductorHandoff(selection,epicKey).lines.join(`
7367
7367
  `);return assertSingleConductorInvocation(text4),deps.log(text4),0}catch(err){return deps.errorLog(`drive-epic failed: ${safeDiagnosticMessage(err,"unexpected error")}`),1}}function escalate(deps,epicKey,reason){let text4=[`Cannot determine which conductor owns ${epicKey}: ${reason}`,"","This is not a not-ready result \u2014 it is an unknown one, and starting the wrong","conductor on an epic wedges it permanently. Restore Bridge API access and run","drive-epic again, or ask an operator to resolve conductor readiness."].join(`
7368
7368
  `);return assertSingleConductorInvocation(text4),deps.errorLog(text4),1}import{randomUUID as randomUUID4}from"crypto";import path32 from"path";var PLANE_BUILD_REMEDIATION="cd mcp_server && npm run build",EXCLUDED_DIR_NAMES=new Set(["node_modules","build"]);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(path32.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat12=await deps.fs.stat(path32.join(dir,entry.name));(newest===null||stat12.mtimeMs>newest)&&(newest=stat12.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 packageRoot=path32.join(repoRoot,"mcp_server"),buildDir=path32.join(packageRoot,"build"),executorEntrypoint=path32.join(buildDir,"index.js"),srcDir=path32.join(packageRoot,"src"),buildMtime=await findBuildMtime(executorEntrypoint,deps);if(!buildMtime.ok)return{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}\``};let sourceMtime=await findNewestSourceMtime(srcDir,deps);return sourceMtime.ok?sourceMtime.mtimeMs!==null&&sourceMtime.mtimeMs>buildMtime.mtimeMs?{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}\``}:null:{check:"executor-build",severity:"blocking",message:`could not read mcp_server/src to compare against the build (${sourceMtime.error}) \u2014 run \`${PLANE_BUILD_REMEDIATION}\``}}var 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.`;function checkPlaneRuntimeEntrypoint(resolution){return resolution.ok?null:{check:"runtime-entrypoint",severity:"blocking",message:PLANE_RUNTIME_ENTRYPOINT_REFUSAL}}function sanitize(err){let code=err?.code;return typeof code=="string"?code:"unreadable"}import path34 from"path";import path33 from"path";var PLANE_ALEMBIC_REMEDIATION="alembic -c alembic.ini upgrade head",PLANE_ALEMBIC_UNVERIFIED_PREFIX="could not verify database migration head";function resolveRepositoryAlembicCommand(repoRoot,platform){return platform==="win32"?path33.join(repoRoot,".venv","Scripts","alembic.exe"):path33.join(repoRoot,".venv","bin","alembic")}var 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)"]);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");let heads=await deps.execFile(alembic,["-c","alembic.ini","heads"],{cwd:repoRoot});if(!heads.ok)return unverified(`\`alembic heads\` did not complete (${heads.error})`);let current=await deps.execFile(alembic,["-c","alembic.ini","current"],{cwd:repoRoot});if(!current.ok)return unverified(`\`alembic current\` did not complete (${current.error})`);let headRevisions=parseAlembicRevisions(heads.stdout),currentRevisions=parseAlembicRevisions(current.stdout);return headRevisions===null||currentRevisions===null?unverified("Alembic output did not contain a readable revision"):headRevisions.length===currentRevisions.length&&headRevisions.every((rev,i)=>rev===currentRevisions[i])?null:{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}\``}}function unverified(detail){return{check:"alembic-head",severity:"warning",message:`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${detail}. Startup continues \u2014 verify with \`alembic -c alembic.ini heads\` and \`alembic -c alembic.ini current\` if a run later reports CONTRACT_MIGRATION_BEHIND.`}}var REQUIRED_REPO_FILES=["main.py","worker.py","alembic.ini"],PLANE_PORT_PROBE_TIMEOUT_MS=1500;async function runPlanePreflight(repoRoot,deps){let diagnostics=[],add=diagnostic=>{diagnostic&&diagnostics.push(diagnostic)},rootCheck=await checkRepositoryRoot(repoRoot,deps);if(add(rootCheck),rootCheck)return{ok:!1,diagnostics};add(await checkClaudeLogin(deps));let credentials=await checkBridgeCredentials(repoRoot,deps);for(let advisory of credentials.advisories)add(advisory);credentials.ok||add(credentials.diagnostic),add(await checkPlaneBuildFreshness(repoRoot,{fs:deps.fs}));let runtimeEntrypoint=deps.resolveRuntimeEntrypoint();return add(checkPlaneRuntimeEntrypoint(runtimeEntrypoint)),add(await checkServerPort(deps)),add(await checkAlembicHead(repoRoot,{execFile:deps.execFile,fileExists:filePath=>fileExists(filePath,deps.fs),platform:deps.platform})),add(await checkExistingPlane(repoRoot,deps)),diagnostics.filter(d=>d.severity==="blocking").length>0||!credentials.ok||!runtimeEntrypoint.ok?{ok:!1,diagnostics}:{ok:!0,diagnostics,context:{repoRoot,repoName:credentials.repoName,baseUrl:PLANE_SERVER_BASE_URL,executorEntrypoint:path34.join(repoRoot,"mcp_server","build","index.js"),runtimeEntrypoint:runtimeEntrypoint.entrypoint,bridgeApiKey:credentials.apiKey,bridgeCredentialSource:credentials.source}}}async function checkRepositoryRoot(repoRoot,deps){if(!path34.isAbsolute(repoRoot))return{check:"repository-root",severity:"blocking",message:"the repository root could not be resolved to an absolute path"};let missing=[];for(let file of REQUIRED_REPO_FILES)await fileExists(path34.join(repoRoot,file),deps.fs)||missing.push(file);return await fileExists(path34.join(repoRoot,"mcp_server"),deps.fs)||missing.push("mcp_server/"),missing.length===0?null:{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.`}}async function checkClaudeLogin(deps){let result=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.fs.readFile});return result.detected?null:{check:"claude-login",severity:"warning",message:formatClaudeLoginAdvisory(result)}}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."}};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{return{ok:!1,advisories:warnings.drain(),diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (resolver unavailable).`}}}return result.ok?{ok:!0,repoName:repo.repoName,apiKey:result.credentials.apiKey,source:result.credentials.source,advisories:warnings.drain()}:{ok:!1,advisories:warnings.drain(),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.`}}}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(`
@@ -7868,7 +7868,7 @@ When done, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_ru
7868
7868
 
7869
7869
  ${command}
7870
7870
 
7871
- When the worktrees have been spawned, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_run_id}" and \`agent_result\` set to a short summary of what start-tickets reported.`;return buildNeedsAgentTaskEnvelope({chainRunId:updated.chain_run_id,chainStage:START_TICKETS_PIPELINE,chainStep:idx+1,chainTotal:total,preamble:buildPreamble(recipe,idx,updated.stages),instruction})}function numericArg(value){if(typeof value=="number"&&Number.isFinite(value))return value}async function continueChainExecution(deps,persistence,recipe,row,autoApprove){let guard=0,guardMax=1e4;for(;guard++<guardMax;){let idx=row.current_stage_index,total=recipe.stages.length;if(idx>=total){try{row=await persistence.patchRun(row.chain_run_id,{status:"completed"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,row)}let stageRecipe=recipe.stages[idx],outcome2=null;if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE)return startStartTicketsStage(persistence,recipe,row);if(stageRecipe.fan_out_input?outcome2=await startOrContinueReviewTicketStage(deps,persistence,recipe,row,autoApprove):outcome2=await startOrContinueIdeaToTicketStage(deps,persistence,recipe,row,autoApprove),outcome2.kind==="pause"||outcome2.kind==="fail")return outcome2.envelope;row=outcome2.row}return failedEnvelope2("TOOL_ERROR","Chain execution exceeded its step guard.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length})}async function runFullAutomation(deps,input){try{if(typeof input.idea!="string"||input.idea.trim()==="")return failedEnvelope2("VALIDATION","idea must be a non-empty string.");let agent=input.agent??"claude";if(agent!=="claude")return failedEnvelope2("VALIDATION",`Unsupported agent "${String(input.agent)}". Only "claude" is supported.`);let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`);let autoApprove=input.auto_approve===void 0?!0:normalizeAutoApprove2(input.auto_approve),args={idea:input.idea,auto_approve:autoApprove,scheduled_at:input.scheduled_at??"",max_children:input.max_children,allow_duplicate:input.allow_duplicate,agent,ttl_seconds:input.ttl_seconds},initialStages=recipe.stages.map(stage=>({pipeline_name:stage.pipeline_name,status:"pending"})),persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.createRun({chain_name:CHAIN_NAME,args,current_stage_index:0,stages:initialStages,status:"running",ttl_seconds:input.ttl_seconds})}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while creating the chain run.")}return continueChainExecution(deps,persistence,recipe,row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in runFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while executing the full-automation chain.")}}async function resumeFullAutomation(deps,input){try{let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`,{chain_run_id:input.chain_run_id});let persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.getRun(input.chain_run_id)}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message,{chain_run_id:input.chain_run_id}):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while fetching the chain run.",{chain_run_id:input.chain_run_id})}if(row.status==="expired")return failedEnvelope2("EXPIRED","Chain run has expired.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length});let autoApprove=normalizeAutoApprove2(row.args.auto_approve),idx=row.current_stage_index,stageRecipe=recipe.stages[idx],total=recipe.stages.length;if(!stageRecipe)return failedEnvelope2("VALIDATION",`Chain run has no active stage at index ${idx}.`,{chain_run_id:row.chain_run_id,chain_total:total});if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE){if(typeof input.agent_result!="string"||input.agent_result.trim()==="")return failedEnvelope2("VALIDATION","agent_result must be a non-empty string to complete the start-tickets stage.",{chain_run_id:row.chain_run_id,chain_stage:START_TICKETS_PIPELINE,chain_step:idx+1,chain_total:total});let startResolution=resolveStartTicketKeys(row,idx,stageRecipe.fan_out_input??"reviewed_ticket_keys"),startedKeys=startResolution.ok?startResolution.keys:[],stages=cloneStages(row.stages);stages[idx].status="completed",stages[idx].pipeline_run_id=null,stages[idx].outputs={started_ticket_keys:startedKeys},stages[idx].summary=summarizeStageCompletion(START_TICKETS_PIPELINE,startedKeys);let updated;try{updated=await persistence.patchRun(row.chain_run_id,{stages,current_stage_index:idx+1,status:"completed",expected_status:"paused",expected_current_stage_index:idx})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,updated)}let activePipelineRunId=row.stages[idx]?.pipeline_run_id;if(!activePipelineRunId)return failedEnvelope2("VALIDATION",`No active child pipeline to resume for stage ${idx+1}.`,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});let peek=await peekPipelineRun(deps,activePipelineRunId);if("error_code"in peek)return failedEnvelope2(peek.error_code,peek.error,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});if(peek.status!=="paused"&&peek.status!=="completed"&&peek.status!=="failed")return{status:"failed",error_code:"VALIDATION",error:`Inner pipeline run is in status "${peek.status}" and cannot be safely resumed or recovered. Inspect pipeline_run_id ${activePipelineRunId}.`,chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total,pipeline_run_id:activePipelineRunId,resumable:!1};try{row=await persistence.patchRun(row.chain_run_id,{status:"running"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}let childEnv;if(peek.status==="paused")childEnv=await resumePipeline(deps,{pipeline_run_id:activePipelineRunId,agent_result:input.agent_result});else if(peek.status==="completed")childEnv={status:"completed",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,total_steps:peek.total_steps,results:peek.results};else{let failedStepError=peek.results.find(r=>!r.ok&&typeof r.error=="string")?.error;childEnv={status:"failed",error_code:"TOOL_ERROR",error:failedStepError?`Inner pipeline run failed before the chain could advance: ${failedStepError}`:"Inner pipeline run failed before the chain could advance.",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,results:peek.results}}let fanOut=!!stageRecipe.fan_out_input,childIndex=row.stages[idx]?.current_child_index??0,ticketKey=fanOut?(resolveCrossStageList(row,idx,stageRecipe.fan_out_input)??[])[childIndex]:void 0,outcome2=await handleChildPipelineEnvelope(persistence,recipe,row,childEnv,{fanOut,ticketKey,childIndex});return outcome2.kind==="pause"||outcome2.kind==="fail"?outcome2.envelope:continueChainExecution(deps,persistence,recipe,outcome2.row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in resumeFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while resuming the full-automation chain.",{chain_run_id:input.chain_run_id})}}import path51 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(path51.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path51.resolve(root,compRef));let cwdCandidate=path51.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=path51.basename(rawName),target=path51.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path51.resolve(dir)+path51.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=path51.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path51.resolve(dir)+path51.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)}]}}function text2(value){return{content:[{type:"text",text:value}]}}async function postDirectInvocation(deps,path53,body,ticketNumber){let resp;try{resp=await(deps.fetchImpl??fetch)(deps.buildUrl(path53),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify({...body,repo_name:deps.repoName})})}catch{return text2(`The request could not be delivered, so it is unknown whether it was accepted. Call get_ticket_state for ${ticketNumber} to check before retrying.`)}return resp.ok?text2(await resp.text()):text2(await deps.handleResponse(resp))}async function runRequestTicketUpdate(args,deps){return postDirectInvocation(deps,`/ticket/${encodeURIComponent(args.ticket_number)}/generate-ticket-update`,{},args.ticket_number)}async function runRequestEstimate(args,deps){return postDirectInvocation(deps,`/ticket/${encodeURIComponent(args.ticket_number)}/generate-estimate`,{recreate:args.recreate===!0},args.ticket_number)}async function runGetTicketUpdateReview(args,deps){let url=deps.buildGetUrl(`/ticket/${encodeURIComponent(args.ticket_number)}/ticket-update-review`,{repo_name:deps.repoName}),resp;try{resp=await(deps.fetchImpl??fetch)(url,{headers:await deps.getHeaders()})}catch{return text2(`The held-for-review proposal for ${args.ticket_number} could not be fetched. Retry shortly.`)}return resp.ok?text2(await resp.text()):text2(await deps.handleResponse(resp))}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 text3(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),result}var SHA_RE2=/^[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_RE2.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";async function precheckReviewCondition(deps,condition,prNumber,expectedHeadSha){if(!SUPPORTED_REVIEW_SOURCES.has(condition.source))return envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber);let unavailable=()=>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 snapshot=normalizeReviewSnapshot(await readJson2(reviewResp));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),envelope(!1,"refused",HEAD_SHA_DRIFT_REASON,"needs_human",expectedHeadSha,prNumber,diagnostics)}let evaluation=evaluateReviewCondition(condition,snapshot);return evaluation.passed?null: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_RE2.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 text3(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 text3(envelope(!1,"gate_unresolved",REQUIRED_CHECKS_EMPTY,"needs_human",expectedHeadSha,prNumber));if(resolution.reviewCondition!==null){let refusal=await precheckReviewCondition(deps,resolution.reviewCondition,prNumber,expectedHeadSha);if(refusal!==null)return text3(refusal)}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 text3(envelope(!1,"unknown","merge_request_not_observed","retry_later",expectedHeadSha,prNumber,{hint:UNKNOWN_HINT}))}if(!mergeResp.ok)return await deps.handleResponse(mergeResp).catch(()=>""),mergeResp.status===409?text3(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber)):text3(envelope(!1,"error","merge_request_failed","needs_human",expectedHeadSha,prNumber,{http_status:mergeResp.status}));let mergeJson=await readJson2(mergeResp);return executionMode==="local"&&isPlainObject12(mergeJson)&&mergeJson.status===LOCAL_APPROVAL_STATUS?text3(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey)):text3(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber))}catch{return text3(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";init_index_scope_contract();var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),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(){started||(started=!0,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),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:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),mkdir:(p,options)=>mkdir15(p,options),writeFile:(p,data,options)=>writeFile14(p,data,options),rename:(oldPath,newPath)=>rename5(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink4(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()=>path52.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path52.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path52.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path52.join(path52.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(),errorCode4=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:errorCode4,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode4,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)}async function saveLocally(dir,filename,content){let filePath=path52.join(dir,filename);try{return await mkdir15(dir,{recursive:!0}),await writeFile14(filePath,content,"utf-8"),`
7871
+ When the worktrees have been spawned, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_run_id}" and \`agent_result\` set to a short summary of what start-tickets reported.`;return buildNeedsAgentTaskEnvelope({chainRunId:updated.chain_run_id,chainStage:START_TICKETS_PIPELINE,chainStep:idx+1,chainTotal:total,preamble:buildPreamble(recipe,idx,updated.stages),instruction})}function numericArg(value){if(typeof value=="number"&&Number.isFinite(value))return value}async function continueChainExecution(deps,persistence,recipe,row,autoApprove){let guard=0,guardMax=1e4;for(;guard++<guardMax;){let idx=row.current_stage_index,total=recipe.stages.length;if(idx>=total){try{row=await persistence.patchRun(row.chain_run_id,{status:"completed"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,row)}let stageRecipe=recipe.stages[idx],outcome2=null;if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE)return startStartTicketsStage(persistence,recipe,row);if(stageRecipe.fan_out_input?outcome2=await startOrContinueReviewTicketStage(deps,persistence,recipe,row,autoApprove):outcome2=await startOrContinueIdeaToTicketStage(deps,persistence,recipe,row,autoApprove),outcome2.kind==="pause"||outcome2.kind==="fail")return outcome2.envelope;row=outcome2.row}return failedEnvelope2("TOOL_ERROR","Chain execution exceeded its step guard.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length})}async function runFullAutomation(deps,input){try{if(typeof input.idea!="string"||input.idea.trim()==="")return failedEnvelope2("VALIDATION","idea must be a non-empty string.");let agent=input.agent??"claude";if(agent!=="claude")return failedEnvelope2("VALIDATION",`Unsupported agent "${String(input.agent)}". Only "claude" is supported.`);let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`);let autoApprove=input.auto_approve===void 0?!0:normalizeAutoApprove2(input.auto_approve),args={idea:input.idea,auto_approve:autoApprove,scheduled_at:input.scheduled_at??"",max_children:input.max_children,allow_duplicate:input.allow_duplicate,agent,ttl_seconds:input.ttl_seconds},initialStages=recipe.stages.map(stage=>({pipeline_name:stage.pipeline_name,status:"pending"})),persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.createRun({chain_name:CHAIN_NAME,args,current_stage_index:0,stages:initialStages,status:"running",ttl_seconds:input.ttl_seconds})}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while creating the chain run.")}return continueChainExecution(deps,persistence,recipe,row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in runFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while executing the full-automation chain.")}}async function resumeFullAutomation(deps,input){try{let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`,{chain_run_id:input.chain_run_id});let persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.getRun(input.chain_run_id)}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message,{chain_run_id:input.chain_run_id}):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while fetching the chain run.",{chain_run_id:input.chain_run_id})}if(row.status==="expired")return failedEnvelope2("EXPIRED","Chain run has expired.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length});let autoApprove=normalizeAutoApprove2(row.args.auto_approve),idx=row.current_stage_index,stageRecipe=recipe.stages[idx],total=recipe.stages.length;if(!stageRecipe)return failedEnvelope2("VALIDATION",`Chain run has no active stage at index ${idx}.`,{chain_run_id:row.chain_run_id,chain_total:total});if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE){if(typeof input.agent_result!="string"||input.agent_result.trim()==="")return failedEnvelope2("VALIDATION","agent_result must be a non-empty string to complete the start-tickets stage.",{chain_run_id:row.chain_run_id,chain_stage:START_TICKETS_PIPELINE,chain_step:idx+1,chain_total:total});let startResolution=resolveStartTicketKeys(row,idx,stageRecipe.fan_out_input??"reviewed_ticket_keys"),startedKeys=startResolution.ok?startResolution.keys:[],stages=cloneStages(row.stages);stages[idx].status="completed",stages[idx].pipeline_run_id=null,stages[idx].outputs={started_ticket_keys:startedKeys},stages[idx].summary=summarizeStageCompletion(START_TICKETS_PIPELINE,startedKeys);let updated;try{updated=await persistence.patchRun(row.chain_run_id,{stages,current_stage_index:idx+1,status:"completed",expected_status:"paused",expected_current_stage_index:idx})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,updated)}let activePipelineRunId=row.stages[idx]?.pipeline_run_id;if(!activePipelineRunId)return failedEnvelope2("VALIDATION",`No active child pipeline to resume for stage ${idx+1}.`,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});let peek=await peekPipelineRun(deps,activePipelineRunId);if("error_code"in peek)return failedEnvelope2(peek.error_code,peek.error,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});if(peek.status!=="paused"&&peek.status!=="completed"&&peek.status!=="failed")return{status:"failed",error_code:"VALIDATION",error:`Inner pipeline run is in status "${peek.status}" and cannot be safely resumed or recovered. Inspect pipeline_run_id ${activePipelineRunId}.`,chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total,pipeline_run_id:activePipelineRunId,resumable:!1};try{row=await persistence.patchRun(row.chain_run_id,{status:"running"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}let childEnv;if(peek.status==="paused")childEnv=await resumePipeline(deps,{pipeline_run_id:activePipelineRunId,agent_result:input.agent_result});else if(peek.status==="completed")childEnv={status:"completed",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,total_steps:peek.total_steps,results:peek.results};else{let failedStepError=peek.results.find(r=>!r.ok&&typeof r.error=="string")?.error;childEnv={status:"failed",error_code:"TOOL_ERROR",error:failedStepError?`Inner pipeline run failed before the chain could advance: ${failedStepError}`:"Inner pipeline run failed before the chain could advance.",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,results:peek.results}}let fanOut=!!stageRecipe.fan_out_input,childIndex=row.stages[idx]?.current_child_index??0,ticketKey=fanOut?(resolveCrossStageList(row,idx,stageRecipe.fan_out_input)??[])[childIndex]:void 0,outcome2=await handleChildPipelineEnvelope(persistence,recipe,row,childEnv,{fanOut,ticketKey,childIndex});return outcome2.kind==="pause"||outcome2.kind==="fail"?outcome2.envelope:continueChainExecution(deps,persistence,recipe,outcome2.row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in resumeFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while resuming the full-automation chain.",{chain_run_id:input.chain_run_id})}}import path51 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(path51.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path51.resolve(root,compRef));let cwdCandidate=path51.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=path51.basename(rawName),target=path51.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path51.resolve(dir)+path51.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=path51.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path51.resolve(dir)+path51.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)}]}}function text2(value){return{content:[{type:"text",text:value}]}}async function postDirectInvocation(deps,path53,body,ticketNumber){let resp;try{resp=await(deps.fetchImpl??fetch)(deps.buildUrl(path53),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify({...body,repo_name:deps.repoName})})}catch{return text2(`The request could not be delivered, so it is unknown whether it was accepted. Call get_ticket_state for ${ticketNumber} to check before retrying.`)}return resp.ok?text2(await resp.text()):text2(await deps.handleResponse(resp))}async function runRequestTicketUpdate(args,deps){return postDirectInvocation(deps,`/ticket/${encodeURIComponent(args.ticket_number)}/generate-ticket-update`,{},args.ticket_number)}async function runRequestEstimate(args,deps){return postDirectInvocation(deps,`/ticket/${encodeURIComponent(args.ticket_number)}/generate-estimate`,{recreate:args.recreate===!0},args.ticket_number)}async function runGetTicketUpdateReview(args,deps){let url=deps.buildGetUrl(`/ticket/${encodeURIComponent(args.ticket_number)}/ticket-update-review`,{repo_name:deps.repoName}),resp;try{resp=await(deps.fetchImpl??fetch)(url,{headers:await deps.getHeaders()})}catch{return text2(`The held-for-review proposal for ${args.ticket_number} could not be fetched. Retry shortly.`)}return resp.ok?text2(await resp.text()):text2(await deps.handleResponse(resp))}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 text3(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_RE2=/^[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_RE2.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 refuse(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 refuse(envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber));let unavailable=()=>refuse(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),refuse(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}:refuse(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_RE2.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 text3(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 text3(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 text3(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 text3(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?text3(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber,{review_waiver:reviewWaiver})):text3(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?text3(withReviewWaiver(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey),reviewWaiver)):text3(withReviewWaiver(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber),reviewWaiver))}catch{return text3(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";init_index_scope_contract();var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),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(){started||(started=!0,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),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:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),mkdir:(p,options)=>mkdir15(p,options),writeFile:(p,data,options)=>writeFile14(p,data,options),rename:(oldPath,newPath)=>rename5(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink4(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()=>path52.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path52.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path52.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path52.join(path52.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(),errorCode4=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:errorCode4,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode4,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)}async function saveLocally(dir,filename,content){let filePath=path52.join(dir,filename);try{return await mkdir15(dir,{recursive:!0}),await writeFile14(filePath,content,"utf-8"),`
7872
7872
 
7873
7873
  ---
7874
7874
  Saved to ${filePath}`}catch(writeErr){return`