@bridge_gpt/mcp-server 0.2.39 → 0.2.41
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,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.
|
|
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,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.41"}});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\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`, `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`, `respawns`, `conflict_attempts`, and `counters.sessions_spawned`, `counters.plan_generations_observed`, `counters.merge_attempts`.\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`, `finished_at`, and `index_branch_override`. The last three are each **a string or `null`**. `started_at` and `finished_at` are the ISO-8601 times of the current or last parse run; `index_branch_override` names the branch the repository-wide index override currently points indexing at. A `null` on any of them is unavailable evidence and **never** permits advancement \u2014 in particular, missing timestamps can never satisfy Row 5's causal check.\n- `deadlines` \u2014 `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\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\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### 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- merged-ticket reconciliation and post-merge parse processing (Row 5),\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, and a merged ticket whose re-index is simply slow, both accumulate staleness while being perfectly actionable. Parking those 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 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: parse, 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`parse.status` is **repository-level**, not ticket-level: it stays `succeeded` from any earlier parse of any earlier ticket. So \"succeeded\" on its own says nothing about whether *this* merge has been indexed, and treating it as proof would mark a ticket `done` against a parse that finished before its code existed. This row is therefore an **ordered state machine keyed on durable evidence**, evaluated top to bottom, and the first matching branch is the tick's action:\n\n1. **No parse has been requested for this merge yet.** Search the ticket's journal for the exact marker\n\n ```\n parse requested for merge <merge_commit_sha or pr.head_sha>\n ```\n\n where the SHA is the merge commit SHA from the merge result when the merge envelope supplied one, and otherwise the fresh `pr.head_sha` from this tick's Stage 2 snapshot. No new checkpoint field is introduced for it \u2014 the journal line *is* the durable record.\n\n When the current ticket's journal contains no such marker, the **only** action is to call the `parse_repository` MCP tool. Then write one checkpoint whose journal line contains that exact marker together with the ISO-8601 timestamp at which the parse was requested. Take no other action and change no other row-specific field this tick. If `parse_repository` reports that a parse is already in progress, that is authoritative and fine \u2014 journal the marker anyway, because the request is what the marker records. **Fail-open.**\n\n2. **A request marker exists and `parse.status` is `queued` or `in_progress`.** Wait. Journal the observed state. Do not spawn anything and do not advance the next ticket.\n\n3. **A request marker exists, `parse.status` is `succeeded`, `parse.terminal` is `true`, and at least one of `parse.started_at` or `parse.finished_at` is a valid timestamp strictly later than the journaled request timestamp.** Only then call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. The strictly-later comparison is what makes this causal: it proves the succeeded run began or ended *after* the request, rather than being an older repository parse.\n\n4. **A request marker exists and `parse.status` is `succeeded`, but the timestamps are missing, malformed, equal to, or older than the journaled request timestamp.** Wait, and journal the causal mismatch naming the request timestamp and the observed `started_at` / `finished_at`. Do **not** mark the ticket done. This is the stale-success case, and it is a wait rather than a park because the correct parse may still be about to start.\n\n5. **`parse.status` is `failed`.** Select `NEEDS_HUMAN:parse_failed`, with the observed parse state as bounded string evidence. **Fail-closed.**\n\n**No next ticket is spawned until this one reaches `done`.** A merged ticket stays in flight until its parse is terminal, 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`), there is no pull request (`pr` is `null`), 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\n### Row 7 \u2014 Soft deadline with no progress: one targeted continuation\n\nWhen there is no pull request, `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 `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\n### Row 11 \u2014 Changes requested for the current head: one targeted review fix\n\nWhen `review.verdict` is `changes_requested` **and** `review.head_sha` equals `pr.head_sha`, spend the single targeted respawn on kind `review_fix`. 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.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\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. The two **print-only parks** are the sole exemption: `init_failed` (Stage 1) and `foreign_lock` (Stage 2) both 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. Every 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**`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.\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- **Seven persisted reasons**, each written durably by the single `checkpoint set` above: `stalled`, `ci_red`, `review_changes_requested`, `merge_blocked`, `conflict`, `parse_failed`, and `wrong_base`. A persisted park is what makes the *next* tick report `already parked` and stop.\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","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
|
|
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`, `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`**, and together they are the durable evidence Row 5 reads: the ISO-8601 time `parse_repository` was called for this ticket's merge, and the `pr.head_sha` it was called for. They are dedicated checkpoint fields, so nothing can evict 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`, `finished_at`, and `index_branch_override`. The last three are each **a string or `null`**. `started_at` and `finished_at` are the ISO-8601 times of the current or last parse run; `index_branch_override` names the branch the repository-wide index override currently points indexing at. A `null` on any of them is unavailable evidence and **never** permits advancement \u2014 in particular, missing timestamps can never satisfy Row 5's causal check.\n- `deadlines` \u2014 `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\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: parse, 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`parse.status` is **repository-level**, not ticket-level: it stays `succeeded` from any earlier parse of any earlier ticket. So \"succeeded\" on its own says nothing about whether *this* merge has been indexed, and treating it as proof would mark a ticket `done` against a parse that finished before its code existed. This row is therefore an **ordered state machine keyed on durable evidence**, evaluated top to bottom, and the first matching branch is the tick's action:\n\n**The request evidence is `ticket.parse_requested_at` and `ticket.parse_requested_for_sha` from this tick's Stage 2 snapshot \u2014 never a journal search.** Both are dedicated checkpoint fields. The journal is capped at 50 lines and evicts oldest-first, so a marker searched for there disappears after roughly fifty wait ticks \u2014 about four hours at a five-minute cadence \u2014 and branch 1 would fire a second time, calling `parse_repository` again against a fresh causal clock. A field cannot be evicted, which is the whole reason these two exist.\n\nThroughout this row, \"the request timestamp\" means `ticket.parse_requested_at`.\n\n1. **No parse has been requested for this merge yet** \u2014 `ticket.parse_requested_at` is `null` **and** `ticket.parse_requested_for_sha` is `null`.\n\n First require a usable SHA: `pr.head_sha` must be a non-null value on this tick's snapshot. If it is `null` \u2014 an unavailable `pr` probe, or a pull request record without a head \u2014 **wait**, journal that the parse request is deferred for want of a head SHA, and take no other action. Requesting a parse you cannot attribute to a commit records evidence that can never be checked.\n\n Otherwise the **only** action is to call the `parse_repository` MCP tool. Capture the ISO-8601 timestamp **immediately before** invoking it, so the recorded time can never be later than a parse the call itself started. Then write one checkpoint setting **both** `parse_requested_at` to that captured timestamp and `parse_requested_for_sha` to the current `pr.head_sha`, in the same single `checkpoint set`, together with the journal line\n\n ```\n parse requested for merge <pr.head_sha>\n ```\n\n carrying that same timestamp. The journal line is **supplementary** \u2014 a human-readable record of what the fields already say durably \u2014 and its eventual eviction has no effect on any branch of this row.\n\n Record the fields if the request was accepted **or** if `parse_repository` authoritatively reports that a parse is already in progress: the request is what the fields record, and an in-progress parse is a satisfied request, not a failed one. Take no other action and change no other row-specific field this tick. **Fail-open.**\n\n2. **A request is recorded and `parse.status` is `queued` or `in_progress`.** Wait. Journal the observed state. Do not spawn anything and do not advance the next ticket.\n\n3. **A request is recorded, `parse.status` is `succeeded`, `parse.terminal` is `true`, and at least one of `parse.started_at` or `parse.finished_at` is a valid timestamp strictly later than `ticket.parse_requested_at`.** Only then call `update_jira_status` for the ticket with `target_status` set to the Jira `Done` state, and prepare `status=done`. The strictly-later comparison is what makes this causal: it proves the succeeded run began or ended *after* the request, rather than being an older repository parse.\n\n4. **A request is recorded and `parse.status` is `succeeded`, but the timestamps are missing, malformed, equal to, or older than `ticket.parse_requested_at`.** Wait, and journal the causal mismatch naming the request timestamp and the observed `started_at` / `finished_at`. Do **not** mark the ticket done. This is the stale-success case, and it is a wait rather than a park because the correct parse may still be about to start.\n\n5. **A request is recorded, `parse.status` is `failed`, and at least one of `parse.started_at` or `parse.finished_at` is a valid timestamp strictly later than `ticket.parse_requested_at`.** Select `NEEDS_HUMAN:parse_failed`, with the observed parse state as bounded string evidence. **Fail-closed.**\n\n The strictly-later requirement is the **same causal test branch 3 applies, and for the same reason**: `parse.status` is repository-level, so a `failed` that predates this request describes some earlier parse of some earlier ticket. Parking on it would strand a ticket on a failure that has nothing to do with it. A `failed` whose timestamps are missing, malformed, equal to, or older than the request is therefore **not** this branch \u2014 it falls to branch 6 and waits.\n\n6. **A request is recorded and none of branches 2\u20135 matched** \u2014 including a stale or non-causal `succeeded`, a stale or non-causal `failed`, and an `idle` repository parse state. Wait, and journal the observed state together with the request timestamp. Neither advance nor park: hard liveness (Row 3) is what eventually escalates a wait that never resolves.\n\n**Inconsistent request evidence never re-requests a parse.** If exactly one of `parse_requested_at` / `parse_requested_for_sha` is populated, or `parse_requested_for_sha` disagrees with the current `pr.head_sha`, do **not** treat that as \"no request yet\" and do **not** call `parse_repository` again. Journal the causal inconsistency, naming both stored values and the observed `pr.head_sha`, and wait. A second parse would mint a new causal clock and invalidate the evidence the earlier request already produced; a mismatch that persists is escalated by Row 3's hard-liveness park, which is the correct place for a state a human must look at.\n\n**No next ticket is spawned until this one reaches `done`.** A merged ticket stays in flight until its parse is terminal, 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`, `parse_requested_at`, `parse_requested_for_sha`, `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 always written together**, in the one `checkpoint set` that also carries Row 5 branch 1's journal line:\n\n```\n--field parse_requested_at '<ISO-8601 time captured immediately before parse_repository>' --field parse_requested_for_sha '<pr.head_sha>'\n```\n\nNever write one without the other. A half-populated pair is the inconsistent-evidence state Row 5 refuses to act on, and splitting them across two invocations would also break the one-tick-one-write rule.\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- **Seven persisted reasons**, each written durably by the single `checkpoint set` above: `stalled`, `ci_red`, `review_changes_requested`, `merge_blocked`, `conflict`, `parse_failed`, and `wrong_base`. A persisted park is what makes the *next* tick report `already parked` and stop.\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","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
|
|
389
389
|
|
|
390
390
|
$ARGUMENTS
|
|
391
391
|
|
|
@@ -5558,7 +5558,7 @@ Agents: scaffolded ${agentTotal} agent${agentTotal===1?"":"s"}`),agentWritten.si
|
|
|
5558
5558
|
`)){let hint=parseClaudeStreamJsonLine(line);hint.phase_hint&&(advisory.phase_hint=hint.phase_hint)}},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),exitCode!==void 0&&(residue.exit_code=exitCode),attemptStartSha!==void 0&&(residue.attempt_start_sha=attemptStartSha),attemptEndSha!==void 0&&(residue.attempt_end_sha=attemptEndSha),residue}}}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="bridge-api",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(`
|
|
5559
5559
|
`)){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=input.effectiveBaseBranch===void 0?{}:{effectiveBaseBranch:input.effectiveBaseBranch};return{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}
|
|
5560
5560
|
|
|
5561
|
-
${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();init_bridge_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 isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}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(!isRecord2(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(!isRecord2(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);return counters.ok?!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}:counters}function validateCiLastPoll(value){return value===null?{ok:!0}:isRecord2(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(!isRecord2(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 isRecord2(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(value){if(!isRecord2(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`);let deadlines=value.deadlines;if(!isRecord2(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(!isRecord2(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(isRecord2(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: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,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},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)}
|
|
5561
|
+
${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();init_bridge_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 isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeConductEpicCheckpoint(value){return!isRecord2(value)||!Array.isArray(value.tickets)?value:{...value,tickets:value.tickets.map(ticket=>{if(!isRecord2(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(!isRecord2(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(!isRecord2(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}:isRecord2(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(!isRecord2(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 isRecord2(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(!isRecord2(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`);let deadlines=value.deadlines;if(!isRecord2(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(!isRecord2(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(isRecord2(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,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)}
|
|
5562
5562
|
`,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_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}}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}}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;try{let res=await post("/claim",manifest,manifest.repo_name);status=res.status,text4=res.text}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"};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}}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},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 byteLength(value){return Buffer.byteLength(value,"utf8")}function appendBounded(existing,chunk,limitBytes){if(byteLength(existing)>=limitBytes)return existing;let combined=existing+chunk;return byteLength(combined)<=limitBytes?combined:Buffer.from(combined,"utf8").subarray(0,limitBytes).toString("utf8")}async function pump(iterable,onChunk){if(iterable)try{for await(let chunk of iterable)onChunk(typeof chunk=="string"?chunk:String(chunk))}catch{}}async function runProcessWithTimeout(proc,timeoutSeconds,deps,options={}){let termGraceMs=options.termGraceMs??1e4,limit=options.excerptLimitBytes??8e3,stdoutExcerpt="",stderrExcerpt="",terminationRequested=!1,terminating=!1,graceTimer,terminate=()=>{terminating||(terminating=!0,proc.kill("SIGTERM"),graceTimer=deps.setTimer(()=>{proc.kill("SIGKILL")},termGraceMs))},pumpStdout=pump(proc.stdout,chunk=>{options.onStdout?.(chunk),stdoutExcerpt=appendBounded(stdoutExcerpt,chunk,limit),options.onStdoutTerminationCheck?.(chunk)===!0&&!terminationRequested&&(terminationRequested=!0,terminate())}),pumpStderr=pump(proc.stderr,chunk=>{stderrExcerpt=appendBounded(stderrExcerpt,chunk,limit)}),timedOut=!1,timeoutTimer=deps.setTimer(()=>{timedOut=!0,terminate()},timeoutSeconds*1e3),{exitCode,signal}=await proc.wait();deps.clearTimer(timeoutTimer),graceTimer!==void 0&&deps.clearTimer(graceTimer),await pumpStdout,await pumpStderr;let classification;return timedOut?classification="timeout":exitCode===0?classification="clean_exit":classification="crashed",{classification,exitCode,signal,stdoutExcerpt,stderrExcerpt,...terminationRequested?{terminationRequested:!0}:{}}}function killOwnedProcess(proc,reason){return proc.kill("SIGKILL"),{classification:"killed",reason}}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",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(`
|
|
5563
5563
|
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9).trim()};else if(line.startsWith("branch ")&¤t){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(`
|
|
5564
5564
|
`):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}
|
|
@@ -5625,10 +5625,10 @@ No run was created and no automation-start charge occurred.`),1)}if(validated&&(
|
|
|
5625
5625
|
${errorDetail(err)}
|
|
5626
5626
|
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)}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.
|
|
5627
5627
|
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`),1)}featureBranch!==void 0&&say(`Branch: creating or validating ${featureBranch} on origin\u2026`);let approval=await approveEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version},deps.fetch).catch(err=>(featureBranch!==void 0&&err instanceof ConductorBridgeApiError&&err.errorCode==="FEATURE_BRANCH_PROVISIONING"?deps.errorLog(`Failed to provision the feature branch '${featureBranch}' \u2014 child-ticket dispatch has NOT started. Correct repository access or the branch configuration, then re-run setup-epic.
|
|
5628
|
-
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}' already exists \u2014 validated, unchanged (the remote ref was not moved or reset); head ${prov.remote_head_sha}`))}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}`)}}return 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."),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 @bridge_gpt/mcp-server executor --repo ${access2.repoName}`)),0}init_base_ref();init_done_gate();init_bridge_api_client();init_pr_discovery();init_start_tickets();init_start_tickets_prereqs();init_start_tickets_repo();init_bridge_client();import{execFile as execFile6}from"node:child_process";import{promises as nodeFs2}from"node:fs";import os14 from"node:os";import path29 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_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)}}async function discoverConductEpicPrState(branch,options={}){let runGh=options.runGh??runGhCommand,result;try{result=await runGh(["pr","
|
|
5629
|
-
`)){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 ")&¤tPath!==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();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:`Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`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}}var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn"],TICKET_FIELDS=["status","branch","pr_number","spawned_at","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=(file,args,options)=>new Promise(resolve2=>{execFile6(file,args,{cwd:options?.cwd,maxBuffer:16777216,encoding:"utf-8",timeout:options?.timeoutMs,shell:!1},(error,stdout,stderr)=>{let code=error?.code;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode:typeof code=="number"?code:error?1:0})})}),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:os14.homedir,hostname:os14.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server conduct-epic <verb> [flags]","","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, create epic/<EPIC> on origin at the fetched base"," tip, repoint the indexed branch, write the checkpoint, and take the lock."," 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.",""," 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(", ")}.`,""," 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.","","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(`
|
|
5628
|
+
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}' already exists \u2014 validated, unchanged (the remote ref was not moved or reset); head ${prov.remote_head_sha}`))}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}`)}}return 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."),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 @bridge_gpt/mcp-server executor --repo ${access2.repoName}`)),0}init_base_ref();init_done_gate();init_bridge_api_client();init_pr_discovery();init_start_tickets();init_start_tickets_prereqs();init_start_tickets_repo();init_bridge_client();import{execFile as execFile6}from"node:child_process";import{promises as nodeFs2}from"node:fs";import os14 from"node:os";import path29 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&¤tBranch!==null&&entries.push({path:currentPath,branch:currentBranch}),currentPath=null,currentBranch=null};for(let rawLine of String(output??"").split(`
|
|
5629
|
+
`)){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 ")&¤tPath!==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();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:`Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`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}}var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn"],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=(file,args,options)=>new Promise(resolve2=>{execFile6(file,args,{cwd:options?.cwd,maxBuffer:16777216,encoding:"utf-8",timeout:options?.timeoutMs,shell:!1},(error,stdout,stderr)=>{let code=error?.code;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode:typeof code=="number"?code:error?1:0})})}),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:os14.homedir,hostname:os14.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server conduct-epic <verb> [flags]","","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, create epic/<EPIC> on origin at the fetched base"," tip, repoint the indexed branch, write the checkpoint, and take the lock."," 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.",""," 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 \u2014 parse_requested_at"," and parse_requested_for_sha are recorded together, never in two writes.",""," 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.","","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(`
|
|
5630
5630
|
`)}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"]};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")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},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"--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;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?path29.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 Promise.resolve(deps.runCommand("git",args,{cwd:deps.cwd}))}function firstLine2(result){let value=result.stdout.split(`
|
|
5631
|
-
`)[0]?.trim()??"";return value.length===0?null:value}function lsRemoteSha(result){let line=firstLine2(result);if(line===null)return null;let sha=line.split(/\s+/)[0]?.trim()??"";return sha.length===0?null:sha}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}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],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,epicBranchAlreadyAtBase=!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 indexBranch=await getIndexBranch(access2,deps.fetchImpl);if(!indexBranch.ok)failures.push(`The indexed-branch state could not be read: ${indexBranch.error}`);else{let override=indexBranch.value.override;override!==null&&override.override_branch!==epicBranch?failures.push(`The repository index is already repointed to '${override.override_branch}' by another epic. Run \`conduct-epic finish\` for that epic first.`):override!==null&&baseBranch===null&&(baseBranch=override.original_base_branch)}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?firstLine2(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`);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&&(baseSha!==null&&existingSha===baseSha?epicBranchAlreadyAtBase=!0:failures.push(`origin/${epicBranch} already exists at a commit other than the ${baseBranch} tip. 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,access:access2,baseBranch,baseSha,epicBranchAlreadyAtBase,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 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);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)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtBase?" (already at the base tip)":""}`,`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,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, 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.`)}if((await git(deps,["push","origin",`refs/remotes/origin/${preflight.baseBranch}:refs/heads/${epicBranch}`])).exitCode!==0)return emitFailure(deps,options.json,[`Could not create origin/${epicBranch} from the ${preflight.baseBranch} tip.`],{epic_key:options.epicKey});let repointed=await repointIndexBranch(access2,{branch:epicBranch},deps.fetchImpl);if(!repointed.ok)return emitFailure(deps,options.json,[`The repository index could not be repointed to ${epicBranch}: ${repointed.error}`],{epic_key:options.epicKey});let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,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,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),index_repointed:!0,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(firstLine2(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 parseStatus2=await getParseStatus(access2,deps.fetchImpl);parseStatus2.ok?parse=normalizeParseStatus(parseStatus2.value):probeErrors.push({probe:"parse",reason:parseStatus2.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;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),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:{key:ticket.key,status:ticket.status,branch:discoveredBranch,pr_number:ticket.pr_number,spawned_at:ticket.spawned_at,respawns:ticket.respawns,conflict_attempts:ticket.conflict_attempts,counters:{...ticket.counters}},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)},lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload)}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"),index_branch_override:optionalText("index_branch_override")}}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"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 spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform},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 restored=await restoreIndexBranch(access2,deps.fetchImpl);if(!restored.ok)return await releaseAcquired(lock),emitFailure(deps,options.json,[`The repository index could not be restored: ${restored.error}`]);await releaseAcquired(lock);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,index_restored:!0,index_changed:restored.value.changed,current_base_branch:restored.value.current_base_branch,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 restore: ${restored.value.changed?"restored":"already restored"}`,`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 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)}}import{randomUUID as randomUUID3}from"crypto";var PLANE_RUNTIME_DIR=".bridge/plane",PLANE_MANIFEST_FILENAME="plane.json",PLANE_RUNTIME_LOG_FILENAME="runtime.log",PLANE_RUNTIME_LOG_PATH=`${PLANE_RUNTIME_DIR}/${PLANE_RUNTIME_LOG_FILENAME}`,PLANE_SERVER_HOST="127.0.0.1",PLANE_SERVER_PORT=8e3,PLANE_SERVER_BASE_URL=`http://${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT}`,PLANE_RUNTIME_ACTION="__runtime",PLANE_ENTRYPOINT_ACTION="__entrypoint",PLANE_ID_ENV_VAR="BAPI_PLANE_ID",PLANE_OBSERVER_COMMAND="CONDUCTOR_DEAD_MAN_ONLY=true python worker.py",PLANE_OBSERVER_CHANNEL_TYPE_ENV="CONDUCTOR_DEADMAN_CHANNEL_TYPE",PLANE_OBSERVER_DESTINATION_ENV="CONDUCTOR_DEADMAN_DESTINATION_REF";import path30 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(path30.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat12=await deps.fs.stat(path30.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=path30.join(repoRoot,"mcp_server"),buildDir=path30.join(packageRoot,"build"),executorEntrypoint=path30.join(buildDir,"index.js"),srcDir=path30.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 path31 from"path";function getPlanePaths(repoRoot){let planeDir=path31.join(repoRoot,".bridge","plane");return{planeDir,manifestPath:path31.join(planeDir,PLANE_MANIFEST_FILENAME),logPathFor:member=>path31.join(planeDir,`${member}.log`)}}function relativeLogPathFor(member){return`${PLANE_RUNTIME_DIR}/${member}.log`}var VALID_STATES=new Set(["spawning","running","ready","exited"]),MANIFEST_KEYS=new Set(["schemaVersion","planeId","repoRoot","supervisorPid","supervisorPgid","createdAt","updatedAt","members"]),MEMBER_KEYS=new Set(["name","pid","state","exitCode","exitSignal","logPath"]);function isPlaneMemberName(value){return typeof value!="string"?!1:value==="server"||value==="worker"?!0:/^executor-[1-9][0-9]*$/.test(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>0}function isIsoTimestamp(value){return typeof value=="string"&&value.length>0&&!Number.isNaN(Date.parse(value))}function parsePlaneManifest(value){if(typeof value!="object"||value===null||Array.isArray(value))return{ok:!1,error:"manifest is not an object"};let record=value;for(let key of Object.keys(record))if(!MANIFEST_KEYS.has(key))return{ok:!1,error:`manifest has an unsupported field '${key}'`};if(record.schemaVersion!==1)return{ok:!1,error:"manifest schema version is not supported"};if(typeof record.planeId!="string"||!/^[0-9a-f-]{8,}$/i.test(record.planeId))return{ok:!1,error:"manifest plane identity is missing or malformed"};if(typeof record.repoRoot!="string"||record.repoRoot.length===0)return{ok:!1,error:"manifest repository root is missing"};if(!isPositiveInteger2(record.supervisorPid))return{ok:!1,error:"manifest supervisor pid is not a positive integer"};if(!isPositiveInteger2(record.supervisorPgid))return{ok:!1,error:"manifest supervisor process-group id is not a positive integer"};if(!isIsoTimestamp(record.createdAt)||!isIsoTimestamp(record.updatedAt))return{ok:!1,error:"manifest timestamps are missing or malformed"};if(!Array.isArray(record.members)||record.members.length===0)return{ok:!1,error:"manifest members are missing"};let members=[],seen=new Set;for(let raw of record.members){if(typeof raw!="object"||raw===null||Array.isArray(raw))return{ok:!1,error:"manifest member is not an object"};let member=raw;for(let key of Object.keys(member))if(!MEMBER_KEYS.has(key))return{ok:!1,error:`manifest member has an unsupported field '${key}'`};if(!isPlaneMemberName(member.name))return{ok:!1,error:"manifest member name is not a recognized plane member"};if(seen.has(member.name))return{ok:!1,error:`manifest lists member '${member.name}' more than once`};if(seen.add(member.name),member.pid!==null&&!isPositiveInteger2(member.pid))return{ok:!1,error:`manifest member '${member.name}' has an invalid pid`};if(typeof member.state!="string"||!VALID_STATES.has(member.state))return{ok:!1,error:`manifest member '${member.name}' has an invalid state`};if(member.exitCode!==null&&!Number.isSafeInteger(member.exitCode))return{ok:!1,error:`manifest member '${member.name}' has an invalid exit code`};if(member.exitSignal!==null&&typeof member.exitSignal!="string")return{ok:!1,error:`manifest member '${member.name}' has an invalid exit signal`};if(member.logPath!==relativeLogPathFor(member.name))return{ok:!1,error:`manifest member '${member.name}' has an unexpected log path`};members.push({name:member.name,pid:member.pid,state:member.state,exitCode:member.exitCode,exitSignal:member.exitSignal,logPath:member.logPath})}return{ok:!0,manifest:{schemaVersion:1,planeId:record.planeId,repoRoot:record.repoRoot,supervisorPid:record.supervisorPid,supervisorPgid:record.supervisorPgid,createdAt:record.createdAt,updatedAt:record.updatedAt,members}}}async function readPlaneManifest(repoRoot,fs7){let{manifestPath}=getPlanePaths(repoRoot),raw;try{raw=await fs7.readFile(manifestPath)}catch(err){let code=err?.code;return code==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:typeof code=="string"?code:"read failed"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed",error:"manifest is not valid JSON"}}let result=parsePlaneManifest(parsed);return result.ok?{kind:"valid",manifest:result.manifest}:{kind:"malformed",error:result.error}}function isProcessAlive(pid,kill){if(!isPositiveInteger2(pid))return"unknown";try{return kill(pid,0),"alive"}catch(err){let code=err?.code;return code==="ESRCH"?"dead":code==="EPERM"?"alive":"unknown"}}function probeManifestMembers(manifest,proc){return manifest.members.map(member=>member.state==="exited"||member.pid===null?{member,liveness:"dead"}:{member,liveness:proc.isAlive(member.pid)})}function manifestHasLiveProcess(manifest,proc){return proc.isAlive(manifest.supervisorPid)!=="dead"?!0:probeManifestMembers(manifest,proc).some(probe=>probe.liveness!=="dead")}function formatPlaneManifest(manifest){return`${JSON.stringify(manifest,null,2)}
|
|
5631
|
+
`)[0]?.trim()??"";return value.length===0?null:value}function lsRemoteSha(result){let line=firstLine2(result);if(line===null)return null;let sha=line.split(/\s+/)[0]?.trim()??"";return sha.length===0?null:sha}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}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],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,epicBranchAlreadyAtBase=!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 indexBranch=await getIndexBranch(access2,deps.fetchImpl);if(!indexBranch.ok)failures.push(`The indexed-branch state could not be read: ${indexBranch.error}`);else{let override=indexBranch.value.override;override!==null&&override.override_branch!==epicBranch?failures.push(`The repository index is already repointed to '${override.override_branch}' by another epic. Run \`conduct-epic finish\` for that epic first.`):override!==null&&baseBranch===null&&(baseBranch=override.original_base_branch)}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?firstLine2(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`);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&&(baseSha!==null&&existingSha===baseSha?epicBranchAlreadyAtBase=!0:failures.push(`origin/${epicBranch} already exists at a commit other than the ${baseBranch} tip. 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,access:access2,baseBranch,baseSha,epicBranchAlreadyAtBase,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 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);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)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtBase?" (already at the base tip)":""}`,`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,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, 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.`)}if((await git(deps,["push","origin",`refs/remotes/origin/${preflight.baseBranch}:refs/heads/${epicBranch}`])).exitCode!==0)return emitFailure(deps,options.json,[`Could not create origin/${epicBranch} from the ${preflight.baseBranch} tip.`],{epic_key:options.epicKey});let repointed=await repointIndexBranch(access2,{branch:epicBranch},deps.fetchImpl);if(!repointed.ok)return emitFailure(deps,options.json,[`The repository index could not be repointed to ${epicBranch}: ${repointed.error}`],{epic_key:options.epicKey});let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,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,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),index_repointed:!0,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(firstLine2(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 parseStatus2=await getParseStatus(access2,deps.fetchImpl);parseStatus2.ok?parse=normalizeParseStatus(parseStatus2.value):probeErrors.push({probe:"parse",reason:parseStatus2.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;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),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)},lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload)}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"),index_branch_override:optionalText("index_branch_override")}}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 spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform},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 restored=await restoreIndexBranch(access2,deps.fetchImpl);if(!restored.ok)return await releaseAcquired(lock),emitFailure(deps,options.json,[`The repository index could not be restored: ${restored.error}`]);await releaseAcquired(lock);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,index_restored:!0,index_changed:restored.value.changed,current_base_branch:restored.value.current_base_branch,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 restore: ${restored.value.changed?"restored":"already restored"}`,`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 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)}}import{randomUUID as randomUUID3}from"crypto";var PLANE_RUNTIME_DIR=".bridge/plane",PLANE_MANIFEST_FILENAME="plane.json",PLANE_RUNTIME_LOG_FILENAME="runtime.log",PLANE_RUNTIME_LOG_PATH=`${PLANE_RUNTIME_DIR}/${PLANE_RUNTIME_LOG_FILENAME}`,PLANE_SERVER_HOST="127.0.0.1",PLANE_SERVER_PORT=8e3,PLANE_SERVER_BASE_URL=`http://${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT}`,PLANE_RUNTIME_ACTION="__runtime",PLANE_ENTRYPOINT_ACTION="__entrypoint",PLANE_ID_ENV_VAR="BAPI_PLANE_ID",PLANE_OBSERVER_COMMAND="CONDUCTOR_DEAD_MAN_ONLY=true python worker.py",PLANE_OBSERVER_CHANNEL_TYPE_ENV="CONDUCTOR_DEADMAN_CHANNEL_TYPE",PLANE_OBSERVER_DESTINATION_ENV="CONDUCTOR_DEADMAN_DESTINATION_REF";import path30 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(path30.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat12=await deps.fs.stat(path30.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=path30.join(repoRoot,"mcp_server"),buildDir=path30.join(packageRoot,"build"),executorEntrypoint=path30.join(buildDir,"index.js"),srcDir=path30.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 path31 from"path";function getPlanePaths(repoRoot){let planeDir=path31.join(repoRoot,".bridge","plane");return{planeDir,manifestPath:path31.join(planeDir,PLANE_MANIFEST_FILENAME),logPathFor:member=>path31.join(planeDir,`${member}.log`)}}function relativeLogPathFor(member){return`${PLANE_RUNTIME_DIR}/${member}.log`}var VALID_STATES=new Set(["spawning","running","ready","exited"]),MANIFEST_KEYS=new Set(["schemaVersion","planeId","repoRoot","supervisorPid","supervisorPgid","createdAt","updatedAt","members"]),MEMBER_KEYS=new Set(["name","pid","state","exitCode","exitSignal","logPath"]);function isPlaneMemberName(value){return typeof value!="string"?!1:value==="server"||value==="worker"?!0:/^executor-[1-9][0-9]*$/.test(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>0}function isIsoTimestamp(value){return typeof value=="string"&&value.length>0&&!Number.isNaN(Date.parse(value))}function parsePlaneManifest(value){if(typeof value!="object"||value===null||Array.isArray(value))return{ok:!1,error:"manifest is not an object"};let record=value;for(let key of Object.keys(record))if(!MANIFEST_KEYS.has(key))return{ok:!1,error:`manifest has an unsupported field '${key}'`};if(record.schemaVersion!==1)return{ok:!1,error:"manifest schema version is not supported"};if(typeof record.planeId!="string"||!/^[0-9a-f-]{8,}$/i.test(record.planeId))return{ok:!1,error:"manifest plane identity is missing or malformed"};if(typeof record.repoRoot!="string"||record.repoRoot.length===0)return{ok:!1,error:"manifest repository root is missing"};if(!isPositiveInteger2(record.supervisorPid))return{ok:!1,error:"manifest supervisor pid is not a positive integer"};if(!isPositiveInteger2(record.supervisorPgid))return{ok:!1,error:"manifest supervisor process-group id is not a positive integer"};if(!isIsoTimestamp(record.createdAt)||!isIsoTimestamp(record.updatedAt))return{ok:!1,error:"manifest timestamps are missing or malformed"};if(!Array.isArray(record.members)||record.members.length===0)return{ok:!1,error:"manifest members are missing"};let members=[],seen=new Set;for(let raw of record.members){if(typeof raw!="object"||raw===null||Array.isArray(raw))return{ok:!1,error:"manifest member is not an object"};let member=raw;for(let key of Object.keys(member))if(!MEMBER_KEYS.has(key))return{ok:!1,error:`manifest member has an unsupported field '${key}'`};if(!isPlaneMemberName(member.name))return{ok:!1,error:"manifest member name is not a recognized plane member"};if(seen.has(member.name))return{ok:!1,error:`manifest lists member '${member.name}' more than once`};if(seen.add(member.name),member.pid!==null&&!isPositiveInteger2(member.pid))return{ok:!1,error:`manifest member '${member.name}' has an invalid pid`};if(typeof member.state!="string"||!VALID_STATES.has(member.state))return{ok:!1,error:`manifest member '${member.name}' has an invalid state`};if(member.exitCode!==null&&!Number.isSafeInteger(member.exitCode))return{ok:!1,error:`manifest member '${member.name}' has an invalid exit code`};if(member.exitSignal!==null&&typeof member.exitSignal!="string")return{ok:!1,error:`manifest member '${member.name}' has an invalid exit signal`};if(member.logPath!==relativeLogPathFor(member.name))return{ok:!1,error:`manifest member '${member.name}' has an unexpected log path`};members.push({name:member.name,pid:member.pid,state:member.state,exitCode:member.exitCode,exitSignal:member.exitSignal,logPath:member.logPath})}return{ok:!0,manifest:{schemaVersion:1,planeId:record.planeId,repoRoot:record.repoRoot,supervisorPid:record.supervisorPid,supervisorPgid:record.supervisorPgid,createdAt:record.createdAt,updatedAt:record.updatedAt,members}}}async function readPlaneManifest(repoRoot,fs7){let{manifestPath}=getPlanePaths(repoRoot),raw;try{raw=await fs7.readFile(manifestPath)}catch(err){let code=err?.code;return code==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:typeof code=="string"?code:"read failed"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed",error:"manifest is not valid JSON"}}let result=parsePlaneManifest(parsed);return result.ok?{kind:"valid",manifest:result.manifest}:{kind:"malformed",error:result.error}}function isProcessAlive(pid,kill){if(!isPositiveInteger2(pid))return"unknown";try{return kill(pid,0),"alive"}catch(err){let code=err?.code;return code==="ESRCH"?"dead":code==="EPERM"?"alive":"unknown"}}function probeManifestMembers(manifest,proc){return manifest.members.map(member=>member.state==="exited"||member.pid===null?{member,liveness:"dead"}:{member,liveness:proc.isAlive(member.pid)})}function manifestHasLiveProcess(manifest,proc){return proc.isAlive(manifest.supervisorPid)!=="dead"?!0:probeManifestMembers(manifest,proc).some(probe=>probe.liveness!=="dead")}function formatPlaneManifest(manifest){return`${JSON.stringify(manifest,null,2)}
|
|
5632
5632
|
`}async function writePlaneManifest(manifest,fs7){let{manifestPath}=getPlanePaths(manifest.repoRoot),tempPath=`${manifestPath}.${manifest.planeId}.tmp`;await fs7.writeFile(tempPath,formatPlaneManifest(manifest)),await fs7.rename(tempPath,manifestPath)}async function clearPlaneManifest(repoRoot,planeId,fs7){let read=await readPlaneManifest(repoRoot,fs7);if(read.kind==="missing")return{ok:!0,removed:!1};if(read.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`refusing to remove an unvalidated manifest (${read.error})`};if(read.manifest.planeId!==planeId)return{ok:!1,reason:"not-owned",message:"refusing to remove a manifest owned by a different plane"};try{return await fs7.unlink(getPlanePaths(repoRoot).manifestPath),{ok:!0,removed:!0}}catch(err){let code=err?.code;return code==="ENOENT"?{ok:!0,removed:!1}:{ok:!1,reason:"error",message:`manifest could not be removed (${typeof code=="string"?code:"unlink failed"})`}}}async function claimPlaneManifest(params){let{manifest,fs:fs7,proc}=params,{planeDir,manifestPath}=getPlanePaths(manifest.repoRoot);try{await fs7.mkdir(planeDir)}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not create ${PLANE_RUNTIME_DIR}/ (${typeof code=="string"?code:"mkdir failed"})`}}let serialized=formatPlaneManifest(manifest);try{return await fs7.createExclusive(manifestPath,serialized),{ok:!0,manifest}}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not claim the plane manifest (${typeof code=="string"?code:"create failed"})`}}let existing=await readPlaneManifest(manifest.repoRoot,fs7);if(existing.kind==="missing")return{ok:!1,reason:"conflict",message:"another `plane up` is claiming the manifest right now"};if(existing.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`an existing ${PLANE_RUNTIME_DIR}/${PLANE_MANIFEST_FILENAME} could not be validated (${existing.error}); refusing to signal or replace it \u2014 inspect and remove it by hand`};if(manifestHasLiveProcess(existing.manifest,proc))return{ok:!1,reason:"live-plane",message:`a plane is already running (supervisor pid ${existing.manifest.supervisorPid}); run \`plane status\` to inspect it, or \`plane down\` to wind it down`};try{await writePlaneManifest(manifest,fs7)}catch(err){let code=err?.code;return{ok:!1,reason:"error",message:`could not replace the stale plane manifest (${typeof code=="string"?code:"write failed"})`}}return{ok:!0,manifest}}import path33 from"path";import path32 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"?path32.join(repoRoot,".venv","Scripts","alembic.exe"):path32.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);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:path33.join(repoRoot,"mcp_server","build","index.js"),runtimeEntrypoint:runtimeEntrypoint.entrypoint,bridgeApiKey:credentials.apiKey,bridgeCredentialSource:credentials.source}}}async function checkRepositoryRoot(repoRoot,deps){if(!path33.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(path33.join(repoRoot,file),deps.fs)||missing.push(file);return await fileExists(path33.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,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 result;try{result=await deps.resolveCredentials(repo.repoName,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.fs.readFile,stat:async filePath=>(await deps.fs.stat(filePath),{mode:384}),stderr:()=>{}})}catch{return{ok:!1,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}:{ok:!1,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.`}}}async function checkServerPort(deps){let result=await deps.probePort(PLANE_SERVER_HOST,PLANE_SERVER_PORT,PLANE_PORT_PROBE_TIMEOUT_MS);return result.kind==="refused"?null:result.kind==="connected"?{check:"server-port",severity:"blocking",message:`${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT} is already accepting connections. That port may belong to a SIBLING WORKTREE's server \u2014 check before you kill it. Stop the existing server (or wind down its plane) and retry.`}:{check:"server-port",severity:"warning",message:`could not determine whether ${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT} is free (${result.error}); startup continues and uvicorn will fail loudly if the port is taken.`}}async function checkExistingPlane(repoRoot,deps){let read=await readPlaneManifest(repoRoot,deps.fs);return read.kind==="missing"?null:read.kind!=="valid"?{check:"existing-plane",severity:"blocking",message:`an existing .bridge/plane/plane.json could not be validated (${read.error}). No process was signalled and the file was left untouched \u2014 inspect it, then remove it by hand if no plane is running.`}:manifestHasLiveProcess(read.manifest,deps.proc)?{check:"existing-plane",severity:"blocking",message:`a plane is already running (supervisor pid ${read.manifest.supervisorPid}). Run \`plane status\` to inspect it, or \`plane down\` to wind it down first.`}:{check:"existing-plane",severity:"warning",message:"a previous plane manifest is present but every recorded process is gone; it will be replaced after a final liveness re-check."}}async function fileExists(filePath,fs7){try{return await fs7.stat(filePath),!0}catch{return!1}}import path34 from"path";var PLANE_READINESS_TIMEOUT_MS=6e4;function resolvePythonExecutable(env){let configured=env.BAPI_PLANE_PYTHON;return typeof configured=="string"&&configured.trim().length>0?configured.trim():"python"}function resolveUvicornExecutable(env){let configured=env.BAPI_PLANE_UVICORN;return typeof configured=="string"&&configured.trim().length>0?configured.trim():"uvicorn"}function buildPlaneChildEnv(parentEnv,context){let env={};for(let[key,value]of Object.entries(parentEnv))value!==void 0&&(env[key]=value);return env.BAPI_REPO_NAME=context.repoName,env.BAPI_BASE_URL=context.baseUrl,env.BAPI_API_KEY=context.bridgeApiKey,delete env.CONDUCTOR_DEAD_MAN_ONLY,env}function buildPlaneMemberRoster(params){let{context,executors,parentEnv,nodeExecutable}=params,env=buildPlaneChildEnv(parentEnv,context),cwd=context.repoRoot,server2={name:"server",command:resolveUvicornExecutable(parentEnv),args:["main:app","--host",PLANE_SERVER_HOST,"--port",String(PLANE_SERVER_PORT)],cwd,env,logPath:relativeLogPathFor("server"),readiness:{host:PLANE_SERVER_HOST,port:PLANE_SERVER_PORT,timeoutMs:PLANE_READINESS_TIMEOUT_MS}},worker={name:"worker",command:resolvePythonExecutable(parentEnv),args:["worker.py"],cwd,env,logPath:relativeLogPathFor("worker"),readiness:null},executorMembers=[];for(let lane=1;lane<=executors;lane+=1){let name=`executor-${lane}`;executorMembers.push({name,command:nodeExecutable,args:[context.executorEntrypoint,"executor","--repo",context.repoName,"--base-url",context.baseUrl,"--executor-id",buildExecutorId(context.repoName,lane)],cwd,env,logPath:relativeLogPathFor(name),readiness:null})}return[server2,worker,...executorMembers]}function buildExecutorId(repoName,lane){return`plane-${repoName.replace(/[^A-Za-z0-9_-]/g,"-").slice(0,40)||"repo"}-${lane}`}function describePlaneMember(spec){return`${spec.name}: ${[spec.command,...spec.args].join(" ")}`}function absoluteLogPath(repoRoot,spec){return path34.join(repoRoot,spec.logPath)}async function getPlaneStatus(repoRoot,deps){let read=await readPlaneManifest(repoRoot,deps.fs);if(read.kind==="missing")return{ok:!0,kind:"no-plane"};if(read.kind!=="valid")return{ok:!1,error:`.bridge/plane/plane.json could not be validated (${read.error}). No process was probed. Inspect the file and remove it by hand once you have confirmed no plane is running.`};let manifest=read.manifest;return{ok:!0,kind:"plane",planeId:manifest.planeId,supervisorPid:manifest.supervisorPid,supervisorPgid:manifest.supervisorPgid,supervisorLiveness:deps.proc.isAlive(manifest.supervisorPid),createdAt:manifest.createdAt,updatedAt:manifest.updatedAt,members:probeManifestMembers(manifest,deps.proc).map(probe=>({name:probe.member.name,pid:probe.member.pid,state:probe.member.state,liveness:probe.liveness,exitCode:probe.member.exitCode,exitSignal:probe.member.exitSignal,logPath:probe.member.logPath}))}}function formatPlaneStatus(result){if(!result.ok)return`plane status FAILED: ${result.error}`;if(result.kind==="no-plane")return`No plane is running (no ${PLANE_RUNTIME_DIR}/plane.json).`;let lines=[`Plane ${result.planeId}`,` supervisor pid ${result.supervisorPid} (group ${result.supervisorPgid}) \u2014 ${result.supervisorLiveness}`,` started ${result.createdAt}`,` updated ${result.updatedAt}`,""];for(let member of result.members)lines.push(` ${member.name.padEnd(12)} ${describeMemberState(member,result.supervisorLiveness)} ${member.logPath}`);let neverStarted=result.members.filter(m=>isPreStart(m)&&result.supervisorLiveness==="dead");neverStarted.length>0&&(lines.push(""),lines.push(`${neverStarted.length} member(s) were NEVER STARTED: the supervisor died before it reached them, so their log files above may be empty or missing entirely.`),lines.push(`Read the runtime trace instead: ${PLANE_RUNTIME_LOG_PATH}`));let holes=result.members.filter(m=>m.liveness!=="alive"&&!neverStarted.includes(m));return holes.length>0&&(lines.push(""),lines.push(`${holes.length} member(s) are not running. They are NOT restarted automatically \u2014 read the log(s) above, then \`plane down\` and \`plane up\` when you have a fix.`)),lines.join(`
|
|
5633
5633
|
`)}function isPreStart(member){return member.state==="spawning"&&member.pid===null}function describeMemberState(member,supervisorLiveness){return member.liveness==="alive"?`running (pid ${member.pid??"?"})`.padEnd(34):member.exitSignal!==null?`exited (signal ${member.exitSignal})`.padEnd(34):member.exitCode!==null?`exited (code ${member.exitCode})`.padEnd(34):member.liveness==="unknown"?`unknown (pid ${member.pid??"?"}, liveness unverifiable)`.padEnd(34):isPreStart(member)?supervisorLiveness==="alive"?"starting (not yet spawned)".padEnd(34):supervisorLiveness==="dead"?"never started".padEnd(34):"unknown (supervisor liveness unverifiable)".padEnd(34):"exited (no recorded exit status)".padEnd(34)}var PLANE_SHUTDOWN_GRACE_MS=1e4,PLANE_SHUTDOWN_KILL_WAIT_MS=3e3,PLANE_SHUTDOWN_POLL_MS=250;async function shutdownPlane(repoRoot,deps){let graceMs=deps.graceMs??PLANE_SHUTDOWN_GRACE_MS,killWaitMs=deps.killWaitMs??PLANE_SHUTDOWN_KILL_WAIT_MS,pollMs=deps.pollIntervalMs??PLANE_SHUTDOWN_POLL_MS,read=await readPlaneManifest(repoRoot,deps.fs);if(read.kind==="missing")return{ok:!0,outcome:"no-plane",escalated:!1,manifestCleared:!1,members:[],messages:["No plane manifest found \u2014 nothing to wind down."]};if(read.kind!=="valid")return{ok:!1,reason:"unvalidated-manifest",message:`.bridge/plane/plane.json could not be validated (${read.error}). No process was signalled. Inspect the file and remove it by hand once you have confirmed no plane is running.`,members:[]};let manifest=read.manifest,messages=[];if(!isAnythingAlive(manifest,deps.proc,deps.selfPid)){let cleared2=await clearPlaneManifest(repoRoot,manifest.planeId,deps.fs);return cleared2.ok?{ok:!0,outcome:"already-dead",escalated:!1,manifestCleared:cleared2.removed,members:reportMembers(manifest,deps.proc),messages:["Every recorded plane process was already gone; manifest cleared."]}:{ok:!1,reason:"clear-failed",message:cleared2.message,members:reportMembers(manifest,deps.proc)}}messages.push(deps.selfPid===void 0?`Sending SIGTERM to plane process group ${manifest.supervisorPgid}.`:"Sending SIGTERM to every plane member."),signalPlane(manifest,"SIGTERM",deps.proc,deps.selfPid),await waitForDeath(manifest,deps,graceMs,pollMs);let escalated=!1;isAnythingAlive(manifest,deps.proc,deps.selfPid)&&(escalated=!0,messages.push("Grace period elapsed with survivors \u2014 escalating to SIGKILL."),signalPlane(manifest,"SIGKILL",deps.proc,deps.selfPid),await waitForDeath(manifest,deps,killWaitMs,pollMs));let members=reportMembers(manifest,deps.proc);if(isAnythingAlive(manifest,deps.proc,deps.selfPid)){let survivors=members.filter(m=>m.liveness!=="dead").map(m=>`${m.name}(pid ${m.pid??"?"}, ${m.liveness})`);return deps.selfPid!==manifest.supervisorPid&&deps.proc.isAlive(manifest.supervisorPid)!=="dead"&&survivors.push(`supervisor(pid ${manifest.supervisorPid})`),{ok:!1,reason:"survivors",message:`wind-down could not confirm every process was terminated: ${survivors.join(", ")}. The manifest was RETAINED so \`plane down\` can be retried.`,members}}let cleared=await clearPlaneManifest(repoRoot,manifest.planeId,deps.fs);return cleared.ok?{ok:!0,outcome:"terminated",escalated,manifestCleared:cleared.removed,members,messages}:{ok:!1,reason:"clear-failed",message:cleared.message,members}}function signalPlane(manifest,signal,proc,selfPid){selfPid===void 0&&(proc.signal(-manifest.supervisorPgid,signal),proc.signal(manifest.supervisorPid,signal));for(let member of manifest.members)member.pid===null||member.pid===selfPid||member.state!=="exited"&&proc.signal(member.pid,signal)}function isAnythingAlive(manifest,proc,selfPid){return manifest.supervisorPid!==selfPid&&proc.isAlive(manifest.supervisorPid)!=="dead"?!0:probeManifestMembers(manifest,proc).some(probe=>probe.member.pid!==selfPid&&probe.liveness!=="dead")}function reportMembers(manifest,proc){return probeManifestMembers(manifest,proc).map(probe=>({name:probe.member.name,pid:probe.member.pid,liveness:probe.liveness,exitCode:probe.member.exitCode,exitSignal:probe.member.exitSignal}))}async function waitForDeath(manifest,deps,budgetMs,pollMs){let waited=0;for(;waited<budgetMs;){if(!isAnythingAlive(manifest,deps.proc,deps.selfPid))return;await deps.clock.sleep(pollMs),waited+=pollMs}}function formatPlaneShutdown(result){let lines=[];if(!result.ok)lines.push(`plane down FAILED: ${result.message}`);else for(let message of result.messages)lines.push(message);for(let member of result.members){let detail=member.exitSignal!==null?`signal ${member.exitSignal}`:member.exitCode!==null?`exit ${member.exitCode}`:"no recorded exit status";lines.push(` ${member.name.padEnd(12)} ${member.liveness.padEnd(7)} (${detail})`)}return result.ok&&result.manifestCleared&&lines.push("Manifest cleared."),lines.join(`
|
|
5634
5634
|
`)}import path35 from"path";function memberLogFilename(member){return`${member}.log`}function openMemberLog(member,planeDir,absolutePath,deps){let expected=`${planeDir}/${memberLogFilename(member)}`;if(absolutePath.split("\\").join("/")!==expected.split("\\").join("/"))return{ok:!1,error:`refusing to open a log outside the plane directory for member '${member}'`};try{return{ok:!0,stream:deps.openAppendStream(absolutePath)}}catch(err){let code=err?.code;return{ok:!1,error:typeof code=="string"?code:"log could not be opened"}}}function openPlaneRuntimeLog(planeDir,absolutePath,deps){let expected=`${planeDir}/${PLANE_RUNTIME_LOG_FILENAME}`;if(absolutePath.split("\\").join("/")!==expected.split("\\").join("/"))return{ok:!1,error:"refusing to open a runtime trace outside the plane directory"};try{return{ok:!0,stream:deps.openAppendStream(absolutePath)}}catch(err){let code=err?.code;return{ok:!1,error:typeof code=="string"?code:"runtime trace could not be opened"}}}var PLANE_MEMBER_STARTUP_FAILURES={spawnThrew:"the process could not be started",noPid:"the process started without a pid",readinessTimeout:"it did not start listening in time"},ALLOWED_STARTUP_FAILURES=new Set(Object.values(PLANE_MEMBER_STARTUP_FAILURES)),PLANE_MEMBER_STARTUP_FAILURE_FALLBACK="it failed to start",PLANE_SUPERVISOR_LOG_PREFIX="[plane supervisor]";function writeMemberStartupFailure(log,member,reason){let safe=ALLOWED_STARTUP_FAILURES.has(reason)?reason:PLANE_MEMBER_STARTUP_FAILURE_FALLBACK;try{log.write(`${PLANE_SUPERVISOR_LOG_PREFIX} ${member} did not start: ${safe}
|