@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.
@@ -6,7 +6,7 @@ export const COMMANDS = {
6
6
  "clarify-ticket.md": "Generate clarifying questions for a Jira ticket and save them locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is a required Jira ticket key (e.g., `PROJ-123`). This command generates clarifying questions for the ticket and saves them locally.\n\nIf any step fails, stop immediately and report which step failed and why.\n\n## Step 1 — Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = \"auto\"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: \"Usage error: --provider requires a provider name (openai, anthropic, or gemini).\"\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate ticket key format**: Confirm the ticket key matches the Jira key pattern `[A-Z][A-Z0-9]+-\\d+`. If it does not match, stop immediately and report: \"Invalid ticket key format. Expected a Jira key like PROJ-123.\"\n\n## Step 2 — Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 — Generate Clarifying Questions\n\nCall the `request_clarifying_questions` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nIf the tool returns an error, stop immediately and report: \"Clarifying questions generation failed.\" Include the error details.\n\n## Final Report\n\nOn successful completion, display:\n\n> **Ticket Key**: {ticket_key}\n>\n> **Local File Path**: {docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md\n>\n> **Status**: The clarifying questions document has been saved locally. No changes were pushed to Jira.\n>\n> To incorporate these findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n",
7
7
  "code-ticket.md": "# Code Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), downloads the AI-generated implementation plan and clarifying questions via MCP tools, then executes the plan step by step directly in the main conversation so all progress is visible.\n\nIf any critical stage fails (Stage 0, Stage 1, or Stage 3), stop immediately and report which stage failed and why. Non-critical stages (Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to implement a Jira ticket using an AI-generated plan. Execute all stages in sequence.\n\n## Stage 0 — Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` — one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: '<value>'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /code-ticket <ticket_key> (e.g., /code-ticket BAPI-150)\n ```\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `\"status\": \"ok\"`, stop immediately and display:\n\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor's MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — Download Implementation Plan\n\nCall the `get_plan` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `true`\n\nInspect the response for errors. If the response text contains `NOT_FOUND` or `404` or indicates the plan was not found, stop immediately and display:\n\n```\nNo implementation plan found for <ticket_key>. Run `/plan-ticket <ticket_key>` first to generate one,\nor use the `request_plan_generation` MCP tool with `wait_for_result: true`.\n```\n\nOn success, read and internalize the full plan content. This is the plan you will execute in Stage 3.\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 — Download Clarifying Questions\n\nCall the `get_clarifying_questions` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `false`\n\nIf the response contains `NOT_FOUND` or `404` or indicates no clarifying questions were found, log a warning note:\n\n```\nWarning: No clarifying questions found for <ticket_key>. Proceeding without supplementary context.\n```\n\nDo **NOT** stop the pipeline. Clarifying questions are supplementary context, not a hard prerequisite for implementation.\n\nOn success, internalize the clarifying questions content. Reference these for additional context where relevant to implementation steps — the answers provide supplementary guidance on requirements and technical decisions.\n\nThis stage is **non-critical** — warn on failure, continue to Stage 3 regardless.\n\n## Stage 3 — Execute Implementation Plan\n\nExecute the implementation plan step by step, directly in this conversation. Work inline so the user can see all progress and approve tool calls.\n\nFollow these rules:\n\n1. **Execute the plan in order.** Do not skip any steps, especially review steps involving test execution, lint checks, and architectural verification.\n2. **Make code changes** as directed by each step in the plan.\n3. **Run tests and checks** as specified in the plan's review steps.\n4. **Do NOT run `git commit` or `git push`.** Leave all changes uncommitted for developer review.\n5. **If a step is ambiguous or blocked**, note the issue clearly and continue with the next step rather than halting entirely.\n6. **Reference clarifying questions** (if retrieved in Stage 2) when they provide relevant context for a given step.\n7. **If a specific plan step or requirement remains ambiguous** after consulting the plan and any retrieved clarifying questions, call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key` from Stage 0 to fetch the live Jira ticket details. Use only the fields relevant to resolving that ambiguity, then continue with the affected step. Do not call `get_ticket` unconditionally or as a prerequisite — only when a step's requirement is genuinely unclear.\n\nThis stage is **critical** — if a blocking error prevents further progress, stop and report the failure.\n\n## Stage 4 — Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Implementation Complete\n\n**Ticket**: <ticket_key>\n\n**Developer Action Items**:\n- All changes are uncommitted. Review the changes with `git diff` before committing.\n- Run the project's test suite to verify nothing is broken before committing.\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 2: no clarifying questions),\nlist them here so the developer has full visibility. If no warnings, omit this section.>\n```\n\n## Final Report\n\nOn success, display the structured report from Stage 4 confirming that implementation of the ticket is complete.\n\nOn failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.\n",
8
8
  "commit-ticket.md": "# Commit Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), verifies the current git branch matches the ticket, identifies implementation files by cross-referencing git changes against the saved implementation plan, and commits and pushes the work. It is designed to run after `/code-ticket` completes.\n\nIf any critical stage fails (Stage 0, Stage 1, or Stage 3), stop immediately and report which stage failed and why. Non-critical stages (Stage 2, Stage 4, Stage 5, and Stage 6) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 7-stage pipeline to commit and push implementation work for a Jira ticket. Execute all stages in sequence.\n\n## Stage 0 — Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` — one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: '<value>'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /commit-ticket <ticket_key> (e.g., /commit-ticket BAPI-150)\n ```\n\n2. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n3. **Verify uncommitted changes exist**: Run `git status --porcelain` in the terminal. If the output is empty (no modified, added, or untracked files), stop immediately and display:\n\n ```\n No uncommitted changes found. Nothing to commit for <ticket_key>.\n ```\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — Branch Verification and Creation\n\n1. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `current_branch`.\n\n2. **Check branch match**: Determine if `current_branch` contains the `ticket_key` (case-insensitive comparison). For example, if the ticket key is `BAPI-150`, branch `feature/BAPI-150-add-caching` matches, as does `feature/BAPI-150` or `bugfix/bapi-150-fix`.\n\n3. **If the branch matches**: Log a confirmation message and proceed:\n\n ```\n Branch '<current_branch>' matches ticket <ticket_key>. Proceeding.\n ```\n\n4. **If the branch does NOT match**: Create a new branch from the current HEAD in the format `feature/<ticket_key>` (e.g., `feature/BAPI-150`). Run `git checkout -b feature/<ticket_key>` in the terminal. If the branch creation fails (e.g., branch already exists), try `git checkout feature/<ticket_key>` instead. If both fail, stop immediately and display:\n\n ```\n Failed to create or switch to branch 'feature/<ticket_key>'.\n Please resolve the branch situation manually and re-run.\n ```\n\n On success, log:\n\n ```\n Created and switched to new branch 'feature/<ticket_key>'.\n ```\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 — Identify and Stage Implementation Files\n\n1. **Collect git changes**: Run `git status --porcelain` in the terminal. Parse the output to build two lists:\n - `modified_files`: files with status `M`, `MM`, `AM`, or `A` (modified or staged)\n - `untracked_files`: files with status `??` (new untracked files)\n\n Combine into a single list `all_changed_files`.\n\n2. **Load the implementation plan**: Look for the implementation plan file at `{docs_dir}/plans/{ticket_key}-plan.md`. Read the file.\n\n - **If the plan file exists**: Extract file paths mentioned in the plan. Look for patterns like backtick-quoted paths (e.g., `src/python/foo.py`), file references in step descriptions, and any explicit file listings. Build a list `plan_files` of all file paths referenced in the plan.\n\n - **If the plan file does NOT exist**: Log a warning:\n\n ```\n Warning: No implementation plan found at {docs_dir}/plans/{ticket_key}-plan.md.\n Cannot cross-reference changes against plan. Will present all changed files for review.\n ```\n\n Set `plan_files` to an empty list.\n\n3. **Classify changed files**: For each file in `all_changed_files`, classify it into one of three categories:\n\n - **Plan-matched**: The file path appears in `plan_files` (exact match or the plan references a parent directory). These are high-confidence implementation files.\n - **Likely related**: The file is not explicitly in the plan but is a test file for a plan-matched file, a migration file, an `__init__.py` in a directory with plan-matched files, or otherwise clearly related to the implementation (e.g., `requirements.txt` if the plan mentions adding a dependency).\n - **Ambiguous**: The file does not appear related to the plan. These may be pre-existing uncommitted changes.\n\n4. **Present file list for user confirmation**: Display the classified file list to the user:\n\n ```\n ## Files to Commit for <ticket_key>\n\n ### Plan-matched files (high confidence):\n - path/to/file1.py\n - path/to/file2.py\n\n ### Likely related files:\n - tests/pytest/routes/test_file1.py\n - db/alembic/versions/xxxx_migration.py\n\n ### Ambiguous files (not referenced in plan):\n - some/other/file.py\n\n Shall I proceed with committing all listed files?\n If you want to exclude any files, please specify which ones to remove.\n ```\n\n If `plan_files` is empty (plan not found), display all files under a single \"All changed files\" heading instead.\n\n5. **Wait for user confirmation**: The user may:\n - Approve all files (proceed)\n - Specify files to exclude (remove those from the commit list)\n - Cancel entirely (stop the pipeline)\n\n If the user cancels, stop immediately and display:\n\n ```\n Commit cancelled by user. No files were staged or committed.\n ```\n\n6. **Stage the approved files**: Run `git add <file1> <file2> ...` in the terminal, listing only the approved files explicitly. Do NOT use `git add -A` or `git add .`.\n\nThis stage is **non-critical** if the plan file is not found (warn and continue with all files). It is **critical** if the user cancels or if `git add` fails — stop immediately on those failures.\n\n## Stage 3 — Commit and Push\n\n1. **Generate commit message**: Based on the staged files and the implementation plan (if available), generate a concise commit message. The message must:\n - Start with a brief summary line (under 72 characters) that references the ticket key\n - Format: `<ticket_key>: <brief description of changes>`\n - Example: `BAPI-150: Add rate limiting to LLM client`\n - If the plan was available, derive the description from the plan's title or objective\n - If the plan was not available, summarize based on the file names and `git diff --staged` output\n\n2. **Commit**: Run `git commit -m \"<message>\"` in the terminal. If the commit fails due to a pre-commit hook, report the hook output and stop:\n\n ```\n Commit failed due to pre-commit hook. Hook output:\n <hook output>\n\n Please fix the issues and re-run /commit-ticket <ticket_key>.\n ```\n\n3. **Push to remote**: Run `git push -u origin <current_branch>` in the terminal. The `-u` flag sets up upstream tracking. If the push fails, stop immediately and display:\n\n ```\n Push failed. Error:\n <error output>\n\n The commit was created locally. You can push manually with:\n git push -u origin <current_branch>\n ```\n\nThis stage is **critical** — stop immediately on failure.\n\n## Stage 4 — Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Commit Complete\n\n**Ticket**: <ticket_key>\n**Branch**: <current_branch>\n**Commit**: <commit_hash> (from `git rev-parse --short HEAD`)\n**Files committed**: <count> files\n**Remote**: Pushed to origin/<current_branch>\n\n**Committed files**:\n- path/to/file1.py\n- path/to/file2.py\n- ...\n\n**Warnings**:\n<If any non-critical warnings occurred (Stage 2: plan not found),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** — display the report regardless.\n\n## Stage 5 — Jira Status Transition\n\nThis stage attempts to transition the Jira ticket to the appropriate post-PR status.\n\n1. **Resolve target status**: Call the `resolve_target_status` MCP tool with `ticket_number` set to the `ticket_key`. This returns the cached or LLM-resolved target status for the project.\n\n2. **Attempt transition**: If `resolve_target_status` returned a non-null `target_status`, call the `update_jira_status` MCP tool with `ticket_number` set to the `ticket_key` and `target_status` set to the resolved value. If the ticket is already in the target status, this is a no-op.\n\n3. **On success**: Display `\"Ticket status updated: <from_status> -> <to_status>\"`.\n\n4. **On failure or not applicable**: Display a warning but do not stop the pipeline:\n - If `resolve_target_status` returned null: `\"Ticket status transition skipped: no target status configured for this project\"`\n - If `update_jira_status` failed: `\"Ticket status transition skipped: <error message>\"`\n\nThis stage is **non-critical** — log a warning on failure but do not stop the pipeline.\n\n## Stage 6 — Smoke Test Validation Comment\n\nThis stage reviews the implementation against the ticket requirements and posts a comment if manual validation is needed.\n\n1. **Fetch ticket description**: Call the `get_ticket` MCP tool with `ticket_number` set to the `ticket_key` to retrieve the current ticket requirements.\n\n2. **Review implementation**: Compare the implementation (from the plan loaded in Stage 2 and the files committed in Stage 3) against the ticket requirements. Identify any behavior or requirements that could NOT be validated through the automated tests written during implementation or through code review alone. Consider the limitations of any tests that were written: what functionality or behavior could not be validated by those tests? Examples include: requirements involving visual UI rendering or layout checks, third-party system integrations where mock tests are insufficient, or non-deterministic behaviors.\n\n3. **If untestable requirements exist**: Compose a structured comment describing specific manual validation steps stakeholders should perform. Then call the `add_comment` MCP tool with `ticket_number` set to the `ticket_key` and the comment text. Display: `\"Smoke test validation comment posted to <ticket_key>\"`.\n\n4. **If no untestable requirements exist**: Skip silently. Display: `\"No untestable requirements identified — skipping smoke test comment\"`.\n\nThis stage is **non-critical** — log a warning on failure but do not stop the pipeline.\n\n## Final Report\n\nOn 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.\n\nOn failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.\n",
9
- "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 — 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 — every tick reconciles from scratch.\n\n## Stage 0 — 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,…>`** (and the equals form `--tickets=<K1,K2,…>`): a non-empty, comma-separated, **ordered** list of ticket keys. Preserve the caller's order exactly — 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 — 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`–`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,… 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 — before Stage 1 initialization, before any CLI invocation, and before any state is written — 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 — 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 — the epic key, a ticket key, a branch name, a checkpoint path, a prompt-file path, a JSON blob, a journal line — 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** — `<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** — `<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** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — 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`** — 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,…>'\n ```\n\n Print the initialization preflight output **verbatim** — 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`** — 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) — 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 — 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 — 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` — 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` — `number`, `state` (`OPEN`, `MERGED`, `CLOSED`), `head_sha`, `base`, `mergeable`, `merge_state`, `updated_at`.\n- `ci` — `required`, `complete`, `stable_across_two_polls`, `head_sha`, and `checks` entries of `name`, `status`, `conclusion`, `required`.\n- `review` — `opted_in`, `source`, `available`, `verdict` (`approved`, `changes_requested`, `unknown`), `head_sha`.\n- `parse` — `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 — in particular, missing timestamps can never satisfy Row 5's causal check.\n- `deadlines` — `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\n- `lock` — `held_by_me`, `owner_pid`, `host`, `alive`.\n- `needs_human` — `null`, or `reason`, `evidence`, `at`.\n- `probe_errors` — 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 — 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 — 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 — even to record a park — is the two-authorities corruption the lock exists to prevent, and `checkpoint set` refuses a live foreign lock anyway.\n\n## Stage 3 — 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 — 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 — `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** — a human reviews and merges the epic into `main`. Then stop.\n\n### Row 2 — 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** — 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 — 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 — 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 — 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 — `/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 — 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** — 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 — 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 — 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 — which is the whole point: the next ticket's review and plan must see this ticket's merged code.\n\n### Row 6 — 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 — 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** — 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 — 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** — a required check with a terminal unsuccessful conclusion is **not** consumed here and falls through to Row 9.\n\n### Row 9 — 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` — the entries whose `required` is `true` — 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 — 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 — 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 — 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 — 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 — 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 — 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** — 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`** — 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`** — 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`** — 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`** — 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** — `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 — 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** — a closed pull request is never respawned into, reopened, or merged by this loop.\n\n### Row 14 — 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 — 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 — no MCP tool call, no spawn, no merge, no parse — 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** — 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 — 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 — a single per-ticket `respawns` counter, not one allowance per row — 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` — 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 — 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 — 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> … --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 — 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 — 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 — 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 — a merge envelope, a command's output — 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 — 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 — 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 — the epic key, the ticket key, the checkpoint path, the `needs_human` JSON, and the journal line are each escaped (`'` → `'\\''`) 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 — 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",
9
+ "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 — 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 — 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 — 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,…>`** (and the equals form `--tickets=<K1,K2,…>`): a non-empty, comma-separated, **ordered** list of ticket keys. Preserve the caller's order exactly — 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 — 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`–`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,… 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 — before Stage 1 initialization, before any CLI invocation, and before any state is written — 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 — 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 — the epic key, a ticket key, a branch name, a checkpoint path, a prompt-file path, a JSON blob, a journal line — 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** — `<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** — `<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** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — 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`** — 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,…>'\n ```\n\n Print the initialization preflight output **verbatim** — 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`** — 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) — 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 — 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 — 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` — 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` — `number`, `state` (`OPEN`, `MERGED`, `CLOSED`), `head_sha`, `base`, `mergeable`, `merge_state`, `updated_at`.\n- `ci` — `required`, `complete`, `stable_across_two_polls`, `head_sha`, and `checks` entries of `name`, `status`, `conclusion`, `required`.\n- `review` — `opted_in`, `source`, `available`, `verdict` (`approved`, `changes_requested`, `unknown`), `head_sha`.\n- `parse` — `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 — in particular, missing timestamps can never satisfy Row 5's causal check.\n- `deadlines` — `soft_seconds`, `hard_seconds`, `elapsed_since_spawn_seconds` (defaults 3600 and 10800).\n- `lock` — `held_by_me`, `owner_pid`, `host`, `alive`.\n- `needs_human` — `null`, or `reason`, `evidence`, `at`.\n- `probe_errors` — 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 — 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** — 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** — 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 — 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 — even to record a park — is the two-authorities corruption the lock exists to prevent, and `checkpoint set` refuses a live foreign lock anyway.\n\n## Stage 3 — 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 — 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 — `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** — 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 — 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 — 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** — 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 — 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 — 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 — 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 — a parse that is queued or in progress, a non-causal `succeeded` or `failed`, an inconsistent request record — 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 — 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 — `/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 — 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** — 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 — 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 — about four hours at a five-minute cadence — 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** — `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` — an unavailable `pr` probe, or a pull request record without a head — **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** — a human-readable record of what the fields already say durably — 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 — it falls to branch 6 and waits.\n\n6. **A request is recorded and none of branches 2–5 matched** — 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 — which is the whole point: the next ticket's review and plan must see this ticket's merged code.\n\n### Row 6 — 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 — it falls through to Row 15 and waits.\n\n### Row 7 — 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** — 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 — 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** — a required check with a terminal unsuccessful conclusion is **not** consumed here and falls through to Row 9.\n\n### Row 9 — 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` — the entries whose `required` is `true` — 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 — 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 — 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 — 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 — 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 — 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 — 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 — 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** — 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`** — 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`** — 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`** — 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`** — 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** — `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 — 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** — a closed pull request is never respawned into, reopened, or merged by this loop.\n\n### Row 14 — 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 — 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 — no MCP tool call, no spawn, no merge, no parse — 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** — 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 — 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 — a single per-ticket `respawns` counter, not one allowance per row — 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` — 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 — 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 — 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 — as the single stated exception to the rule below — 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> … --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 — 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 — 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 — 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 — a merge envelope, a command's output — 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 — 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 — 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 — the epic key, the ticket key, the checkpoint path, the `needs_human` JSON, and the journal line are each escaped (`'` → `'\\''`) 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 — 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",
10
10
  "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 — 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 — Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 — 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 — Confirm Success\n\nResolve the local file path from `doc_type`:\n- `tdd` → `{docs_dir}/architecture/<ticket_key>-architecture-plan.md`\n- `fsd` → `{docs_dir}/fsd/<ticket_key>-fsd-plan.md`\n- `prd` → `{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",
11
11
  "create-pr.md": "# Create PR: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), fetches the ticket summary, determines the base branch, and creates a pull request on the configured VCS provider. It is designed to run after `/commit-ticket` completes.\n\nIf any critical stage fails (Stage 0), stop immediately and report which stage failed and why. Non-critical stages (Stage 1 and Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 3-stage pipeline to create a pull request for a Jira ticket. Execute all stages in sequence.\n\n## Stage 0 — Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` — one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: '<value>'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /create-pr <ticket_key> (e.g., /create-pr BAPI-150)\n ```\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `\"status\": \"ok\"`, stop immediately and display:\n\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor's MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n3. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `head_branch`. Verify that `head_branch` contains the `ticket_key` (case-insensitive comparison). If the branch does not contain the ticket key, stop immediately and display:\n\n ```\n Current branch '<head_branch>' does not contain ticket key <ticket_key>.\n Please switch to the correct feature branch before running /create-pr.\n ```\n\n4. **Resolve base branch**: Resolve the base through this ordered precedence and take the first tier that yields a usable value.\n\n 1. **`BAPI_BASE_BRANCH` from the environment, when set and non-empty.** Read it first, explicitly, with Bash — never infer the base from branch ancestry or the repository default branch:\n\n ```bash\n echo \"${BAPI_BASE_BRANCH:-}\"\n ```\n\n The `:-` form returns an empty line when the variable is unset, so the read never fails the stage. `start-tickets` exports this variable into a worker's shell whenever the run has a non-default base (an epic branch, for example), so it must win over the repository-wide configured value.\n 2. **The repository's configured base branch** — only when the environment value is unset. Call the `config_field` MCP tool with `operation` set to `\"get\"` and `field_name` set to `base_branch`.\n 3. **`main`** — the expected fallback default.\n\n Treat a null, empty, or whitespace-only value, an HTTP 400 Validation Error / Invalid field name, or any lookup error as not set, and fall back to `main` rather than failing the stage. Store the resolved value as `base_branch`.\n\n5. **Fetch ticket summary**: Call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key`. Extract the ticket summary from the response. If the tool returns an error, log a warning and use a generic summary based on the ticket key.\n\n6. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — Create Pull Request\n\n1. **Compose PR title**: Format the title as `<ticket_key>: <ticket_summary>`. Truncate to 72 characters if needed.\n\n2. **Compose PR body**: Build a PR body that includes, in this order:\n - A brief description derived from the ticket summary\n - A plain text reference to the local implementation plan: `Implementation Plan available locally at {docs_dir}/plans/{ticket_key}-plan.md` (do not use markdown hyperlink syntax — the local path is sufficient for team members pulling the branch)\n - The checklist text of `.github/PULL_REQUEST_TEMPLATE.md`, read from the current worktree when that file exists and appended after the plan reference without rewriting its markdown structure. Omit this part when the file is absent. GitHub's REST API does not automatically apply the repository pull request template — it is a web-UI affordance — so the checklist must be inlined into the body here or the created PR has none.\n\n3. **Create the pull request**: Call the `create_pull_request` MCP tool with:\n - `head_branch`: the current branch from Stage 0\n - `base_branch`: the resolved base branch from Stage 0\n - `title`: the composed PR title\n - `body`: the composed PR body\n\n4. **Handle the response with graceful degradation**:\n - If the response contains `available: false`: Report the reason to the user and skip to Stage 2. Do not halt the pipeline.\n - If the response contains `created: false`: Log \"PR already exists\" and store the returned PR URL. Continue to Stage 2.\n - If the response contains `created: true`: Store the PR URL. Continue to Stage 2.\n - If an HTTP error occurs: Warn the user with the error details and continue to Stage 2. Do not halt the pipeline.\n\nThis stage is **non-critical** — warn on failure, continue to Stage 2 regardless.\n\n## Stage 2 — Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Pull Request Report\n\n**Ticket**: <ticket_key>\n**Branch**: <head_branch>\n**Base Branch**: <base_branch>\n**PR URL**: <pr_url or \"N/A — see warnings\">\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 1: PR creation failed or unavailable),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** — display the report regardless.\n\n## Final Report\n\nOn success, display the structured report from Stage 2 confirming that the pull request was created (or already existed), including the branch name, base branch, PR URL, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0), display which stage failed and the error details.\n",
12
12
  "critique-ticket.md": "Generate a ticket quality critique and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command triggers an AI-powered critique of a Jira ticket and saves the result locally. **No human confirmation gates** — the command runs end-to-end without pausing. `$ARGUMENTS` should contain a single Jira ticket key in `PROJECT-NUMBER` format (e.g., `BAPI-123`).\n\nIf any step fails, stop immediately and report which step failed and why.\n\n## Step 1 — Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = \"auto\"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: \"Usage error: --provider requires a provider name (openai, anthropic, or gemini).\"\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate the ticket key format**: Validate that `ticket_key` matches the regex pattern `^[A-Za-z][A-Za-z0-9]+-\\d+$`. If validation fails, stop immediately and report: \"The argument does not match the expected `PROJECT-NUMBER` format. Example: `BAPI-123`.\"\n\n## Step 2 — Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 — Generate Critique\n\nCall the `request_ticket_critique` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nIf the tool returns an error, stop immediately and report: \"Critique generation failed.\" Include the error details.\n\n## Final Report\n\n**On success**, display a summary including:\n\n- Path to the saved critique document: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nNote: The critique was NOT pushed to Jira. To incorporate the critique findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n\n**On failure at any step**, stop immediately and display the step that failed and the error details.\n",
@@ -87,6 +87,39 @@ export function resolveConductEpicPromptsDirectory(repoName, epicKey, deps) {
87
87
  function isRecord(value) {
88
88
  return typeof value === "object" && value !== null && !Array.isArray(value);
89
89
  }
90
+ /**
91
+ * Supply `null` for parse-request fields a pre-BAPI-825 version-1 ticket does
92
+ * not carry, returning a NEW document rather than mutating the input.
93
+ *
94
+ * The two fields were added without incrementing
95
+ * {@link CONDUCT_EPIC_CHECKPOINT_VERSION}, because bumping the version would
96
+ * make every checkpoint written by an in-flight pilot run instantly
97
+ * `unsupported-version` — a hard error whose documented recovery is a human
98
+ * editing the file. Normalizing an absent field to `null` is the compatible
99
+ * alternative: an old document stays valid, and every downstream TypeScript
100
+ * reader sees a real `null` instead of `undefined`.
101
+ *
102
+ * Only ABSENT keys are filled. A key that is present but malformed is left
103
+ * exactly as it is, so validation still rejects it rather than having it
104
+ * quietly repaired into a legal value.
105
+ */
106
+ export function normalizeConductEpicCheckpoint(value) {
107
+ if (!isRecord(value) || !Array.isArray(value.tickets))
108
+ return value;
109
+ return {
110
+ ...value,
111
+ tickets: value.tickets.map((ticket) => {
112
+ if (!isRecord(ticket))
113
+ return ticket;
114
+ const normalized = { ...ticket };
115
+ if (!("parse_requested_at" in normalized))
116
+ normalized.parse_requested_at = null;
117
+ if (!("parse_requested_for_sha" in normalized))
118
+ normalized.parse_requested_for_sha = null;
119
+ return normalized;
120
+ }),
121
+ };
122
+ }
90
123
  /** A non-empty string. Blank-only values are rejected everywhere. */
91
124
  function isText(value) {
92
125
  return typeof value === "string" && value.trim().length > 0;
@@ -147,6 +180,14 @@ function validateTicket(value, index) {
147
180
  const counters = validateCounters(value.counters, where);
148
181
  if (!counters.ok)
149
182
  return counters;
183
+ // Present-but-malformed is rejected; ABSENT is impossible here, because
184
+ // `normalizeConductEpicCheckpoint` runs before every validation and fills a
185
+ // missing parse-request field with `null` (BAPI-825/A2).
186
+ for (const key of ["parse_requested_at", "parse_requested_for_sha"]) {
187
+ if (!isNullableText(value[key])) {
188
+ return fail(`${where}.${key} must be a non-empty string or null`);
189
+ }
190
+ }
150
191
  if (!Array.isArray(value.journal) || value.journal.some((line) => typeof line !== "string")) {
151
192
  return fail(`${where}.journal must be an array of strings`);
152
193
  }
@@ -203,8 +244,14 @@ function validateLock(value) {
203
244
  * this guards against is a partially-hand-edited checkpoint (an operator
204
245
  * unparking a run edits this file by hand) being accepted because its top-level
205
246
  * keys still exist, and then driving the loop from a nonsense value.
247
+ *
248
+ * The candidate is passed through {@link normalizeConductEpicCheckpoint} first,
249
+ * so a version-1 document written before the parse-request fields existed
250
+ * validates unchanged. Normalization only fills ABSENT keys, so it can never
251
+ * launder a malformed present value past the rules below.
206
252
  */
207
- export function validateConductEpicCheckpoint(value) {
253
+ export function validateConductEpicCheckpoint(candidate) {
254
+ const value = normalizeConductEpicCheckpoint(candidate);
208
255
  if (!isRecord(value))
209
256
  return fail("checkpoint must be a JSON object");
210
257
  if (value.version !== CONDUCT_EPIC_CHECKPOINT_VERSION) {
@@ -301,7 +348,14 @@ export async function readConductEpicCheckpoint(checkpointPath, fs) {
301
348
  if (!validation.ok) {
302
349
  return { kind: "invalid", error: `checkpoint at ${checkpointPath} is invalid: ${validation.error}` };
303
350
  }
304
- return { kind: "ok", checkpoint: parsed };
351
+ // Return the NORMALIZED document, not the parsed one. Validation normalizes a
352
+ // private copy, so handing back `parsed` would give every caller `undefined`
353
+ // where the interface promises `string | null` — and `undefined` is exactly
354
+ // the value a `!== null` causal check reads as "a request was recorded".
355
+ return {
356
+ kind: "ok",
357
+ checkpoint: normalizeConductEpicCheckpoint(parsed),
358
+ };
305
359
  }
306
360
  /** Build the initial document: every ticket `pending`, every counter zero. */
307
361
  export function createInitialConductEpicCheckpoint(input) {
@@ -328,6 +382,8 @@ export function createInitialConductEpicCheckpoint(input) {
328
382
  respawns: 0,
329
383
  conflict_attempts: 0,
330
384
  counters: { sessions_spawned: 0, plan_generations_observed: 0, merge_attempts: 0 },
385
+ parse_requested_at: null,
386
+ parse_requested_for_sha: null,
331
387
  journal: [],
332
388
  })),
333
389
  counters: { iterations: 0, merges: 0 },
@@ -55,6 +55,8 @@ const TICKET_FIELDS = [
55
55
  "branch",
56
56
  "pr_number",
57
57
  "spawned_at",
58
+ "parse_requested_at",
59
+ "parse_requested_for_sha",
58
60
  "respawns",
59
61
  "conflict_attempts",
60
62
  "counters.sessions_spawned",
@@ -152,6 +154,8 @@ export function getConductEpicUsage() {
152
154
  " Apply ABSOLUTE field values (the caller computes n+1 from status).",
153
155
  ` Ticket fields: ${TICKET_FIELDS.join(", ")}.`,
154
156
  ` Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`,
157
+ " Repeat --field to write several in ONE atomic mutation — parse_requested_at",
158
+ " and parse_requested_for_sha are recorded together, never in two writes.",
155
159
  "",
156
160
  " finish <EPIC> [--checkpoint-path <p>] [--json]",
157
161
  " Restore the server's indexed base branch (idempotent), release the owned",
@@ -1106,18 +1110,7 @@ export async function runConductEpicStatus(deps, options) {
1106
1110
  checkpoint_path: checkpointPath,
1107
1111
  checkpoint_exists: true,
1108
1112
  all_done: allDone,
1109
- ticket: ticket === null
1110
- ? null
1111
- : {
1112
- key: ticket.key,
1113
- status: ticket.status,
1114
- branch: discoveredBranch,
1115
- pr_number: ticket.pr_number,
1116
- spawned_at: ticket.spawned_at,
1117
- respawns: ticket.respawns,
1118
- conflict_attempts: ticket.conflict_attempts,
1119
- counters: { ...ticket.counters },
1120
- },
1113
+ ticket: ticket === null ? null : projectConductEpicTicketFacts(ticket, discoveredBranch),
1121
1114
  worktree_path: worktreePath,
1122
1115
  worktree_exists: worktreeExists,
1123
1116
  branch_head: branchHead,
@@ -1141,6 +1134,30 @@ export async function runConductEpicStatus(deps, options) {
1141
1134
  };
1142
1135
  return emitSuccess(deps, options.json, payload);
1143
1136
  }
1137
+ /**
1138
+ * Project one checkpoint ticket into the published `ticket` facts.
1139
+ *
1140
+ * `branch` comes from the caller's freshly discovered value rather than the
1141
+ * stored one, matching the rest of the status contract. `journal` is copied so
1142
+ * a consumer of the payload can never reach back into checkpoint state — the
1143
+ * status verb's write allowlist is deliberately narrow, and handing out a live
1144
+ * array reference would be a fifth, undeclared write path.
1145
+ */
1146
+ export function projectConductEpicTicketFacts(ticket, discoveredBranch) {
1147
+ return {
1148
+ key: ticket.key,
1149
+ status: ticket.status,
1150
+ branch: discoveredBranch,
1151
+ pr_number: ticket.pr_number,
1152
+ spawned_at: ticket.spawned_at,
1153
+ parse_requested_at: ticket.parse_requested_at,
1154
+ parse_requested_for_sha: ticket.parse_requested_for_sha,
1155
+ respawns: ticket.respawns,
1156
+ conflict_attempts: ticket.conflict_attempts,
1157
+ counters: { ...ticket.counters },
1158
+ journal: [...ticket.journal],
1159
+ };
1160
+ }
1144
1161
  /**
1145
1162
  * Poll CI for `headSha`, warming the resolve cache at most once.
1146
1163
  *
@@ -1455,6 +1472,22 @@ function applyFieldAssignment(checkpoint, ticketIndex, assignment) {
1455
1472
  ticket.spawned_at = value;
1456
1473
  return null;
1457
1474
  }
1475
+ case "parse_requested_at":
1476
+ case "parse_requested_for_sha": {
1477
+ // The durable half of the post-merge causal barrier (BAPI-825/A2). Both
1478
+ // follow the module's nullable-string convention so a tick can clear them
1479
+ // as deliberately as it sets them — an operator re-running a parse writes
1480
+ // `null` here rather than hand-editing the checkpoint.
1481
+ if (value === "null") {
1482
+ ticket[name] = null;
1483
+ return null;
1484
+ }
1485
+ if (value.trim().length === 0) {
1486
+ return `${name} must be a non-empty string or null.`;
1487
+ }
1488
+ ticket[name] = value;
1489
+ return null;
1490
+ }
1458
1491
  case "respawns":
1459
1492
  case "conflict_attempts": {
1460
1493
  const parsed = parseIntegerField(value);