@bridge_gpt/mcp-server 0.2.65 → 0.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
package/build/docs.generated.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// This file is produced by scripts/bundle-docs.js
|
|
3
3
|
export const DOCS = {
|
|
4
4
|
"docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_COUNCIL_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery AI councils. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n### AI Council (BLOCK on a code index, mode-dependent)\n\n`request_council` in **technical** or **discovery** mode searches your indexed\ncodebase, so it **BLOCK**s on `code_index`. **Design**-mode AI council requests never\nquery the index and are never gated.\n\n### Document generation that DEGRADEs (usable without codebase context)\n\nTools like `generate_prd_direct`, `generate_fsd_direct`,\n`code_writer_generate_fsd`, `generate_clarifying_questions_direct`,\n`generate_ticket_critique_direct`, `generate_ticket_review_direct`, and\n`create_doc` for **PRD** / **FSD** documents **DEGRADE** rather than block: they\nrun today from the ticket alone, and connecting VCS simply lets them ground their\noutput in your codebase. They always appear under \"Tools you can use now\", with a\nreduced-context caveat when the VCS connection is missing.\n\n### Never gated\n\nSetup and bootstrap tools (`ping`, `config_field`, `get_install_manifest`,\n`apply_install_manifest`, `persist_routing_credential`, and the\nbootstrap-invite exchange) are always available — they\nare how you configure everything else.\n",
|
|
5
|
-
"docs/install/sfcc-integration.md": "# Installing the SFCC Integration (OCAPI)\n\nBridge's Salesforce B2C Commerce (SFCC) tools give an AI coding agent read access to\na sandbox's object model, custom object definitions, and site preferences — plus a\nsmall set of sandbox-only writes — through the **OCAPI Data API**. This guide covers\nsetting up the OCAPI client that those tools authenticate against.\n\n> **Sandbox / local development only.** This integration is intended for a **developer\n> sandbox**, and that restriction is **enforced in code**: before any SFCC tool runs,\n> Bridge validates the hostname your credentials actually resolve to — from `dw.json`\n> or `SFCC_*` — against the sandbox forms listed below. An unrecognized host is refused\n> with a `403` (`error.code: \"TARGET_NOT_SANDBOX\"`) before any request leaves your\n> machine. The check reads the resolved hostname, never the `instance` tool argument,\n> so omitting `instance` or passing `\"sandbox\"` cannot bypass it.\n>\n> Accepted sandbox hostname forms:\n>\n> - `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.dx.commercecloud.salesforce.com`\n>\n> Anything else — a `production-`/`staging-`/`development-` prefixed host, or any\n> `*.demandware.net` host — is rejected.\n>\n> Still do not configure the grants below on an instance that holds real data.\n> Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to\n> Bridge.\n\nFor the full per-tool list and what each SFCC tool depends on, see\n[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference\nand the `BRIDGE_MCP_PROFILE` gating, see the SFCC section of the\n[package README](../../README.md).\n\n## Prerequisites\n\n- A running SFCC **developer sandbox** and its hostname\n (e.g. `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com`).\n- An **Account Manager API client** — a `client-id` and `client-secret`. This is the\n OCAPI client the tools use to obtain an OAuth token. Create one in Account Manager\n (**API Client** → *Add API Client*) if you don't already have it, and note its\n `client_id`.\n- Business Manager access to the sandbox with permission to edit **Open Commerce API\n Settings**.\n\n## 1. Grant the OCAPI client access in Business Manager\n\nIn Business Manager for the sandbox:\n\n**Administration → Site Development → Open Commerce API Settings → Data API** tab.\n\nAdd the client entry below to the `clients` array of the Data API settings, then\n**Save**. It grants only the resource families and HTTP methods Bridge's SFCC tools\nactually call — not a global `/**` grant. `check_permissions` prints the same JSON on\na 401/403, split into the two blocks.\n\n**READ/SEARCH TOOL GRANTS** — required by the `sfcc` read tools. (`post` is OCAPI's\nconvention for its `*_search` endpoints, not a mutation.)\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\n**MUTATION GRANTS** — required **only if you enable `BRIDGE_MCP_PROFILE=sfcc-write`**,\nwhich registers the nine destructive write tools. These are shipped capabilities, not\nfuture work. No `delete` is granted, because no shipped write tool performs one; the\n`get` entries are needed for the If-Match ETag round trip that precedes each `PATCH`.\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\nNotes:\n\n- The `client_id` **must match** the Account Manager API client whose credentials you\n put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if\n it differs.\n- If the Data API settings are empty, wrap the entries in the standard settings\n envelope. Merge the resource lists from the block(s) above into one `resources`\n array — do not substitute a global `\"resource_id\": \"/**\"` grant:\n\n ```json\n {\n \"_v\": \"23.2\",\n \"clients\": [\n {\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n }\n ]\n }\n ```\n\n- `check_permissions` (below) prints a ready-to-paste grant JSON on a 401/403, so you can\n also let the tool tell you exactly what to add.\n\n## 2. Provide credentials locally\n\nCreate a `dw.json` in your project root (auto-added to git exclude — never commit it):\n\n```json\n{\n \"hostname\": \"zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com\",\n \"client-id\": \"<your-client-id-here>\",\n \"client-secret\": \"<account-manager-client-secret>\"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,\n`client-secret`/`clientSecret`/`client_secret`. Prefer a single config — a multi-entry\n`configs[]` array forces an explicit `instance` on every call. Alternatively, export\n`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n## 3. Set the repo `version` config field\n\nSet the repo's `version` config to your SFCC project type — one of\n`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;\na non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your\nnormal config path, the `config_field` MCP tool (operation `update`, field `version`),\nor the `/teach-bridge` skill.\n\n## 4. Enable the SFCC tools\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always\nregistered. Everything else is gated, behind **two independent profile groups**:\n\n| Group | Registers |\n|---|---|\n| `sfcc` | the 8 OCAPI read tools + `sfcc_log_query` — read-only |\n| `sfcc-write` | the 9 destructive write tools |\n\nNeither implies the other. Add what you need to `BRIDGE_MCP_PROFILE` in the MCP server\n`env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n\"env\": { \"BRIDGE_MCP_PROFILE\": \"sfcc\" }\n```\n\nFor reads plus writes, use `\"sfcc,sfcc-write\"`. `full` expands to every group and is\ntherefore write-capable.\n\n> **Migration.** `sfcc` used to register the nine write tools too. It no longer does.\n> If you were relying on SFCC writes through `BRIDGE_MCP_PROFILE=sfcc`, change it to\n> `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`. `full` users keep write access and need no\n> change.\n\n## 5. Verify\n\nAsk your agent to run:\n\n1. `sfcc_setup_status` — expect all prerequisite checks ✓ (Bridge API key, repo name,\n `version` config, `dw.json` presence/uniqueness, AM/OCAPI token acquisition).\n2. `check_permissions` — probes OCAPI via `GET /system_object_definitions`. A 200 (with\n the OCAPI version) confirms the grant. On 401/403 it prints the exact grant JSON to\n paste back in step 1.\n\nRestart the MCP client after any credential, grant, or env change — a running session\ndoes not pick them up.\n\n## Notes\n\n- **WebDAV logs are separate.** `sfcc_log_query` authenticates with a Business Manager\n username + a 40-character **WebDAV access key** over HTTP Basic auth — *not* the OCAPI\n OAuth token configured here. `sfcc_setup_status` reports OCAPI (step 5) and WebDAV\n (step 6) independently; one can be green while the other is not.\n- **Writes are sandbox-only.** The write tools (attribute/preference create/update) target\n a developer sandbox and echo a paste-ready grant JSON on a 403.\n",
|
|
6
|
-
"docs/bridge-ticket-authoring.md": "# Bridge ticket-authoring posture\n\nBridge has several surfaces that can create a ticket. Without a shared posture\neach one behaves differently, and the most-used surface carries none of the\nmaintainer's preferences at all. This document is the deep reference behind the\nshort posture block that every one of those surfaces carries verbatim.\n\nThe block itself is short on purpose — it competes for attention inside prompts\nthat are already long. Everything that explains *why* lives here.\n\n## The canonical block\n\nThis file **is** the canonical source. The marker-delimited block below is\nduplicated byte-identically onto every carrier — every surface that decides\nticket shape holds these exact bytes, so no surface can quietly drift into its\nown house style.\n\nCarriers (all four hold the block verbatim):\n\n- `agents/src/jira-ticket-writer.md` — the writer itself, plus a compressed\n posture line in its `description:` frontmatter, which is the only coverage a\n bare-chat session gets with no file read and no `tools/list` cost.\n- `commands/src/define-tickets.md` — Stage 9, the most-used authoring surface.\n- `mcp_server/instructions/decompose-epic-candidate.md` — the `idea-to-ticket`\n decomposition step.\n- `mcp_server/instructions/decompose-epic.md` — the `plan-epic` decomposition\n step.\n\nEdit the block here, copy it verbatim to each carrier, and let\n`tests/pytest/mcp_server/test_ticket_authoring_posture_assets.py` prove it\nlanded. That test permits **no** per-surface variation.\n\n<!-- BEGIN BRIDGE TICKET-AUTHORING POSTURE -->\n<!-- Canonical source: docs/bridge-ticket-authoring.md.\n This block is duplicated byte-identically onto every carrier. Never edit a\n copy: edit the canonical source and re-copy it verbatim. A cross-surface\n byte-equality test fails the build if any copy drifts by a single byte. -->\n\n## Ticket-authoring posture\n\nDeep reference: `docs/bridge-ticket-authoring.md`.\n\n**Draft through the writer.** Every ticket body — an epic parent, an epic child,\nand an ordinary sibling alike — is drafted by the `jira-ticket-writer` agent\nbefore `create_ticket` is called. Do not compose a ticket description inline.\n\n**Size the work.** Size each ticket by file-touch breadth and depth plus rough\nlines of code (LOC) changed:\n\n- `S = 1-2 files / <~80 LOC`\n- `M = ~3-8 files / ~80-400 LOC`\n- `L = ~8-15 files / ~400-900 LOC`\n- `XL = >15 files / >~900 LOC`\n\nTarget size priority: **L (target) -> XL (when the work does not fit in L) -> M\n(third choice) -> S (only when unavoidable)**. This applies equally to a\nstandalone ticket and to an epic child.\n\nAim each slice at L. When one will not fit, grow it to XL rather than splitting\nit — split only when the slice is genuinely two independent pieces of work,\nnever merely to land inside a band. Bridge's grooming and implementation process\nhandles a large vertical slice well and is overkill on small ones: every extra\nticket is another worktree, another PR, another rebase, and another chance for\ntwo workers to touch the same file. Reach for M because the work genuinely is\nthat size, not to avoid an XL.\n\nBeyond roughly 40 files or ~3000 LOC, split anyway. Past that point review\nturnaround and rebase cost dominate the run's budget, and a review that wedges\nholds the gate to its full retry ceiling before anyone notices.\n\n**Group at three.** Three or more implementable tickets is an epic: propose an\nepic parent plus an ordered child manifest, and resolve this surface's own\napproval gate before anything is created. One or two tickets are ordinary\nsiblings — no epic parent, no manifest. The threshold is exactly three.\n\n**Hand off once.** An epic handoff names exactly one conductor entry point,\n`drive-epic`, which selects the runnable path itself. Never present a choice\nbetween conductors.\n\n**Departure is closed-list only.** These three exceptions, and no others, permit\ndeparting from the rules above. Invoking one requires no announcement.\n\n- **E1 External-tracker mirroring** — a recorded upstream identifier exists and\n its granularity is contractual. Bypasses sizing and the epic threshold.\n- **E2 Discovery-only spike** — no committed production-code deliverable.\n Bypasses sizing only; does not bypass drafting through the writer.\n- **E3 Authorized incident containment** — tied to an active incident record,\n not to schedule pressure. Bypasses sizing and the epic threshold.\n\nThe list is closed. Anything outside it is an escalation to the operator, not a\njudgement call. Explicitly refused as grounds for departure: a single-file\ntrivial fix (that is `S` reached through the normal path, not an exception),\ngeneric time pressure, \"already well specified\", \"faster without the writer\",\ndeveloper discretion, minor refactor, unattended mode, context limits, and \"hard\nto decompose\" (XL is the normal overflow, so that is the ordinary path and not a\ndeparture). Writer unavailability escalates; it never silently authorizes inline\ndrafting.\n\n<!-- END BRIDGE TICKET-AUTHORING POSTURE -->\n\nThe rest of this document is the rationale the block is deliberately too short to\ncarry.\n\n## Decision: Jira ticket authoring ships through the Jira Ticket Writer (BAPI-900)\n\nBridge ships two very different kinds of ticket-authoring surface, and customers\nneed to know which one they actually have.\n\n**Shipped customer surface.** A customer project gets the `jira-ticket-writer`\nagent — the same writer this posture requires every ticket body to go through —\nplus the agent-directed capability to revise an existing ticket's description.\nAsk your agent to draft a ticket with the Jira Ticket Writer, and ask your agent\nto update an existing ticket's description when it needs revising. Both reach a\ncustomer project because they are packaged: the writer through `AGENTS`\n(`mcp_server/src/agents.generated.ts`) and the description-update path through\nthe registered `update_ticket_description` / `request_ticket_update` MCP tools.\n\n**Repository-local workflows.** `.claude/commands/write-ticket.md` and\n`.claude/commands/update-ticket.md` are bridge-api's own repository-maintenance\ncommands. They are **not** scaffolded into a customer project by `--init`, have\nno `commands/src/` source, and have no generated Cursor or `mcp_server/`\nmirror — deliberately, not by omission. A customer asking their agent to run\nthe write-ticket or update-ticket slash command will not find either one,\nbecause neither ships.\n\n**Why (Option B, not a promotion to shipped status).** The Jira Ticket Writer is\nalready the packaged drafting surface this posture mandates, so shipping\n`write-ticket.md` as a second drafting entry point would duplicate it. More\nimportantly, `write-ticket.md` is today an autonomous, single-ticket, no-halt\npipeline (\"No human confirmation gates — run end-to-end\") with no decomposition\nstep and no approval gate — it cannot honor the \"group at three\" epic rule or\nthe epic approval gate this posture requires, because it was never built to\npropose an epic at all. Promoting it to a shipped surface without that redesign\nwould ship a customer-facing command that silently violates this file's own\nposture. Until that redesign happens, `write-ticket.md` and `update-ticket.md`\nstay repository-local, and every packaged surface directs customers to the Jira\nTicket Writer and to agent-directed description updates instead.\n\n\n## The four rules\n\n### 1. Draft through `jira-ticket-writer`\n\nEvery ticket body is drafted by the `jira-ticket-writer` agent before\n`create_ticket` is called — an epic parent, an epic child, and an ordinary\nsibling alike. Nothing composes a ticket description inline.\n\nThe writer is not a formatter. It runs a codebase-research pass first, so its\ntickets cite the files, functions, and extension points a change actually\ntouches. A description written inline skips that pass, and the difference shows\nup two steps later: plan generation and implementation both ground themselves in\nthe ticket body, so a body with no code references produces a plan with no code\nreferences.\n\n\"The ticket is already well specified\" is not a reason to skip the writer. A\nwell-specified *request* is the writer's input, not a substitute for its output.\n\n### 2. Size toward L\n\nSize each ticket by file-touch breadth and depth plus rough lines of code\nchanged:\n\n| Band | Files | LOC |\n| --- | --- | --- |\n| `S` | 1–2 | `<~80` |\n| `M` | ~3–8 | ~80–400 |\n| `L` | ~8–15 | ~400–900 |\n| `XL` | >15 | `>~900` |\n\nPriority: **L (target) → XL (when the work does not fit in L) → M (third choice)\n→ S (only when unavoidable)**.\n\nThe target is L because the Bridge implementation tooling works best on\nindependently implementable vertical slices. What matters as much as the target\nis the **direction you move when a slice misses it**: upward, not downward.\n\nA slice that will not fit in L becomes **one XL ticket**, not two L ones. Split\nonly when the slice is genuinely two independent pieces of work — never merely to\nland inside a band. Fragmenting a coherent slice to fit is the failure this\nladder exists to prevent: every extra ticket is another worktree, another PR,\nanother rebase, and another chance for two workers to touch the same file, and\nBridge's grooming process is overkill on small tickets. Fewer, larger slices\nspend less of the run's budget on coordination.\n\nThis applies equally to a standalone ticket and to an epic child. There is no\nsize ceiling on a child that a lone ticket does not also have.\n\n`M` is where you land when the work genuinely is three to eight files — not\nsomewhere to retreat to in order to avoid an XL. `S` is likewise not forbidden:\nit is simply what you reach when the work genuinely is one or two files. A\nsingle-file trivial fix is `S` arrived at through the normal path. It is not an\nexception to anything, and it does not license skipping the writer.\n\n**Beyond roughly 40 files or ~3000 LOC, split anyway.** XL is the preferred\noverflow, not an unbounded one. Past that point review turnaround and rebase cost\ndominate the run's budget, and a review that wedges holds the gate to its full\nretry ceiling before anyone notices. That is a real bound, not a preference — and\nit is high enough that reaching it means the work really is two things.\n\n### 3. Group at three\n\nThree or more implementable tickets is an epic. The surface proposes an epic\nparent plus an **ordered child manifest**, and resolves its own approval gate\nbefore anything is created.\n\nOne or two tickets are ordinary siblings: no epic parent, no manifest. The\nthreshold is exactly three — not \"several\", not \"a lot\".\n\nThe manifest carries, per child: the boundary of its scope, its size band,\n`depends_on` (hard prerequisites that must land first), `recommended_after` (soft\nsequencing preferences that are not blockers), and a one-line order rationale.\nHard prerequisites and soft sequencing stay strictly separate, because the\nrecommended implementation order is derived from them and conflating the two\nproduces a serialized order where a parallel one was available.\n\nApproval follows each surface's **existing** attended/unattended rule. Nothing\nhere introduces a new gate policy: `/define-tickets` requires an explicit\naffirmative because creation is irreversible, and the recipe path gates on the\npipeline's own auto-approval variable. What is *not* conditional is the grouping\nitself — an unattended run still produces the epic; only the gate's behavior\nvaries.\n\nDecomposition happens **once**. The pass that decides the split freezes the\nmanifest; body rendering then fans out one writer invocation per entry against\nthat frozen manifest. A rendering invocation may not re-split, merge, reorder,\nrenumber, or rescope. Two independent decisions about the same split disagree,\nand the disagreement surfaces as children that overlap or contradict their\nparent.\n\n### 4. Hand off to exactly one conductor\n\nAn epic handoff names exactly one conductor entry point: `drive-epic`.\n\nBridge currently has two conductors — the v2 server-side engine and the LLM\nconductor pilot — and a standing rule that they must never operate on the same\nepic, because two transition authorities on one epic wedge it permanently. Asking\na model to pick correctly every time is not a control. `drive-epic` makes the\nchoice structural instead: it reads conductor readiness and routes to the one\npath the project can actually run, so no prompt names either underlying conductor\nand no prompt can present both.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes — no prompt, bundled doc, command mirror, or posture\ntest moves.\n\n## Epic coherence check before creation\n\nBefore an epic's tickets are created or its plan approved, its children are\nchecked **as a set** against the epic's own acceptance criteria. The check asks\nwhether every epic criterion has a child that carries out its verification, and\nwhether each cross-child hand-off is owned at both ends. It runs through one\nshared procedure, [`review-epic-coherence.md`](../mcp_server/instructions/review-epic-coherence.md),\nwhich writes `epic-coherence.json` (the record) and `epic-coherence.md` (a short\nsummary) beside the frozen manifest.\n\n**Where it runs.** Three call sites run the check:\n\n- `/define-tickets` Stage 9 — after the epic split is frozen, before the outline\n and the `(y/N)` gate.\n- `idea-to-ticket` (attended, `--auto`, and inside `/idea-to-pr`) — in the Epic\n path of `upload-and-track.md`, after `decomposition-plan.json` and before\n rendering and the external approval gate.\n- `plan-epic` — in `decompose-epic.md`, after the plan artifacts are written and\n before the decomposition is presented for approval.\n\nEvery decomposition pass fills in each child's `owns_verifications`: which epic\ncriterion it proves, and the concrete part of that criterion's verification it\ncarries out. When a criterion is proven by an owner-run step a person runs\noutside the conductor — a release-checklist step, say — the pass declares a\n`non_child_owners` entry naming who runs it, the step, its verbatim text, and\nwhere the result is recorded, instead of forcing a child to own it. Owner-run\nowners count toward coverage only; hand-offs stay child-owned.\n\n**The render preflight.** `render-ticket-manifest.md` refuses to render an epic\nunless the record exists beside the manifest, all three of its digests\n(`manifest_sha256`, `child_evidence_sha256`, and `non_child_owners_sha256`) match\nwhat is being rendered, those digests equal the last reviewer round's, and\nits verdict is `pass`, `accepted_with_gaps`, or `not_checked` with a recorded\nbypass. A missing record, a mismatch, `blocked`, or an un-bypassed `not_checked`\nstops the flow before any writer runs or any ticket is created. `/define-tickets`\nstates the same rule in its own creation step.\n\n**Attended runs: fix or accept.** Each remaining blocking finding is either\nfixed — the manifest is amended, re-frozen, and checked again — or accepted with\na typed reason, which is recorded and turns the verdict into `accepted_with_gaps`.\nWhen the reviewer cannot run, the verdict is `not_checked`. The operator can\nretry, or bypass with a typed reason. A bypass is recorded, and the verdict stays\n`not_checked`; it is never shown as a pass.\n\n**Unattended runs: the risk-scaled round limit, then halt.** An unattended\n`idea-to-ticket` run amends and re-checks within the procedure's risk-scaled\nround limit: at most 2 reviewer runs for a low-risk epic and 3 for a high-risk\none. If a blocking finding remains, it halts with `blocked` before any ticket is\ncreated. A fix made after the last round gets one confirmation pass. An unavailable reviewer, or a check\nthat could not run independently, halts with `not_checked`.\n\nUnattended runs have no bypass; an operator who needs one reruns attended.\n\n**Siblings skip it.** A one- or two-ticket outcome runs no check, writes no\nrecord, and costs nothing extra. The residual risk — a pair that really needed a\nset-level check — is contained by the existing manifest-identity and\nwriter-draft gates. If the work is really epic-shaped, regroup it as an epic\nbefore creation. `plan-epic` is the one exception: the epic already exists, so it\nchecks even a one- or two-sub-task decomposition.\n\n**What lands in the tickets.** Each epic child carries its obligations verbatim\nas acceptance criteria. The epic parent carries a `## Epic coverage map`\nsection: the verdict, one bullet per epic criterion naming its owning child and\nobligation, and any accepted findings or bypasses with their reasons.\n\n## The closed exception list\n\nExactly three exceptions permit departing from the rules above. Invoking one\nrequires **no announcement** — the departure is silent by design, because a\nmandatory announcement would be one more instruction to drift from, and the cost\nof silence was weighed and accepted.\n\n| Id | Exception | Objective trigger | Bypasses |\n| --- | --- | --- | --- |\n| **E1** | External-tracker mirroring | A recorded upstream identifier exists and its granularity is contractual | Sizing and the epic threshold |\n| **E2** | Discovery-only spike | No committed production-code deliverable | Sizing only — **not** drafting through the writer |\n| **E3** | Authorized incident containment | Tied to an active incident record, not to schedule pressure | Sizing and the epic threshold |\n\nEach trigger is objective: an identifier that exists, a deliverable that is\nabsent, an incident record that is open. None of them is a judgement about how\nthe work feels.\n\n### The list is closed\n\nAnything outside the three rows above is an **escalation to the operator**, not a\njudgement call. The following are explicitly refused as grounds for departure:\n\n- a single-file trivial fix — that is `S` reached through the normal path;\n- generic time pressure;\n- \"the request is already well specified\";\n- \"it would be faster without the writer\";\n- developer discretion;\n- \"it's just a minor refactor\";\n- running unattended;\n- context limits;\n- \"this is hard to decompose\" — XL is the normal overflow, so that is the\n ordinary path and not a departure.\n\n**Writer unavailability escalates.** It never silently authorizes inline\ndrafting. A surface that cannot reach `jira-ticket-writer` stops and says so.\n\n## The spike ticket format (BAPI-986)\n\nA spike ticket defines a finding to be produced, not code to be written, so it\ngets its own body shape. This is a second **format**, not a fourth exception:\nthe writer still drafts it, the sizing ladder still applies, and E2 still governs\nwhether the posture's sizing rule is bypassed at all.\n\n### Who chooses, and on what\n\nThe run manifest's `scope` chooses, and it is resolved once, upstream, in\n`mcp_server/instructions/preflight-and-readiness.md`. The writer receives that\nvalue and never decides it. Only `scope: spike` selects the spike format.\n\nThe trigger is E2's exact objective condition — **no committed production-code\ndeliverable**. A deliverable that is absent, not a judgement about how the work\nfeels. Every other case produces the ordinary code-ticket format:\n\n- `scope: task` and `scope: epic_candidate`;\n- a missing scope value;\n- a malformed or unrecognized scope value;\n- a standalone writer invocation with no resolved spike scope;\n- any ambiguity.\n\nCode is the default **in code, not in prose**: it is what every unresolved path\nalready produces, so no one has to remember a rule to get it.\n\n### The five body sections\n\nA spike draft's substantive body is exactly these five, in order:\n\n1. `## Question` — the single question the investigation must answer.\n2. `## Scope` — the systems, code paths, data, and behavior in bounds.\n3. `## Out-of-scope` — what it will not examine, stated explicitly.\n4. `## What the findings must answer (the deliverable)` — names an inspectable\n findings artifact at a specific repository path, and states what that artifact\n must contain: the decisions it settles, the evidence it cites, the\n recommendations it makes, and the unresolved risks it records. This is the\n spike's analogue of Acceptance Criteria — a reviewer holds the finished\n artifact against it to tell whether the spike is done.\n5. `## Which related tickets consume the finding` — names real consumer tickets\n by key, or the literal `none yet`. Never an invented key.\n\nThose five are the spike-specific **body** sections. They are not the whole\ndraft. The following remain mandatory structural metadata, identical to a code\ndraft, and are not additional spike sections:\n\n- the `# [Title]` heading;\n- the execution-class marker line (below);\n- a `## Regression Completeness Notes` block when the gated regression pass\n applies;\n- the trailing `## Materials & Access` section, always last.\n\nThe code-ticket structure — Summary, Requirements, Acceptance Criteria,\nMaterials & Access — is unchanged, and a code draft carries no marker.\n\n### The execution-class marker\n\nA spike draft carries exactly one canonical marker line, alone on its own line,\nimmediately after the title and before `## Question`:\n\n```markdown\n# [Concise Title Describing the Investigation]\n<!-- bridge:execution-class=investigation -->\n\n## Question\n```\n\nIt appears **exactly once**, never in a code-format ticket, and never\nillustratively inside a generated ticket body — not in prose, not in a fence,\nnot in backticks. Duplicates do not reinforce it: a second occurrence makes the\ndescription ambiguous and the parser discards the whole thing, leaving no signal.\n\n**It renders as visible text.** Bridge converts the description from Markdown to\nAtlassian Document Format, and ADF has no comment node, so the marker becomes one\nvisible line in the Jira description rather than a hidden comment. The exact\ntoken survives conversion and stays parseable from the stored description, which\nis what it is for. The placement immediately after the title is therefore an\neditorial choice as much as a parsing one.\n\n**It is a weak hint.** One non-authoritative input alongside the existing weak\nJira issue-type signal. It never overrides the five-way ticket-type\nclassification and never bypasses a human gate. A classifier verdict that\ncontradicts it wins, and the disagreement is reported rather than reconciled.\n\nThe marker's shape lives in exactly one runtime place —\n`parse_execution_class_marker` in\n`src/python/llms/agents/planner_agent/ticket_evaluator/execution_class_marker_utils.py`,\nwhich imports the `code`/`investigation` vocabulary from BAPI-980's constants in\n`api/models/epic_run.py` rather than redeclaring it. The literal in this document\nand in `agents/src/jira-ticket-writer.md` is pinned against that runtime constant\nby test, because Markdown cannot import a Python value.\n\n### The marker survives to the ticket record\n\nThe single-ticket path creates the Jira issue from `slim_description`; the full\ndraft is only an attachment. A marker preserved solely in the attachment would\nleave the ticket record with no signal at all. Two rules close that:\n\n- `mcp_server/instructions/draft-and-critique.md` carries the title and marker\n into `slim_description` in their original order, exactly once, and exempts that\n one literal from the forbidden-raw-HTML hygiene rule that would otherwise strip\n it. A `task` draft has no exemption, and slimming must not synthesize a marker.\n- `mcp_server/instructions/upload-and-track.md` passes `slim_description`\n unchanged as `create_ticket`'s `description`, without stripping, reconstructing,\n relocating, duplicating, or validating the marker.\n\n`create_ticket` gains no execution-class parameter and the MCP tool schema is\nunchanged: the marker rides in `description` and nowhere else.\n\n### Post-creation marker loss is out of scope\n\nA later description rewrite can drop the marker. `ticket_store_lib.update_ticket`\nforwards a full replacement description and the Jira backend re-renders all of it\nthrough `render_adf`, so anything a rewrite omits is simply gone — there is no\nmerge. Preserving the marker across rewrites would mean auditing every\ndescription-rewriting surface, which is excluded here.\n\nThis is a named, owned gap rather than an unstated one: preservation belongs to\nthe downstream slice that enables investigation routing. No local design record\nnames that slice yet, so its ownership is recorded here as an unresolved\ndownstream discovery rather than assigned to an invented slice name.\n\n## Residual risks and their operational contracts\n\nFour limitations ship with the spike format. Each is stated as detection,\ncontainment, and escalation, because a residual-risk note without those three\nreads as coverage while providing none.\n\n### Incorrect E1/E2/E3 invocation\n\n- **Detection**: manual ticket review only. There is no log, counter, or report\n (see the accepted trade-off below).\n- **Containment**: the objective selection rule — any ambiguity produces a code\n ticket — plus the marker's non-authority over classification and every human\n gate.\n- **Escalation**: suspected misuse goes to the authoring-workflow owner.\n\n### Class observability does not cover departure invocation\n\nThe marker makes a spike's intended execution **class** observable. It does not\nmake E1/E2/E3 posture-departure **invocation** observable; that remains silent by\naccepted design, and the accepted trade-off below is unchanged by this slice.\n\n- **Detection**: none for invocation, by design. Ticket review is the only signal.\n- **Containment**: the closed exception list itself — anything outside those\n three rows is an escalation, not a judgement call.\n- **Escalation**: the authoring-workflow owner, as above.\n\n### Premature routing on the marker\n\nMarker-bearing tickets exist before any consumer does. Nothing routes on the\nmarker yet, and the guard for that is **structural, not a feature flag** — no\nsuch flag exists, and naming an imaginary control would be exactly the failure\nthe accepted trade-off below warns about.\n\n- **Detection**: the absence of any marker reader outside the parse/normalize\n helper and the disagreement observer. A static test pins that set, so the first\n premature consumer fails at review time.\n- **Containment**: BAPI-980 seeds every run node `execution_class = 'code'` and\n exposes no caller-controlled `investigation` override anywhere — not on the DAL\n seed signature, not on a Pydantic request model, not on an MCP input. No code\n path writes `investigation`.\n- **Escalation**: any proposal to consume the field in runtime conductor behavior\n goes to the Conductor owner before implementation, per BAPI-980's decision D10;\n any discovered marker-based routing is a release-blocking regression on that\n same path. Enabling investigation routing belongs to the downstream epic\n rollout owner, once the complete path and its human gate exist.\n\n### Marker/classifier disagreement\n\n- **Detection**: the reserved structured-log event\n `ticket_classification_event=execution_class_marker_disagreement`, emitted at\n warning level when an investigation marker meets a code-facing classifier\n verdict. Every `evaluate_ticket_type` entry point compares, so the signal\n covers the ordinary Bridge classification path and not only the estimator\n backfill; a cached verdict is not a classification and is not compared.\n Persistent or high-volume disagreement is surfaced by querying that reserved\n key through the existing logging/Sentry workflow.\n- **Containment**: the comparison is non-authoritative. The classifier verdict is\n persisted and returned exactly as produced, whether or not an event fires.\n- **Escalation**: the event is **diagnostic-only** and the Bridge platform team\n owns it operationally. No alert rule, threshold, or paging behavior ships with\n it; that is net-new work belonging to the slice that enables routing. The field\n and redaction contract lives in\n `docs/claude/jira-optional-ticket-storage.md`, not here.\n\n## Isolation binding for shared-state test support\n\nA ticket that builds or changes test support which resets shared state — a\nharness, a fixture, a reset helper, anything that truncates, drops, or\nreinitializes state another process or another run can also read or write —\nmust state its isolation binding as a requirement: which database, directory,\nor other shared resource the reset targets, and how the ticket's own\ndeliverable proves that target's identity before it destroys anything.\n\nThis doctrine exists because BAPI-1006 shipped a conductor integration-test\nharness with nothing in its ticket stating what the harness's destructive\nreset was bound to, and a v2 worker dispatched to build it truncated the\noperator's operational `bridgeapi` database (Architecture Miss 28). The\ncontainment-hazard classifier this repository runs at planning time\n(`api/library/github/workflow_planning_prediction.py`) can flag such a ticket\nfrom its text, but the flag is advisory — it surfaces the hazard before\ndispatch, it does not prevent a destructive worker on its own. Stating the\nisolation binding in the ticket itself is the durable requirement; treat this\nas an authoring-posture rule, not as runtime enforcement.\n\n## Accepted trade-off: silent departure is unobservable\n\nObservability was deliberately dropped when this posture was ratified. A model\nmay invoke E1, E2, or E3 without recording that it did, so posture drift is only\ndetectable through ticket quality — not through a log, a counter, or a report.\n\nThis is known and accepted. The alternative was another mandatory instruction on\nevery surface, and an instruction that is skipped silently is worse than one that\ndoes not exist: it reads as coverage while providing none.\n\n## Why duplication, not a shared include\n\nCommands, agents, instructions, and docs have four separate build paths in this\nrepository and no shared compiler. Introducing a generated include step to share\none block would mean a fifth build path, a placeholder that can go unresolved,\nand a failure mode where a carrier ships with the placeholder text still in it.\n\nMarker-delimited duplication plus one byte-equality test is the right mechanism\nat this scale. The test reads the canonical sources directly — never the\ngenerated command mirrors, whose byte-identity the command tests already cover —\nand permits **no** per-surface variation. Any drift, down to a single byte, fails.\n\n## A fresh install inherits this\n\nNo configuration step, no server call. The posture reaches a new project through\nthe packaged bundles that `--init` scaffolds:\n\n- `COMMANDS` (`mcp_server/src/commands.generated.ts`) — carries\n `commands/src/define-tickets.md`;\n- `AGENTS` (`mcp_server/src/agents.generated.ts`) — carries\n `agents/src/jira-ticket-writer.md`, including the compressed posture line in\n its `description:` frontmatter;\n- `INSTRUCTIONS` (`mcp_server/src/pipelines.generated.ts`) — carries the\n canonical source and both decomposition instructions;\n- `DOCS` (`mcp_server/src/docs.generated.ts`) — carries this document.\n\nThe compressed frontmatter line matters more than its size suggests: agent\ndescriptions land in every session's system prompt with no file read and no\n`tools/list` cost, so it is the entire bare-chat coverage story.\n\n## Worked examples\n\n**One ticket.** \"Add a `--json` flag to `doctor`.\" Two files and a test, ~90 LOC.\nThat is `M`. One ticket, drafted by the writer, no epic, no manifest, no\nconductor handoff.\n\n**Two tickets.** \"Add rate limiting to the LLM client, and surface the limit in\nthe config UI.\" Backend and frontend are independently implementable and land\nseparately: two ordinary siblings. Still no epic — the threshold is three.\n\n**Four tickets → an epic.** \"Make local ticket mode a first-class system.\"\nDecomposition freezes a parent plus four children, each `L`, with `depends_on`\nnaming the one child that must land first. The full manifest goes to the approval\ngate; on approval, four writer invocations render four bodies against their\nfrozen entries; creation follows `upload-epic-hierarchy.md`; the handoff names\n`drive-epic` and nothing else.\n\n**A child that outgrows `L`.** A proposed child comes out at 19 files. It ships\nas one `XL` child. Do not split it into two `L` children to make it fit — the\nslice is one coherent piece of work, and halving it buys a second worktree, a\nsecond PR, and a rebase between them in exchange for nothing. Split only if the\n19 files really are two independent deliverables.\n\n**Past the ceiling.** A proposed ticket comes out at 60 files and ~5000 LOC.\nThat is over the bound, so it splits — but into the largest coherent pieces\navailable, not into a swarm. Two `XL` tickets is the right answer here; six `M`\nones is not.\n",
|
|
7
|
-
"docs/install/review-epic-coherence-procedure.md": "# Review Epic Coherence\n\nThis is the shared procedure for checking an epic's children **as a set**. It answers two\nquestions no per-ticket review asks: does every epic acceptance criterion have a child that\ncarries out its verification, and is everything one child produces wired to whatever consumes\nit? Drafting surfaces and the on-demand `/review-epic-coherence` command run this same text.\n\nThe check fails **visibly**. A run that cannot finish is `not_checked`. It is never `pass`.\n\n**Enforcement status.** The three drafting surfaces — `define-tickets`, `plan-epic`, and\n`idea-to-ticket` — call this procedure in drafting mode before creation or approval, and\n`render-ticket-manifest.md` refuses to render an epic without a clearing record. A\nreport-mode result from the on-demand command is a diagnostic, not a gate: it clears nothing.\n\n## Inputs\n\nState these four values before starting:\n\n1. **Input document path** — the path to `epic-coherence-input.json`.\n2. **Output directory** — a directory beneath the `docs_dir` that the `ping` tool returned.\n3. **Mode** — `drafting` or `report`.\n4. **Unattended** — true only when `auto_approve_external == \"true\"`. Otherwise the run is\n attended.\n\nIf any of the four is missing, stop and report which one. Do not guess a default.\n\n## The input document\n\nThe document carries artifacts only. It never carries the drafting conversation.\n\n```json\n{\n \"epic\": {\n \"key\": \"EPIC-1\",\n \"title\": \"Short epic title\",\n \"acceptance_criteria\": [\n { \"id\": \"AC-1\", \"text\": \"What must be true\", \"verification\": \"How it is proven\" }\n ]\n },\n \"design_text\": \"Goals and design text: the exploration's Proposed Design, the epic parent body, or equivalent.\",\n \"manifest\": [\n {\n \"id\": \"1\",\n \"title\": \"Child title\",\n \"scope\": \"What the child does\",\n \"size_band\": \"L\",\n \"depends_on\": [],\n \"recommended_after\": [],\n \"touched_files\": [\"path/relative/to/repo.py\"],\n \"read_files\": [\"path/the/child/only/reads.py\"],\n \"owns_verifications\": [\n { \"epic_ac\": \"AC-1\", \"obligation\": \"The concrete part of AC-1's verification this child carries out\" }\n ]\n }\n ],\n \"child_evidence\": [\n {\n \"id\": \"1\",\n \"acceptance_criteria\": [\"Each acceptance criterion of the child, verbatim\"],\n \"body\": \"The child ticket body, if available\"\n }\n ],\n \"non_child_owners\": [\n {\n \"id\": \"OR-1\",\n \"kind\": \"owner_run\",\n \"who\": \"Release owner\",\n \"step_ref\": \"Release checklist step 6\",\n \"step_text\": \"Release checklist step 6: the release owner runs the smoke test against production and posts the output as a comment on the epic.\",\n \"records_result_in\": \"a comment on the epic\",\n \"owns_verifications\": [\n { \"epic_ac\": \"AC-2\", \"obligation\": \"Run the production smoke test and post its output on the epic\" }\n ]\n }\n ]\n}\n```\n\nField rules:\n\n- `epic` carries either `key` or `slug` (one is enough), plus `title` and\n `acceptance_criteria`. Each acceptance criterion has `id`, `text`, and `verification`. When\n an epic AC has no verification sentence, coverage is judged against its text, the basis is\n `text_only`, and the reviewer raises an advisory input finding naming the missing sentence.\n- `design_text` is the goals and design text.\n- `manifest` is the **complete, ordered** child list. Each entry carries `id` (a placeholder\n index or a real ticket key), `title`, `scope`, `size_band`, `depends_on`, and\n `recommended_after`. These are the same field names `decompose-epic.md` and\n `decompose-epic-candidate.md` already use.\n- `touched_files` is optional. It lists the files the child **edits or creates**. When absent,\n the fit check uses the file paths the child's scope and body name. A `touched_files` path that\n does not exist yet can be a planned creation; that alone is not a problem.\n- `read_files` is optional. It lists files the child **only reads**. It lives in the manifest,\n so it is hashed. Reads inform the fit check. Only edits (`touched_files`) count toward edit\n overlap between children and toward the high-risk path check in \"Escalation rule\". A path\n must not appear in both lists. A `read_files` path that does not exist is an `input` problem,\n because a child cannot read a file that is not there. When `read_files` is absent, behavior is\n unchanged.\n- `owns_verifications` is optional. It is a list of entries with `epic_ac` (an epic AC id) and\n `obligation` (the concrete part of that AC's *verification* this child will carry out, in\n words a reviewer can hold against the verification sentence). It is a **claim** the reviewer\n checks. It is never proof by itself. When it is absent (older epics, the on-demand path), the\n reviewer derives candidate owners from each child's own acceptance criteria, and the record\n says `owns_verifications_source: derived`. When it is present, the record says `declared`.\n- `child_evidence` is a **separate top-level array**, keyed by child `id`. Each entry may carry\n `acceptance_criteria` and `body`. It lives **outside** the hashed manifest, so evidence can be\n refreshed without changing manifest identity. Derived ownership counts only when a child's\n acceptance criteria contain a concrete commitment to the obligation. When the material needed\n to establish ownership is absent, that is an explicit `input` gap, not coverage.\n- `non_child_owners` is an optional **top-level array** beside `child_evidence`. It names the\n owners of epic AC verifications that no child carries out, such as a step in an owner-run\n release checklist. Each entry has:\n - `id` — for example `OR-1`. It is unique across the array and never equal to any child `id`.\n - `kind` — a closed vocabulary with one value: `owner_run`.\n - `who` — a named person or role, for example \"Release owner\".\n - `step_ref` — the step it points at, for example \"Release checklist step 6\".\n - `step_text` — the step's text, copied **verbatim** from `design_text`.\n - `records_result_in` — where the result is recorded, for example \"a comment on the epic\".\n - `owns_verifications` — a list of `epic_ac` plus `obligation` entries, the same shape\n children use.\n\n `step_text` is copied into the entry on purpose. `design_text` is not hashed, so a step that\n lived only there could change without changing the review's identity. The copied, hashed\n `step_text` is the reviewed commitment. An owner-run owner owns **coverage only**: it never\n fills a hand-off grid cell. When the array is absent, no AC has a non-child owner.\n\n**Local markdown drafts.** When an epic is drafted as local markdown files outside a drafting\nsurface, build this document with the packaged `epic-coherence-input build` subcommand of the\nBridge MCP package: `epic-coherence-input build --outline <outline-path> --out <output-path>`.\nDo not assemble it by hand. The outline names the epic draft, each child's draft, its\ndependencies, size band, `edits` (which become `touched_files`), `reads` (which become\n`read_files`), and `owns_verifications`, plus any `non_child_owners`. Every epic AC bullet in the\nepic draft must start with an explicit id such as `AC-3:`; the builder never numbers ACs by\nposition or matches them by keyword. `epic-coherence-input check <input-path>` validates an\nexisting document against the structural rules in \"Validation\" and prints the three digests. It\nnever judges coverage; that stays the reviewer's job. The three drafting surfaces keep their\nstructured normalization, and the on-demand `/review-epic-coherence` command keeps building its\ninput from ticket data.\n\n## Identities\n\nThree digests identify what was reviewed. All three use the same canonical JSON encoding.\n\n- `manifest_sha256` is the SHA-256 hex digest of\n `json.dumps(manifest, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")`,\n computed over the `manifest` array only. Compute it locally with this one line:\n\n ```bash\n python3 -c 'import hashlib,json,sys; manifest=json.load(open(sys.argv[1], encoding=\"utf-8\"))[\"manifest\"]; print(hashlib.sha256(json.dumps(manifest, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")).hexdigest())' <input-path>\n ```\n\n- `child_evidence_sha256` is the SHA-256 hex digest of the same encoding applied to the\n `child_evidence` array (an absent array hashes as an empty array).\n- `non_child_owners_sha256` is the SHA-256 hex digest of the same encoding applied to the\n `non_child_owners` array. An absent array hashes as an empty array, as `child_evidence`\n does. Compute it locally with this one line:\n\n ```bash\n python3 -c 'import hashlib,json,sys; owners=json.load(open(sys.argv[1], encoding=\"utf-8\")).get(\"non_child_owners\", []); print(hashlib.sha256(json.dumps(owners, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")).hexdigest())' <input-path>\n ```\n\nRecord **all three** digests, at the top level and in every round. A caller that wants to\nreuse a prior review must compare **all three** identities against the manifest, evidence,\nand non-child owners it is about to use. A matching manifest with changed evidence or changed\nowners is not the same review. The sibling integration's preflight must compare all three.\n\n`design_text` deliberately stays **outside** identity. The copied and hashed `step_text` is\nthe reviewed commitment of an owner-run step. There is no fourth digest, and no render-time\ncheck that `design_text` still contains the step.\n\n## Validation\n\nBefore any reviewer run, validate the input document. Every problem becomes a visible `input`\nfinding. Nothing is silently dropped.\n\n- Child ids are unique. Epic AC ids are unique. Every `owns_verifications.epic_ac` names a real\n epic AC, and every `child_evidence.id` names a real manifest child.\n- The document shapes and the key or slug syntax are valid.\n- Every `non_child_owners` entry is well formed. Its `id` is unique across the array and never\n collides with a child id. Its `kind` is `owner_run`. Its `who`, `step_ref`, `step_text`, and\n `records_result_in` are non-empty. Every one of its `owns_verifications.epic_ac` values names\n a real epic AC. A malformed entry, a colliding id, an invalid AC reference, and a missing\n required field are each a visible `input` finding.\n- Every named file path is repository-relative. Reject absolute paths and any path that\n traverses upward. A path listed in both `touched_files` and `read_files` of the same child is\n an `input` finding.\n- The output directory is beneath the `docs_dir` that `ping` resolved. Refuse to write anywhere\n else.\n- A missing, invalid, or unreadable named file is an `input` finding. So is inaccessible child\n material and a missing set of epic acceptance criteria. A nonexistent `touched_files` path can\n be a planned creation and is not, by itself, a finding. A nonexistent `read_files` path is an\n `input` finding.\n- A missing verification sentence alone is **advisory**. Any problem that prevents a meaningful\n review (no epic acceptance criteria, an empty manifest, an unreadable input document) makes\n the verdict `not_checked`. It never produces a complete-looking coverage map.\n\nDescribe variable paths in words or angle brackets, such as `<output-directory>`. Do not write\nany literal brace-wrapped identifier other than `docs_dir`, including in examples.\n\n## Reviewer run\n\nInvoke the `epic-coherence-reviewer` agent in a **fresh context**. Pass it only the input\ndocument path. Run it **once per round over the whole frozen manifest** — never per child and\nnever over a subset. The reviewer must not receive the drafting conversation.\n\nResolve the agent the way `draft-ticket.md` resolves `jira-ticket-writer`: through the Task tool\nor a host equivalent you have confirmed can resolve it. Cursor has no agent mirror for this\nagent, so on Cursor resolution normally fails.\n\n**Fresh-context fallback.** If no fresh context can be obtained, read the agent file\n`agents/src/epic-coherence-reviewer.md` (or its installed mirror) and execute its methodology\ninline. Set `independent: false` in the record and say so in the report. An inline run is never\na silent independent pass.\n\n- In an **attended** run, offer a fresh-context rerun when one becomes available.\n- In an **unattended drafting** run, an inline `pass` is not the independent gate. Halt.\n\n## Escalation rule\n\nEscalate to a council **only** when at least one of these is true:\n\n1. Any blocking finding has `confidence: uncertain`.\n2. The caller **disputes** a blocking finding instead of amending it. The caller is the drafting\n agent in drafting mode and the operator in report mode.\n3. The epic is **high risk**.\n\nHigh risk means the epic has **six or more children**, or any child's `touched_files` falls\nunder one of these paths:\n\n- `db/migrations/` and `db/alembic/` (the design says `db/migrations/`; this repository's\n migrations live in `db/alembic/`, so match both)\n- `api/library/billing/`\n- `api/routes/setup/auth.py`\n- `api/library/auth/`\n- Epic Implementer enforcement code — every prefix in `_CONTAINMENT_HAZARD_PATHS` in\n `api/library/github/workflow_planning_prediction.py`:\n `mcp_server/src/executor/`, `mcp_server/src/agent-launchers/`,\n `mcp_server/src/claude-settings.ts`, `.github/workflows/`,\n `tests/integration/conductor_harness/`, `api/library/db/`, and `db/alembic/`\n\nOnly edits count. A path a child lists in `read_files` never makes the epic high risk, even\nunder one of these prefixes, so a child that only reads `api/library/db/` does not trigger\nthis rule.\n\nWhen `touched_files` is absent for a child, apply the same path check to the concrete paths its\nscope and body name. When a child's file ownership is unknown, record **uncertainty**. Do not\ndeclare the epic low risk.\n\n**Classify the risk before round 1.** Decide the initial risk tier from the input alone,\nbefore the first reviewer run, using the child count and the edit paths. The tier is `high`\nwhen this rule matches, and also when unknown file ownership leaves the question open. It is\n`low` otherwise. Record it as `risk_tier` with its `risk_reasons`, for example \"Child 1 edits\n`api/library/db/`\". Trigger 3 therefore fires **before round 1**. Triggers 1 and 2 still fire\nafter any later round.\n\nAn uncompleted required escalation leaves the result unresolved. It is never a quiet pass.\n\n## Council call\n\nBefore **every** billable council request, state:\n\n- the cost: **26–65 credits and about 15 minutes** (26 credits for standard models, 65 credits\n for frontier models; state the applicable tier if providers are pinned). The source is\n `api/library/billing/pricing.py`;\n- the escalation reason (which of the three triggers fired);\n- that council output cannot itself clear a blocker.\n\nAn **attended** run asks the operator to approve before proceeding. An **unattended** run states\nthe cost in its output and proceeds. This holds for every request, including a `mode: general`\nretry.\n\n**When the council starts.**\n\n- **High-risk trigger.** For an epic classified high risk before round 1, state the cost and,\n when attended, get the operator's approval **before** round 1 starts. Then request the\n council and start the round-1 reviewer run **in parallel**. Do not wait for a later round.\n- **Uncertain or disputed trigger.** Request the council after the round that raised the\n finding.\n- **How many.** One council per drafting run for the initial high-risk trigger. A later\n qualifying trigger (an uncertain or disputed blocking finding) may start **at most one more**\n council, with its own cost statement and approval.\n\nCall `request_council` with:\n\n- `mode`: `technical`\n- `wait_for_result`: `false`\n- no `repo_name`\n- task text containing **only** the input document's artifacts and the contested findings.\n The early high-risk council has no reviewer findings yet, so its task text holds **only** the\n input document's artifacts. Never invent findings for it. Ask it to look for cross-child and\n release-level risks, such as concurrent writers, migration and rollback order, and rollout\n that runs ahead of the code.\n\nThen poll `get_council` for a bounded period. If technical mode cannot run because the\nrepository is not indexed, repeat the request with `mode: general`, and state the cost again\nbefore that request too.\n\n**The amendment freeze.** The round-1 amendment freezes at the moment the caller begins writing\nthe amended input document after collecting the round-1 findings. Poll `get_council` while the\nround-1 reviewer runs. Council conclusions received before the freeze join that **same**\namendment pass. Conclusions received after it join the next pass. A frozen round is never\nchanged after the fact.\n\nCouncil conclusions join the finding list as **separate** findings with `source: council`. The\noriginal reviewer finding is kept unchanged. It is never edited, downgraded, or removed because\na council failed, timed out, is still running, left the dispute unresolved, or concluded.\nRecord the `council_id` and the council outcome. Record still-running and failed outcomes as\nunresolved.\n\n**An unfinished required council.** A required council that is `still_running`, `failed`, or\n`timed_out` when the last round or confirmation pass ends is unresolved. The verdict is never\n`pass`. An **attended** operator, after being shown the council's id, cost, and elapsed time,\nmay accept it with a typed override of kind `accept` that names its `council_id`. An\n**unattended** run records `blocked` and halts before any ticket is created.\n\n## Drafting mode\n\nDrafting mode is for a drafting surface that can still change the manifest.\n\n- **Amend and recheck.** The reviewer only describes amendments. The **caller** amends the\n manifest (adds or rescopes children), re-freezes it, and writes a **new** input document. The\n procedure then reruns the reviewer in a fresh context. Record each round with its own\n `manifest_sha256`, `child_evidence_sha256`, and `non_child_owners_sha256`, blocking and\n advisory counts, whether a council ran, and whether it was a confirmation pass.\n- **Risk-scaled round limit.** The number of ordinary reviewer runs, `round_limit`, depends on\n the risk tier from \"Escalation rule\":\n - **Low risk** (the epic is not high risk and no blocking finding is `uncertain`):\n `round_limit` is **2**. Run one round, and one recheck only when blocking findings remain.\n That is at most **2 reviewer runs**.\n - **High risk**: `round_limit` is **3**. Allow at most **2 re-checks**, which is\n **3 reviewer runs in total**.\n - **Promotion.** A low-risk epic whose round produces a blocking finding with\n `confidence: uncertain` becomes high risk from that point. It gets the high-risk limit, and\n trigger 1 fires.\n\n Record the final `risk_tier`, `risk_reasons`, and `round_limit`. A confirmation pass is not an\n ordinary run and does not count toward `round_limit`.\n- **Confirmation pass.** When the caller changes the input after the last allowed ordinary\n round, run **one confirmation pass**. It is a fresh-context reviewer run over the **whole**\n amended manifest, never a subset. Its requested focus is narrower: the amended children, the\n hand-offs they touch, and whether the findings the amendment targeted are closed. It must\n still report **any** blocking finding it observes anywhere in the manifest; the narrower\n focus never suppresses one. When it cannot establish where an amendment's impact ends, it\n raises that as a blocking finding with `confidence: uncertain` instead of calling the\n amendment confirmed. Record it in `rounds[]` with `\"confirmation\": true`.\n - A confirmation pass **never** starts another automatic amendment loop. A new blocking\n finding goes to attended fix-or-accept, or to the unattended halt.\n - In an **attended** run, the operator may direct a fix of a confirmation blocker. Then run\n another whole-manifest confirmation pass, so the last round bears the final digests. A typed\n acceptance stays available instead.\n- **Final identity.** The top-level `manifest_sha256`, `child_evidence_sha256`, and\n `non_child_owners_sha256` equal the digests of the **last** entry in `rounds[]`. A record\n whose top-level digests no round saw does not clear. The sole exception is a confirmation\n pass that cannot run: an **attended** operator may record `amended_after_final_round: true`\n with an override of kind `bypass` carrying a typed reason. The verdict is then\n `not_checked`, never `pass` or `accepted_with_gaps`, and the top-level digests still describe\n the amended input. An **unattended** run halts instead.\n- **Attended resolution.** After the bound, each remaining blocking finding must be fixed or\n accepted with a typed reason. An accepted finding is an override with `kind: accept`, plus\n `finding_id`, `reason`, `accepted_by`, and `round`. Declining keeps the verdict `blocked`.\n When **every** remaining blocking finding is accepted, the verdict is `accepted_with_gaps`.\n- **Unattended halt.** If any blocking finding remains after the bound, the verdict is\n `blocked`. Return an explicit halt signal: **do not call `create_ticket`**. The caller must\n stop.\n\n## When the review cannot run\n\nIf the reviewer cannot run (a tool error, a timeout, or a host that lacks the capability while\ninline execution also fails), or the material prevents a meaningful review, the verdict is\n`not_checked`. It is never `pass`.\n\n- **Attended:** offer a retry, or a bypass with a typed reason. A bypass is recorded as an\n override with `kind: bypass`. The verdict stays `not_checked`.\n- **Unattended:** halt.\n\n## Report mode\n\nReport mode is diagnostic. It runs the reviewer **once**. There is no amendment loop and no halt\nsignal. Escalation still applies, with the cost stated and confirmation asked. The verdict is\n`pass`, `blocked`, or `not_checked`.\n\nA `blocked` or `not_checked` report is **diagnostic and not a clearance**.\n\n## The record: epic-coherence.json\n\nWrite `epic-coherence.json` into the output directory:\n\n```json\n{\n \"schema_version\": 2,\n \"checked_at\": \"2026-01-01T00:00:00Z\",\n \"mode\": \"drafting\",\n \"unattended\": false,\n \"manifest_sha256\": \"<64 hex characters, or null>\",\n \"child_evidence_sha256\": \"<64 hex characters, or null>\",\n \"non_child_owners_sha256\": \"<64 hex characters, or null>\",\n \"owns_verifications_source\": \"declared\",\n \"verdict\": \"pass\",\n \"independent\": true,\n \"risk_tier\": \"low\",\n \"risk_reasons\": [\"Three children; no child edits a high-risk path\"],\n \"round_limit\": 2,\n \"amended_after_final_round\": false,\n \"rounds\": [\n {\n \"manifest_sha256\": \"<64 hex characters>\",\n \"child_evidence_sha256\": \"<64 hex characters>\",\n \"non_child_owners_sha256\": \"<64 hex characters>\",\n \"blocking_count\": 0,\n \"advisory_count\": 0,\n \"council_ran\": false,\n \"confirmation\": false\n }\n ],\n \"owner_run_acs\": [\"AC-2\"],\n \"coverage_map\": [\n {\n \"epic_ac\": \"AC-1\",\n \"judgment_basis\": \"verification\",\n \"status\": \"covered\",\n \"owners\": [\n {\n \"owner_kind\": \"child\",\n \"child\": \"1\",\n \"obligation\": \"Concrete obligation\",\n \"child_ac\": \"The child AC id or a quoted child AC\",\n \"source\": \"declared\"\n }\n ]\n },\n {\n \"epic_ac\": \"AC-2\",\n \"judgment_basis\": \"verification\",\n \"status\": \"covered\",\n \"owners\": [\n {\n \"owner_kind\": \"owner_run\",\n \"child\": null,\n \"owner\": \"OR-1\",\n \"who\": \"Release owner\",\n \"step_ref\": \"Release checklist step 6\",\n \"records_result_in\": \"a comment on the epic\",\n \"obligation\": \"Run the production smoke test and post its output on the epic\",\n \"source\": \"declared\"\n }\n ]\n }\n ],\n \"handoff_grid\": [\n {\n \"thing\": \"The stored field, endpoint, flag, event, job payload, or scope\",\n \"consumer\": \"The consuming piece\",\n \"write_path\": \"child id or null\",\n \"read_path\": \"child id or null\",\n \"caller\": \"child id or null\",\n \"start\": \"child id or null\",\n \"steady_state\": \"child id or null\",\n \"wind_down\": \"child id or null\"\n }\n ],\n \"findings\": [\n {\n \"id\": \"F-1\",\n \"kind\": \"coverage\",\n \"severity\": \"blocking\",\n \"confidence\": \"certain\",\n \"concerns\": \"AC-1\",\n \"evidence\": \"What was observed, with file and line where code was read\",\n \"smallest_amendment\": \"The smallest change that would close it\",\n \"source\": \"reviewer\"\n }\n ],\n \"overrides\": [\n { \"kind\": \"accept\", \"finding_id\": \"F-1\", \"reason\": \"Typed reason\", \"accepted_by\": \"Operator\", \"round\": 3 }\n ],\n \"council_id\": null,\n \"council_outcome\": \"not_run\",\n \"councils\": []\n}\n```\n\nRules:\n\n- `schema_version` is `2`. A reader also accepts a version `1` record: it has no\n `non_child_owners_sha256`, no owner-run owners, and no confirmation rounds, so it describes\n an input with no `non_child_owners`.\n- `verdict` is one of `pass`, `accepted_with_gaps`, `blocked`, `not_checked`.\n- `pass` requires **zero unresolved blocking findings**. A `pass` with `independent: false` is\n allowed but flagged prominently in both files.\n- An `accept` override accepts a known gap. A `bypass` override skips a review that could not\n run. Keep the two distinct.\n- `kind` on a finding is one of `coverage`, `handoff`, `fit`, `e2e_proof`, `input`.\n `severity` is `blocking` or `advisory`. `confidence` is `certain` or `uncertain`. `source`\n is `reviewer` or `council`.\n- Digests may be `null` **only** when unreadable input makes computing them impossible. Then\n add an explicit `input` finding and set the verdict to `not_checked`.\n- The top-level digests equal the last round's digests. The only record allowed to differ is\n an attended one with `amended_after_final_round: true`, a `bypass` override with a typed\n reason, and verdict `not_checked` (see \"Drafting mode\").\n- `rounds[].confirmation` is `true` only for a confirmation pass. `risk_tier` is `low` or\n `high`, `risk_reasons` lists why, and `round_limit` is the ordinary-run limit that applied\n (`2` or `3`; report mode runs once).\n- Each coverage owner carries `owner_kind`, which is `child` or `owner_run`. A child owner\n carries `child`, `obligation`, `child_ac`, and `source`. An owner-run owner carries\n `child: null`, `owner` (its `non_child_owners` id), `who`, `step_ref`, `records_result_in`,\n `obligation`, and `source: declared`.\n- `owner_run_acs` lists the id of every epic AC with at least one owner-run owner. It is empty\n when there are none. Owner-run coverage is always visible, never silently passed.\n- `council_id` is `null` when no council ran. `council_outcome` is one of `not_run`,\n `concluded`, `failed`, `timed_out`, or `still_running`. These two fields describe the first\n council. When a second council runs, `councils` lists **every** council in order, each with\n `council_id`, `trigger` (`uncertain`, `disputed`, or `high_risk`), and `outcome`. An override\n that accepts an unfinished council carries `council_id` in place of `finding_id`.\n- Unknown fields, such as a historical `council_note`, are outside this schema. Readers ignore\n them. They never authorize clearance.\n- A `not_checked` record must **never** carry a coverage map that looks complete. Leave the map\n empty, or mark every entry as not assessed.\n- Keep this schema in this file. Do not copy it into the ticket-authoring posture block.\n\n## The record: epic-coherence.md\n\nWrite `epic-coherence.md` into the same directory as a short human summary. Use **no tables**.\nIn this order:\n\n1. The **verdict**, as the very first line of the file (after the title heading). Nothing else comes\n before it.\n2. **Independent: yes** or **Independent: no**, prominently, on the line directly after the\n verdict, then the **next action**.\n3. The round count, the risk tier, and the round limit. Say which round, if any, was a\n confirmation pass.\n4. Directly after the round count, one line: \"Owner-run coverage: N of M epic ACs depend on\n steps a person runs outside the conductor.\"\n5. One **bullet per epic AC**: its id, the judgment basis (`verification` or `text only`), the\n status, the owning child or the absence of one, the obligation, and the cited child AC\n evidence. Mark derived ownership as derived. For an owner-run owner, say \"owner-run\" in\n words, with the person or role, the step reference, and where the result is recorded, for\n example: AC-4: covered, owner-run (Release owner, checklist step 5; result recorded in an\n epic comment).\n6. **Blocking findings**, each labelled `blocking` with its `certain` or `uncertain` label.\n Write each hand-off gap as a sentence naming the shared thing, the affected consumer, the\n missing lifecycle end, and the smallest amendment.\n7. **Advisory and input findings.**\n8. **Council state.**\n9. **Overrides**, each with its typed reason.\n\nState every meaning in words, never by color alone.\n\nIn both files:\n\n- Label a report-mode `blocked` or `not_checked` result as **diagnostic and not a clearance**.\n- In report mode, state that the result gates no drafting surface: each drafting surface runs\n its own drafting-mode check before creation.\n- Put `independent: false` at the top, directly after the verdict line, and offer an attended\n fresh-context rerun.\n\n## Writing the records safely\n\n- Render both files to temporary files first. Replace the real files from the temporary files\n where the host supports it. Both files must carry the same run identity (`checked_at` and\n all three final digests), and both describe the same final round.\n- Report a write failure explicitly. A write failure is an **incomplete review**.\n- When a newer attempt fails or is incomplete, **invalidate or remove** any older successful\n artifact in the output directory. Never leave an older `pass` beside a newer failed outcome.\n An incomplete newer attempt must not leave an older clearing record in place.\n\n## Return\n\nReport to the caller:\n\n- the verdict and whether the run was independent;\n- the next action;\n- the paths of `epic-coherence.json` and `epic-coherence.md`;\n- for drafting mode, the halt signal (`do not call create_ticket`) when one applies, and the\n amendments the caller must make before the next round.\n\nDo not describe a `blocked` or `not_checked` report-mode result as a clearance.\n"
|
|
5
|
+
"docs/install/sfcc-integration.md": "# Installing the SFCC Integration (OCAPI)\n\nBridge's Salesforce B2C Commerce (SFCC) tools give an AI coding agent read access to\na sandbox's object model, custom object definitions, and site preferences — plus a\nsmall set of sandbox-only writes — through the **OCAPI Data API**. This guide covers\nsetting up the OCAPI client that those tools authenticate against.\n\n> **Sandbox / local development only.** This integration is intended for a **developer\n> sandbox**, and that restriction is **enforced in code**: before any SFCC tool runs,\n> Bridge validates the hostname your credentials actually resolve to — from `dw.json`\n> or `SFCC_*` — against the sandbox forms listed below. An unrecognized host is refused\n> with a `403` (`error.code: \"TARGET_NOT_SANDBOX\"`) before any request leaves your\n> machine. The check reads the resolved hostname, never the `instance` tool argument,\n> so omitting `instance` or passing `\"sandbox\"` cannot bypass it.\n>\n> Accepted sandbox hostname forms:\n>\n> - `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.dx.commercecloud.salesforce.com`\n>\n> Anything else — a `production-`/`staging-`/`development-` prefixed host, or any\n> `*.demandware.net` host — is rejected.\n>\n> Still do not configure the grants below on an instance that holds real data.\n> Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to\n> Bridge.\n\nFor the full per-tool list and what each SFCC tool depends on, see\n[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference\nand the `BRIDGE_MCP_PROFILE` gating, see the SFCC section of the\n[package README](../../README.md).\n\n## Prerequisites\n\n- A running SFCC **developer sandbox** and its hostname\n (e.g. `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com`).\n- An **Account Manager API client** — a `client-id` and `client-secret`. This is the\n OCAPI client the tools use to obtain an OAuth token. Create one in Account Manager\n (**API Client** → *Add API Client*) if you don't already have it, and note its\n `client_id`.\n- Business Manager access to the sandbox with permission to edit **Open Commerce API\n Settings**.\n\n## 1. Grant the OCAPI client access in Business Manager\n\nIn Business Manager for the sandbox:\n\n**Administration → Site Development → Open Commerce API Settings → Data API** tab.\n\nAdd the client entry below to the `clients` array of the Data API settings, then\n**Save**. It grants only the resource families and HTTP methods Bridge's SFCC tools\nactually call — not a global `/**` grant. `check_permissions` prints the same JSON on\na 401/403, split into the two blocks.\n\n**READ/SEARCH TOOL GRANTS** — required by the `sfcc` read tools. (`post` is OCAPI's\nconvention for its `*_search` endpoints, not a mutation.)\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\n**MUTATION GRANTS** — required **only if you enable `BRIDGE_MCP_PROFILE=sfcc-write`**,\nwhich registers the nine destructive write tools. These are shipped capabilities, not\nfuture work. No `delete` is granted, because no shipped write tool performs one; the\n`get` entries are needed for the If-Match ETag round trip that precedes each `PATCH`.\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/sites/*/site_preferences/**\", \"methods\": [\"get\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\nNotes:\n\n- Value writes (`site_preference_values_set`) are site-scoped: they need the mutation\n resource `/sites/*/site_preferences/**`. The read/search grant keeps the org-level\n `/site_preferences/**`. **If you pasted an earlier mutation grant, it lacks the\n site-scoped resource.** Replace `<YOUR_CLIENT_ID>` with your client id in the newly\n printed grant (`check_permissions` prints it) and paste it again before running\n value writes.\n- `check_permissions` probes a GET only. It shows read access, not write access.\n- The `client_id` **must match** the Account Manager API client whose credentials you\n put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if\n it differs.\n- If the Data API settings are empty, wrap the entries in the standard settings\n envelope. Merge the resource lists from the block(s) above into one `resources`\n array — do not substitute a global `\"resource_id\": \"/**\"` grant:\n\n ```json\n {\n \"_v\": \"23.2\",\n \"clients\": [\n {\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n }\n ]\n }\n ```\n\n- `check_permissions` (below) prints a ready-to-paste grant JSON on a 401/403, so you can\n also let the tool tell you exactly what to add.\n\n## 2. Provide credentials locally\n\nCreate a `dw.json` in your project root (auto-added to git exclude — never commit it):\n\n```json\n{\n \"hostname\": \"zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com\",\n \"client-id\": \"<your-client-id-here>\",\n \"client-secret\": \"<account-manager-client-secret>\"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,\n`client-secret`/`clientSecret`/`client_secret`. A single config is simplest. With a\nmulti-entry `configs[]` array, pass one of the listed sandbox hostnames as `instance`\nand that config's credentials are used (an unapproved host is refused before any\ncredential lookup); without an `instance` the call fails as `multi-sandbox`. Alternatively, export\n`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n## 3. Set the repo `version` config field\n\nSet the repo's `version` config to your SFCC project type — one of\n`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;\na non-SFCC value blocks the OCAPI tools (the eight reads, the nine writes, and\n`check_permissions`). It does not block `sfcc_setup_status` or `sfcc_log_query` — log\nquery has its own backend gate. Set it via your\nnormal config path, the `config_field` MCP tool (operation `update`, field `version`),\nor the `/teach-bridge` skill.\n\n## 4. Enable the SFCC tools\n\nNo SFCC tool is registered by default — a session on the default `core` profile has\nnone. Every SFCC tool sits behind **two independent profile groups**:\n\n| Group | Registers |\n|---|---|\n| `sfcc` | the two diagnostics (`sfcc_setup_status`, `check_permissions`), the 8 OCAPI read tools, and `sfcc_log_query` — read-only |\n| `sfcc-write` | the 9 destructive write tools |\n\nNeither implies the other. An `sfcc-write`-only session has no diagnostics, so it\ncannot run `check_permissions` to print the grant JSON; use `sfcc,sfcc-write` when\nyou need writes. Add what you need to `BRIDGE_MCP_PROFILE` in the MCP server\n`env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n\"env\": { \"BRIDGE_MCP_PROFILE\": \"sfcc\" }\n```\n\nFor reads plus writes, use `\"sfcc,sfcc-write\"`. `full` expands to every group and is\ntherefore write-capable.\n\n> **Migration.** `sfcc` used to register the nine write tools too. It no longer does.\n> If you were relying on SFCC writes through `BRIDGE_MCP_PROFILE=sfcc`, change it to\n> `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`. `full` users keep write access and need no\n> change.\n\n## 5. Verify\n\nAsk your agent to run:\n\n1. `sfcc_setup_status` — expect all prerequisite checks ✓ (Bridge API key, repo name,\n `version` config, `dw.json` presence/uniqueness, AM/OCAPI token acquisition).\n2. `check_permissions` — probes OCAPI via `GET /system_object_definitions`. A 200 (with\n the OCAPI version) confirms the grant. On 401/403 it prints the exact grant JSON to\n paste back in step 1.\n\nRestart the MCP client after any credential, grant, or env change — a running session\ndoes not pick them up.\n\n## Notes\n\n- **WebDAV logs are separate.** `sfcc_log_query` authenticates with a Business Manager\n username + a 40-character **WebDAV access key** over HTTP Basic auth — *not* the OCAPI\n OAuth token configured here. `sfcc_setup_status` reports OCAPI (step 5) and WebDAV\n (step 6) independently; one can be green while the other is not.\n- **Writes are sandbox-only.** The write tools (attribute/preference create/update) target\n a developer sandbox and echo a paste-ready grant JSON on a 403.\n",
|
|
6
|
+
"docs/bridge-ticket-authoring.md": "# Bridge ticket-authoring posture\n\nBridge has several surfaces that can create a ticket, and `/define-tickets` is the\nsingle entry point for creating them. Without a shared posture\neach one behaves differently, and the most-used surface carries none of the\nmaintainer's preferences at all. This document is the deep reference behind the\nshort posture block that every one of those surfaces carries verbatim.\n\nThe block itself is short on purpose — it competes for attention inside prompts\nthat are already long. Everything that explains *why* lives here.\n\n## The canonical block\n\nThis file **is** the canonical source. The marker-delimited block below is\nduplicated byte-identically onto every carrier — every surface that decides\nticket shape holds these exact bytes, so no surface can quietly drift into its\nown house style.\n\nCarriers (all four hold the block verbatim):\n\n- `agents/src/jira-ticket-writer.md` — the writer itself, plus a compressed\n posture line in its `description:` frontmatter, which is the only coverage a\n bare-chat session gets with no file read and no `tools/list` cost.\n- `commands/src/define-tickets.md` — Stage 9, the most-used authoring surface.\n- `mcp_server/instructions/decompose-epic-candidate.md` — the `idea-to-ticket`\n decomposition step.\n- `mcp_server/instructions/decompose-epic.md` — the `plan-epic` decomposition\n step.\n\nEdit the block here, copy it verbatim to each carrier, and let\n`tests/pytest/mcp_server/test_ticket_authoring_posture_assets.py` prove it\nlanded. That test permits **no** per-surface variation.\n\n<!-- BEGIN BRIDGE TICKET-AUTHORING POSTURE -->\n<!-- Canonical source: docs/bridge-ticket-authoring.md.\n This block is duplicated byte-identically onto every carrier. Never edit a\n copy: edit the canonical source and re-copy it verbatim. A cross-surface\n byte-equality test fails the build if any copy drifts by a single byte. -->\n\n## Ticket-authoring posture\n\nDeep reference: `docs/bridge-ticket-authoring.md`.\n\n**Draft through the writer.** Every ticket body — an epic parent, an epic child,\nand an ordinary sibling alike — is drafted by the `jira-ticket-writer` agent\nbefore `create_ticket` is called. Do not compose a ticket description inline.\n\n**Size the work.** Size each ticket by file-touch breadth and depth plus rough\nlines of code (LOC) changed:\n\n- `S = 1-2 files / <~80 LOC`\n- `M = ~3-8 files / ~80-400 LOC`\n- `L = ~8-15 files / ~400-900 LOC`\n- `XL = >15 files / >~900 LOC`\n\nTarget size priority: **L (target) -> XL (when the work does not fit in L) -> M\n(third choice) -> S (only when unavoidable)**. This applies equally to a\nstandalone ticket and to an epic child.\n\nAim each slice at L. When one will not fit, grow it to XL rather than splitting\nit — split only when the slice is genuinely two independent pieces of work,\nnever merely to land inside a band. Bridge's grooming and implementation process\nhandles a large vertical slice well and is overkill on small ones: every extra\nticket is another worktree, another PR, another rebase, and another chance for\ntwo workers to touch the same file. Reach for M because the work genuinely is\nthat size, not to avoid an XL.\n\nBeyond roughly 40 files or ~3000 LOC, split anyway. Past that point review\nturnaround and rebase cost dominate the run's budget, and a review that wedges\nholds the gate to its full retry ceiling before anyone notices.\n\n**Group at three.** Three or more implementable tickets is an epic: propose an\nepic parent plus an ordered child manifest, and resolve this surface's own\napproval gate before anything is created. One or two tickets are ordinary\nsiblings — no epic parent, no manifest. The threshold is exactly three.\n\n**Hand off once.** An epic handoff names exactly one conductor entry point,\n`drive-epic`, which selects the runnable path itself. Never present a choice\nbetween conductors.\n\n**Departure is closed-list only.** These three exceptions, and no others, permit\ndeparting from the rules above. Invoking one requires no announcement.\n\n- **E1 External-tracker mirroring** — a recorded upstream identifier exists and\n its granularity is contractual. Bypasses sizing and the epic threshold.\n- **E2 Discovery-only spike** — no committed production-code deliverable.\n Bypasses sizing only; does not bypass drafting through the writer.\n- **E3 Authorized incident containment** — tied to an active incident record,\n not to schedule pressure. Bypasses sizing and the epic threshold.\n\nThe list is closed. Anything outside it is an escalation to the operator, not a\njudgement call. Explicitly refused as grounds for departure: a single-file\ntrivial fix (that is `S` reached through the normal path, not an exception),\ngeneric time pressure, \"already well specified\", \"faster without the writer\",\ndeveloper discretion, minor refactor, unattended mode, context limits, and \"hard\nto decompose\" (XL is the normal overflow, so that is the ordinary path and not a\ndeparture). Writer unavailability escalates; it never silently authorizes inline\ndrafting.\n\n<!-- END BRIDGE TICKET-AUTHORING POSTURE -->\n\nThe rest of this document is the rationale the block is deliberately too short to\ncarry.\n\n## Decision: Jira ticket authoring ships through the Jira Ticket Writer (BAPI-900)\n\nBridge ships two very different kinds of ticket-authoring surface, and customers\nneed to know which one they actually have.\n\n**Shipped customer surface.** A customer project gets the `jira-ticket-writer`\nagent — the same writer this posture requires every ticket body to go through —\nplus the agent-directed capability to revise an existing ticket's description.\nAsk your agent to draft a ticket with the Jira Ticket Writer, and ask your agent\nto update an existing ticket's description when it needs revising. Both reach a\ncustomer project because they are packaged: the writer through `AGENTS`\n(`mcp_server/src/agents.generated.ts`) and the description-update path through\nthe registered `update_ticket_description` / `request_ticket_update` MCP tools.\n\n**Repository-local workflows.** `.claude/commands/write-ticket.md` and\n`.claude/commands/update-ticket.md` are bridge-api's own repository-maintenance\ncommands. They are **not** scaffolded into a customer project by `--init`, have\nno `commands/src/` source, and have no generated Cursor or `mcp_server/`\nmirror — deliberately, not by omission. A customer asking their agent to run\nthe write-ticket or update-ticket slash command will not find either one,\nbecause neither ships.\n\n**Why (Option B, not a promotion to shipped status).** The Jira Ticket Writer is\nalready the packaged drafting surface this posture mandates, so shipping\n`write-ticket.md` as a second drafting entry point would duplicate it. More\nimportantly, `write-ticket.md` is today an autonomous, single-ticket, no-halt\npipeline (\"No human confirmation gates — run end-to-end\") with no decomposition\nstep and no approval gate — it cannot honor the \"group at three\" epic rule or\nthe epic approval gate this posture requires, because it was never built to\npropose an epic at all. Promoting it to a shipped surface without that redesign\nwould ship a customer-facing command that silently violates this file's own\nposture. Until that redesign happens, `write-ticket.md` and `update-ticket.md`\nstay repository-local, and every packaged surface directs customers to the Jira\nTicket Writer and to agent-directed description updates instead.\n\n\n## The four rules\n\n### 1. Draft through `jira-ticket-writer`\n\nEvery ticket body is drafted by the `jira-ticket-writer` agent before\n`create_ticket` is called — an epic parent, an epic child, and an ordinary\nsibling alike. Nothing composes a ticket description inline.\n\nThe writer is not a formatter. It runs a codebase-research pass first, so its\ntickets cite the files, functions, and extension points a change actually\ntouches. A description written inline skips that pass, and the difference shows\nup two steps later: plan generation and implementation both ground themselves in\nthe ticket body, so a body with no code references produces a plan with no code\nreferences.\n\n\"The ticket is already well specified\" is not a reason to skip the writer. A\nwell-specified *request* is the writer's input, not a substitute for its output.\n\n### 2. Size toward L\n\nSize each ticket by file-touch breadth and depth plus rough lines of code\nchanged:\n\n| Band | Files | LOC |\n| --- | --- | --- |\n| `S` | 1–2 | `<~80` |\n| `M` | ~3–8 | ~80–400 |\n| `L` | ~8–15 | ~400–900 |\n| `XL` | >15 | `>~900` |\n\nPriority: **L (target) → XL (when the work does not fit in L) → M (third choice)\n→ S (only when unavoidable)**.\n\nThe target is L because the Bridge implementation tooling works best on\nindependently implementable vertical slices. What matters as much as the target\nis the **direction you move when a slice misses it**: upward, not downward.\n\nA slice that will not fit in L becomes **one XL ticket**, not two L ones. Split\nonly when the slice is genuinely two independent pieces of work — never merely to\nland inside a band. Fragmenting a coherent slice to fit is the failure this\nladder exists to prevent: every extra ticket is another worktree, another PR,\nanother rebase, and another chance for two workers to touch the same file, and\nBridge's grooming process is overkill on small tickets. Fewer, larger slices\nspend less of the run's budget on coordination.\n\nThis applies equally to a standalone ticket and to an epic child. There is no\nsize ceiling on a child that a lone ticket does not also have.\n\n`M` is where you land when the work genuinely is three to eight files — not\nsomewhere to retreat to in order to avoid an XL. `S` is likewise not forbidden:\nit is simply what you reach when the work genuinely is one or two files. A\nsingle-file trivial fix is `S` arrived at through the normal path. It is not an\nexception to anything, and it does not license skipping the writer.\n\n**Beyond roughly 40 files or ~3000 LOC, split anyway.** XL is the preferred\noverflow, not an unbounded one. Past that point review turnaround and rebase cost\ndominate the run's budget, and a review that wedges holds the gate to its full\nretry ceiling before anyone notices. That is a real bound, not a preference — and\nit is high enough that reaching it means the work really is two things.\n\n### 3. Group at three\n\nThree or more implementable tickets is an epic. The surface proposes an epic\nparent plus an **ordered child manifest**, and resolves its own approval gate\nbefore anything is created.\n\nOne or two tickets are ordinary siblings: no epic parent, no manifest. The\nthreshold is exactly three — not \"several\", not \"a lot\".\n\nThe manifest carries, per child: the boundary of its scope, its size band,\n`depends_on` (hard prerequisites that must land first), `recommended_after` (soft\nsequencing preferences that are not blockers), and a one-line order rationale.\nHard prerequisites and soft sequencing stay strictly separate, because the\nrecommended implementation order is derived from them and conflating the two\nproduces a serialized order where a parallel one was available.\n\nApproval follows each surface's **existing** attended/unattended rule. Nothing\nhere introduces a new gate policy: `/define-tickets` requires an explicit\naffirmative because creation is irreversible, and the recipe path gates on the\npipeline's own auto-approval variable. What is *not* conditional is the grouping\nitself — an unattended run still produces the epic; only the gate's behavior\nvaries.\n\nDecomposition happens **once**. The pass that decides the split freezes the\nmanifest; body rendering then fans out one writer invocation per entry against\nthat frozen manifest. A rendering invocation may not re-split, merge, reorder,\nrenumber, or rescope. Two independent decisions about the same split disagree,\nand the disagreement surfaces as children that overlap or contradict their\nparent.\n\n### 4. Hand off to exactly one conductor\n\nAn epic handoff names exactly one conductor entry point: `drive-epic`.\n\nBridge currently has two conductors — the v2 server-side engine and the LLM\nconductor pilot — and a standing rule that they must never operate on the same\nepic, because two transition authorities on one epic wedge it permanently. Asking\na model to pick correctly every time is not a control. `drive-epic` makes the\nchoice structural instead: it reads conductor readiness and routes to the one\npath the project can actually run, so no prompt names either underlying conductor\nand no prompt can present both.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes — no prompt, bundled doc, command mirror, or posture\ntest moves.\n\n## Epic coherence check before creation\n\nBefore an epic's tickets are created or its plan approved, its children are\nchecked **as a set** against the epic's own acceptance criteria. The check asks\nwhether every epic criterion has a child that carries out its verification, and\nwhether each cross-child hand-off is owned at both ends. It runs through one\nshared procedure, [`review-epic-coherence.md`](../mcp_server/instructions/review-epic-coherence.md),\nwhich writes `epic-coherence.json` (the record) and `epic-coherence.md` (a short\nsummary) beside the frozen manifest.\n\n**Where it runs.** Four call sites run the check:\n\n- `/define-tickets` Stage 9 (attended) — after the epic split is frozen, before\n the outline and the single `create` gate.\n- `/define-tickets --auto` Stage 9 (unattended) — after the split is frozen and\n capped by `--max-children`, before any writer runs. It has no create gate: an\n independently cleared record is what allows creation.\n- `idea-to-ticket` (the still-bundled legacy recipe) — in the Epic path of\n `upload-and-track.md`, after `decomposition-plan.json` and before rendering and\n the external approval gate. `/idea-to-pr` no longer runs it: it reaches the\n coherence check through `/define-tickets` Stage 9 under `--auto`.\n- `plan-epic` — in `decompose-epic.md`, after the plan artifacts are written and\n before the decomposition is presented for approval.\n\nEvery decomposition pass fills in each child's `owns_verifications`: which epic\ncriterion it proves, and the concrete part of that criterion's verification it\ncarries out. When a criterion is proven by an owner-run step a person runs\noutside the conductor — a release-checklist step, say — the pass declares a\n`non_child_owners` entry naming who runs it, the step, its verbatim text, and\nwhere the result is recorded, instead of forcing a child to own it. Owner-run\nowners count toward coverage only; hand-offs stay child-owned.\n\n**The render preflight.** `render-ticket-manifest.md` refuses to render an epic\nunless the record exists beside the manifest, all three of its digests\n(`manifest_sha256`, `child_evidence_sha256`, and `non_child_owners_sha256`) match\nwhat is being rendered, those digests equal the last reviewer round's, and\nits verdict is `pass`, `accepted_with_gaps`, or `not_checked` with a recorded\nbypass. A missing record, a mismatch, `blocked`, or an un-bypassed `not_checked`\nstops the flow before any writer runs or any ticket is created. `/define-tickets`\nstates the same rule in its own creation step.\n\n**Attended runs: fix or accept, at one gate.** Each remaining blocking finding is\neither fixed — the manifest is amended, re-frozen, and checked again — or\naccepted with a non-empty reason, which is recorded and turns the verdict into\n`accepted_with_gaps`. The fix budget is bounded: after the first confirmation\npass, the operator may direct one fix and one further whole-manifest\nconfirmation pass, counted across re-runs in the same attended `/define-tickets`\nrun. After that, a blocker is accepted or stays `blocked`.\n\n`/define-tickets` resolves all of this at one create gate. The gate lists every\nremaining blocker and any unresolved declined council, each with a recommended\naction, its exact override target, and an agent-drafted reason shown before\napproval. Replying `create` accepts only the items recommended for acceptance;\nnaming an item opens discussion and creates nothing. Changing the list, a target,\nor a reason means the gate is shown again.\n\n**Council pre-approval.** The consent for a high-risk coherence council is\ncollected earlier, in Stage 6's single reply, so the create gate carries no\nconsent question. `council` and `proceed` pre-approve the initial council;\n`proceed no-council` declines it and records `council_outcome:\n\"declined_by_operator\"`. A declined council stays unresolved until an attended\n`accept` override targets `council: \"declined_by_operator\"` with a non-empty\nreason. Callers that supply no pre-approval keep the procedure's own\npre-round-1 ask.\n\n**What counts as high risk.** Six or more children, or one child editing\n`db/alembic/`, `db/migrations/`, `api/library/billing/`,\n`api/routes/setup/auth.py`, or `api/library/auth/`. A child editing\n`api/library/db/` or another executor-containment path is high risk only when at\nleast two distinct children edit such paths; the procedure's case table is the\nsingle statement of this rule. Reads never count. A lone such child still gets\nthe low-risk reviewer rounds, is promoted to high risk if a blocking finding is\nuncertain, and reaches the create gate with any remaining blockers.\n\nWhen the reviewer cannot run, the verdict is `not_checked`. The operator can\nretry, or bypass with a typed reason. A bypass is recorded, and the verdict stays\n`not_checked`; it is never shown as a pass.\n\n**Unattended runs: the risk-scaled round limit, then halt.** An unattended\n`idea-to-ticket` run or `/define-tickets --auto` run amends and re-checks within the procedure's risk-scaled\nround limit: at most 2 reviewer runs for a low-risk epic and 3 for a high-risk\none. If a blocking finding remains, it halts with `blocked` before any ticket is\ncreated. A fix made after the last round gets one confirmation pass. An unavailable reviewer, or a check\nthat could not run independently, halts with `not_checked`.\n\nUnattended runs have no bypass; an operator who needs one reruns attended.\n`/define-tickets --auto` follows the same rule.\n\n**Siblings skip it.** A one- or two-ticket outcome runs no check, writes no\nrecord, and costs nothing extra. The residual risk — a pair that really needed a\nset-level check — is contained by the existing manifest-identity and\nwriter-draft gates. If the work is really epic-shaped, regroup it as an epic\nbefore creation. `plan-epic` is the one exception: the epic already exists, so it\nchecks even a one- or two-sub-task decomposition.\n\n**What lands in the tickets.** Each epic child carries its obligations verbatim\nas acceptance criteria. The epic parent carries a `## Epic coverage map`\nsection: the verdict, one bullet per epic criterion naming its owning child and\nobligation, and any accepted findings or bypasses with their reasons.\n\n## The closed exception list\n\nExactly three exceptions permit departing from the rules above. Invoking one\nrequires **no announcement** — the departure is silent by design, because a\nmandatory announcement would be one more instruction to drift from, and the cost\nof silence was weighed and accepted.\n\n| Id | Exception | Objective trigger | Bypasses |\n| --- | --- | --- | --- |\n| **E1** | External-tracker mirroring | A recorded upstream identifier exists and its granularity is contractual | Sizing and the epic threshold |\n| **E2** | Discovery-only spike | No committed production-code deliverable | Sizing only — **not** drafting through the writer |\n| **E3** | Authorized incident containment | Tied to an active incident record, not to schedule pressure | Sizing and the epic threshold |\n\nEach trigger is objective: an identifier that exists, a deliverable that is\nabsent, an incident record that is open. None of them is a judgement about how\nthe work feels.\n\n### The list is closed\n\nAnything outside the three rows above is an **escalation to the operator**, not a\njudgement call. The following are explicitly refused as grounds for departure:\n\n- a single-file trivial fix — that is `S` reached through the normal path;\n- generic time pressure;\n- \"the request is already well specified\";\n- \"it would be faster without the writer\";\n- developer discretion;\n- \"it's just a minor refactor\";\n- running unattended;\n- context limits;\n- \"this is hard to decompose\" — XL is the normal overflow, so that is the\n ordinary path and not a departure.\n\n**Writer unavailability escalates.** It never silently authorizes inline\ndrafting. A surface that cannot reach `jira-ticket-writer` stops and says so.\n\n## The spike ticket format (BAPI-986)\n\nA spike ticket defines a finding to be produced, not code to be written, so it\ngets its own body shape. This is a second **format**, not a fourth exception:\nthe writer still drafts it, the sizing ladder still applies, and E2 still governs\nwhether the posture's sizing rule is bypassed at all.\n\n### Who chooses, and on what\n\nThe run manifest's `scope` chooses, and it is resolved once, upstream, in\n`mcp_server/instructions/preflight-and-readiness.md`. The writer receives that\nvalue and never decides it. Only `scope: spike` selects the spike format.\n\nThe trigger is E2's exact objective condition — **no committed production-code\ndeliverable**. A deliverable that is absent, not a judgement about how the work\nfeels. Every other case produces the ordinary code-ticket format:\n\n- `scope: task` and `scope: epic_candidate`;\n- a missing scope value;\n- a malformed or unrecognized scope value;\n- a standalone writer invocation with no resolved spike scope;\n- any ambiguity.\n\nCode is the default **in code, not in prose**: it is what every unresolved path\nalready produces, so no one has to remember a rule to get it.\n\n### The five body sections\n\nA spike draft's substantive body is exactly these five, in order:\n\n1. `## Question` — the single question the investigation must answer.\n2. `## Scope` — the systems, code paths, data, and behavior in bounds.\n3. `## Out-of-scope` — what it will not examine, stated explicitly.\n4. `## What the findings must answer (the deliverable)` — names an inspectable\n findings artifact at a specific repository path, and states what that artifact\n must contain: the decisions it settles, the evidence it cites, the\n recommendations it makes, and the unresolved risks it records. This is the\n spike's analogue of Acceptance Criteria — a reviewer holds the finished\n artifact against it to tell whether the spike is done.\n5. `## Which related tickets consume the finding` — names real consumer tickets\n by key, or the literal `none yet`. Never an invented key.\n\nThose five are the spike-specific **body** sections. They are not the whole\ndraft. The following remain mandatory structural metadata, identical to a code\ndraft, and are not additional spike sections:\n\n- the `# [Title]` heading;\n- the execution-class marker line (below);\n- a `## Regression Completeness Notes` block when the gated regression pass\n applies;\n- the trailing `## Materials & Access` section, always last.\n\nThe code-ticket structure — Summary, Requirements, Acceptance Criteria,\nMaterials & Access — is unchanged, and a code draft carries no execution-class\nmarker.\n\n### The execution-class marker\n\nA spike draft carries exactly one canonical marker line, alone on its own line,\nimmediately after the title and before `## Question`:\n\n```markdown\n# [Concise Title Describing the Investigation]\n<!-- bridge:execution-class=investigation -->\n\n## Question\n```\n\nIt appears **exactly once**, never in a code-format ticket, and never\nillustratively inside a generated ticket body — not in prose, not in a fence,\nnot in backticks. Duplicates do not reinforce it: a second occurrence makes the\ndescription ambiguous and the parser discards the whole thing, leaving no signal.\n\n**It renders as visible text.** Bridge converts the description from Markdown to\nAtlassian Document Format, and ADF has no comment node, so the marker becomes one\nvisible line in the Jira description rather than a hidden comment. The exact\ntoken survives conversion and stays parseable from the stored description, which\nis what it is for. The placement immediately after the title is therefore an\neditorial choice as much as a parsing one.\n\n**It is a weak hint.** One non-authoritative input alongside the existing weak\nJira issue-type signal. It never overrides the five-way ticket-type\nclassification and never bypasses a human gate. A classifier verdict that\ncontradicts it wins, and the disagreement is reported rather than reconciled.\n\nThe marker's shape lives in exactly one runtime place —\n`parse_execution_class_marker` in\n`src/python/llms/agents/planner_agent/ticket_evaluator/execution_class_marker_utils.py`,\nwhich imports the `code`/`investigation` vocabulary from BAPI-980's constants in\n`api/models/epic_run.py` rather than redeclaring it. The literal in this document\nand in `agents/src/jira-ticket-writer.md` is pinned against that runtime constant\nby test, because Markdown cannot import a Python value.\n\n### The marker survives to the ticket record\n\nThe single-ticket path creates the Jira issue from `slim_description`; the full\ndraft is only an attachment. A marker preserved solely in the attachment would\nleave the ticket record with no signal at all. Two rules close that:\n\n- `mcp_server/instructions/draft-and-critique.md` carries the title and marker\n into `slim_description` in their original order, exactly once, and exempts that\n one literal from the forbidden-raw-HTML hygiene rule that would otherwise strip\n it. A `task` draft has no exemption, and slimming must not synthesize a marker.\n- `mcp_server/instructions/upload-and-track.md` passes `slim_description`\n unchanged as `create_ticket`'s `description`, without stripping, reconstructing,\n relocating, duplicating, or validating the marker.\n\n`create_ticket` gains no execution-class parameter and the MCP tool schema is\nunchanged: the marker rides in `description` and nowhere else.\n\n### Post-creation marker loss is out of scope\n\nA later description rewrite can drop the marker. `ticket_store_lib.update_ticket`\nforwards a full replacement description and the Jira backend re-renders all of it\nthrough `render_adf`, so anything a rewrite omits is simply gone — there is no\nmerge. Preserving the marker across rewrites would mean auditing every\ndescription-rewriting surface, which is excluded here.\n\nThis is a named, owned gap rather than an unstated one: preservation belongs to\nthe downstream slice that enables investigation routing. No local design record\nnames that slice yet, so its ownership is recorded here as an unresolved\ndownstream discovery rather than assigned to an invented slice name.\n\n## The plan-route marker\n\nA code-format ticket whose requested changes are entirely test code or test\ntooling may declare its plan route with one marker line:\n\n```markdown\n<!-- bridge:plan-route=test -->\n```\n\nThe runtime owner of the literal is `plan_route_marker_utils.py` in\n`src/python/llms/agents/planner_agent/ticket_evaluator/`\n(`PLAN_ROUTE_MARKER_TEMPLATE`); the writer copies it, never redefines it.\n\n### Writer rules\n\n- **Scope.** Stamp it only when all requested work is test code or test tooling:\n tests, flaky-test fixes, fixtures, fakes, harnesses, test configuration,\n runners, development/test dependencies, test-running CI workflows, and\n documentation about those tests.\n- **Placement.** Exactly once, alone, immediately after the title line.\n Otherwise stamp nothing and let the classifier decide.\n- **Test only.** The writer never authors an `implementation` or `bug`\n declaration, and never puts a plan-route declaration on a spike draft.\n- **Never illustratively.** The marker does not appear anywhere else in a\n generated ticket body.\n\n### Parser validity is broader than writer placement\n\nThe parser accepts one canonical literal alone on an unfenced, unindented line\nanywhere in the body. A malformed, inline, indented, fenced, or duplicated\noccurrence is no declaration.\n\n### Consequences\n\nJira renders the marker as one visible line. Unlike the weak-hint\nexecution-class marker, a valid plan-route declaration **binds** plan routing.\nMisused on production-code work, planning fails visibly, and ticket review\nrecommends a split or removal of the declaration (the critic's\n`Split recommended (unrelated test changes):` finding). The marker and the\nspike-only execution-class marker are the two limited exceptions to the\nno-raw-HTML and `{placeholder}` output rules.\n\n## Residual risks and their operational contracts\n\nFour limitations ship with the spike format. Each is stated as detection,\ncontainment, and escalation, because a residual-risk note without those three\nreads as coverage while providing none.\n\n### Incorrect E1/E2/E3 invocation\n\n- **Detection**: manual ticket review only. There is no log, counter, or report\n (see the accepted trade-off below).\n- **Containment**: the objective selection rule — any ambiguity produces a code\n ticket — plus the marker's non-authority over classification and every human\n gate.\n- **Escalation**: suspected misuse goes to the authoring-workflow owner.\n\n### Class observability does not cover departure invocation\n\nThe marker makes a spike's intended execution **class** observable. It does not\nmake E1/E2/E3 posture-departure **invocation** observable; that remains silent by\naccepted design, and the accepted trade-off below is unchanged by this slice.\n\n- **Detection**: none for invocation, by design. Ticket review is the only signal.\n- **Containment**: the closed exception list itself — anything outside those\n three rows is an escalation, not a judgement call.\n- **Escalation**: the authoring-workflow owner, as above.\n\n### Premature routing on the marker\n\nMarker-bearing tickets exist before any consumer does. Nothing routes on the\nmarker yet, and the guard for that is **structural, not a feature flag** — no\nsuch flag exists, and naming an imaginary control would be exactly the failure\nthe accepted trade-off below warns about.\n\n- **Detection**: the absence of any marker reader outside the parse/normalize\n helper and the disagreement observer. A static test pins that set, so the first\n premature consumer fails at review time.\n- **Containment**: BAPI-980 seeds every run node `execution_class = 'code'` and\n exposes no caller-controlled `investigation` override anywhere — not on the DAL\n seed signature, not on a Pydantic request model, not on an MCP input. No code\n path writes `investigation`.\n- **Escalation**: any proposal to consume the field in runtime conductor behavior\n goes to the Conductor owner before implementation, per BAPI-980's decision D10;\n any discovered marker-based routing is a release-blocking regression on that\n same path. Enabling investigation routing belongs to the downstream epic\n rollout owner, once the complete path and its human gate exist.\n\n### Marker/classifier disagreement\n\n- **Detection**: the reserved structured-log event\n `ticket_classification_event=execution_class_marker_disagreement`, emitted at\n warning level when an investigation marker meets a code-facing classifier\n verdict. Every `evaluate_ticket_type` entry point compares, so the signal\n covers the ordinary Bridge classification path and not only the estimator\n backfill; a cached verdict is not a classification and is not compared.\n Persistent or high-volume disagreement is surfaced by querying that reserved\n key through the existing logging/Sentry workflow.\n- **Containment**: the comparison is non-authoritative. The classifier verdict is\n persisted and returned exactly as produced, whether or not an event fires.\n- **Escalation**: the event is **diagnostic-only** and the Bridge platform team\n owns it operationally. No alert rule, threshold, or paging behavior ships with\n it; that is net-new work belonging to the slice that enables routing. The field\n and redaction contract lives in\n `docs/claude/jira-optional-ticket-storage.md`, not here.\n\n## Isolation binding for shared-state test support\n\nA ticket that builds or changes test support which resets shared state — a\nharness, a fixture, a reset helper, anything that truncates, drops, or\nreinitializes state another process or another run can also read or write —\nmust state its isolation binding as a requirement: which database, directory,\nor other shared resource the reset targets, and how the ticket's own\ndeliverable proves that target's identity before it destroys anything.\n\nThis doctrine exists because BAPI-1006 shipped a conductor integration-test\nharness with nothing in its ticket stating what the harness's destructive\nreset was bound to, and a v2 worker dispatched to build it truncated the\noperator's operational `bridgeapi` database (Architecture Miss 28). The\ncontainment-hazard classifier this repository runs at planning time\n(`api/library/github/workflow_planning_prediction.py`) can flag such a ticket\nfrom its text, but the flag is advisory — it surfaces the hazard before\ndispatch, it does not prevent a destructive worker on its own. Stating the\nisolation binding in the ticket itself is the durable requirement; treat this\nas an authoring-posture rule, not as runtime enforcement.\n\n## Accepted trade-off: silent departure is unobservable\n\nObservability was deliberately dropped when this posture was ratified. A model\nmay invoke E1, E2, or E3 without recording that it did, so posture drift is only\ndetectable through ticket quality — not through a log, a counter, or a report.\n\nThis is known and accepted. The alternative was another mandatory instruction on\nevery surface, and an instruction that is skipped silently is worse than one that\ndoes not exist: it reads as coverage while providing none.\n\n## Why duplication, not a shared include\n\nCommands, agents, instructions, and docs have four separate build paths in this\nrepository and no shared compiler. Introducing a generated include step to share\none block would mean a fifth build path, a placeholder that can go unresolved,\nand a failure mode where a carrier ships with the placeholder text still in it.\n\nMarker-delimited duplication plus one byte-equality test is the right mechanism\nat this scale. The test reads the canonical sources directly — never the\ngenerated command mirrors, whose byte-identity the command tests already cover —\nand permits **no** per-surface variation. Any drift, down to a single byte, fails.\n\n## A fresh install inherits this\n\nNo configuration step, no server call. The posture reaches a new project through\nthe packaged bundles that `--init` scaffolds:\n\n- `COMMANDS` (`mcp_server/src/commands.generated.ts`) — carries\n `commands/src/define-tickets.md`;\n- `AGENTS` (`mcp_server/src/agents.generated.ts`) — carries\n `agents/src/jira-ticket-writer.md`, including the compressed posture line in\n its `description:` frontmatter;\n- `INSTRUCTIONS` (`mcp_server/src/pipelines.generated.ts`) — carries the\n canonical source and both decomposition instructions;\n- `DOCS` (`mcp_server/src/docs.generated.ts`) — carries this document.\n\nThe compressed frontmatter line matters more than its size suggests: agent\ndescriptions land in every session's system prompt with no file read and no\n`tools/list` cost, so it is the entire bare-chat coverage story.\n\n## Worked examples\n\n**One ticket.** \"Add a `--json` flag to `doctor`.\" Two files and a test, ~90 LOC.\nThat is `M`. One ticket, drafted by the writer, no epic, no manifest, no\nconductor handoff.\n\n**Two tickets.** \"Add rate limiting to the LLM client, and surface the limit in\nthe config UI.\" Backend and frontend are independently implementable and land\nseparately: two ordinary siblings. Still no epic — the threshold is three.\n\n**Four tickets → an epic.** \"Make local ticket mode a first-class system.\"\nDecomposition freezes a parent plus four children, each `L`, with `depends_on`\nnaming the one child that must land first. The full manifest goes to the approval\ngate; on approval, four writer invocations render four bodies against their\nfrozen entries; creation follows `upload-epic-hierarchy.md`; the handoff names\n`drive-epic` and nothing else.\n\n**A child that outgrows `L`.** A proposed child comes out at 19 files. It ships\nas one `XL` child. Do not split it into two `L` children to make it fit — the\nslice is one coherent piece of work, and halving it buys a second worktree, a\nsecond PR, and a rebase between them in exchange for nothing. Split only if the\n19 files really are two independent deliverables.\n\n**Past the ceiling.** A proposed ticket comes out at 60 files and ~5000 LOC.\nThat is over the bound, so it splits — but into the largest coherent pieces\navailable, not into a swarm. Two `XL` tickets is the right answer here; six `M`\nones is not.\n",
|
|
7
|
+
"docs/install/review-epic-coherence-procedure.md": "# Review Epic Coherence\n\nThis is the shared procedure for checking an epic's children **as a set**. It answers two\nquestions no per-ticket review asks: does every epic acceptance criterion have a child that\ncarries out its verification, and is everything one child produces wired to whatever consumes\nit? Drafting surfaces and the on-demand `/review-epic-coherence` command run this same text.\n\nThe check fails **visibly**. A run that cannot finish is `not_checked`. It is never `pass`.\n\n**Enforcement status.** The three drafting surfaces — `define-tickets`, `plan-epic`, and\n`idea-to-ticket` — call this procedure in drafting mode before creation or approval, and\n`render-ticket-manifest.md` refuses to render an epic without a clearing record. A\nreport-mode result from the on-demand command is a diagnostic, not a gate: it clears nothing.\n\n## Inputs\n\nState these four values before starting:\n\n1. **Input document path** — the path to `epic-coherence-input.json`.\n2. **Output directory** — a directory beneath the `docs_dir` that the `ping` tool returned.\n3. **Mode** — `drafting` or `report`.\n4. **Unattended** — true only when `auto_approve_external == \"true\"`. Otherwise the run is\n attended.\n\nIf any of the four is missing, stop and report which one. Do not guess a default.\n\nAn optional fifth input, **`council_preapproval`**, may accompany them. It carries a `value`\n(`approved` or `declined`) and its `source` (for example, the operator's Stage 6 reply in\n`define-tickets`). It records the operator's prior consent for the **initial** high-risk\ncouncil only; see \"Council call\". It is not one of the required inputs, so its absence never\nstops the run. When it is absent, the attended pre-round-1 ask stays exactly as written below.\n\n## The input document\n\nThe document carries artifacts only. It never carries the drafting conversation.\n\n```json\n{\n \"epic\": {\n \"key\": \"EPIC-1\",\n \"title\": \"Short epic title\",\n \"acceptance_criteria\": [\n { \"id\": \"AC-1\", \"text\": \"What must be true\", \"verification\": \"How it is proven\" }\n ]\n },\n \"design_text\": \"Goals and design text: the exploration's Proposed Design, the epic parent body, or equivalent.\",\n \"manifest\": [\n {\n \"id\": \"1\",\n \"title\": \"Child title\",\n \"scope\": \"What the child does\",\n \"size_band\": \"L\",\n \"depends_on\": [],\n \"recommended_after\": [],\n \"touched_files\": [\"path/relative/to/repo.py\"],\n \"read_files\": [\"path/the/child/only/reads.py\"],\n \"owns_verifications\": [\n { \"epic_ac\": \"AC-1\", \"obligation\": \"The concrete part of AC-1's verification this child carries out\" }\n ]\n }\n ],\n \"child_evidence\": [\n {\n \"id\": \"1\",\n \"acceptance_criteria\": [\"Each acceptance criterion of the child, verbatim\"],\n \"body\": \"The child ticket body, if available\"\n }\n ],\n \"non_child_owners\": [\n {\n \"id\": \"OR-1\",\n \"kind\": \"owner_run\",\n \"who\": \"Release owner\",\n \"step_ref\": \"Release checklist step 6\",\n \"step_text\": \"Release checklist step 6: the release owner runs the smoke test against production and posts the output as a comment on the epic.\",\n \"records_result_in\": \"a comment on the epic\",\n \"owns_verifications\": [\n { \"epic_ac\": \"AC-2\", \"obligation\": \"Run the production smoke test and post its output on the epic\" }\n ]\n }\n ]\n}\n```\n\nField rules:\n\n- `epic` carries either `key` or `slug` (one is enough), plus `title` and\n `acceptance_criteria`. Each acceptance criterion has `id`, `text`, and `verification`. When\n an epic AC has no verification sentence, coverage is judged against its text, the basis is\n `text_only`, and the reviewer raises an advisory input finding naming the missing sentence.\n- `design_text` is the goals and design text.\n- `manifest` is the **complete, ordered** child list. Each entry carries `id` (a placeholder\n index or a real ticket key), `title`, `scope`, `size_band`, `depends_on`, and\n `recommended_after`. These are the same field names `decompose-epic.md` and\n `decompose-epic-candidate.md` already use.\n- `touched_files` is optional. It lists the files the child **edits or creates**. When absent,\n the fit check uses the file paths the child's scope and body name. A `touched_files` path that\n does not exist yet can be a planned creation; that alone is not a problem.\n- `read_files` is optional. It lists files the child **only reads**. It lives in the manifest,\n so it is hashed. Reads inform the fit check. Only edits (`touched_files`) count toward edit\n overlap between children and toward the high-risk path check in \"Escalation rule\". A path\n must not appear in both lists. A `read_files` path that does not exist is an `input` problem,\n because a child cannot read a file that is not there. When `read_files` is absent, behavior is\n unchanged.\n- `owns_verifications` is optional. It is a list of entries with `epic_ac` (an epic AC id) and\n `obligation` (the concrete part of that AC's *verification* this child will carry out, in\n words a reviewer can hold against the verification sentence). It is a **claim** the reviewer\n checks. It is never proof by itself. When it is absent (older epics, the on-demand path), the\n reviewer derives candidate owners from each child's own acceptance criteria, and the record\n says `owns_verifications_source: derived`. When it is present, the record says `declared`.\n- `child_evidence` is a **separate top-level array**, keyed by child `id`. Each entry may carry\n `acceptance_criteria` and `body`. It lives **outside** the hashed manifest, so evidence can be\n refreshed without changing manifest identity. Derived ownership counts only when a child's\n acceptance criteria contain a concrete commitment to the obligation. When the material needed\n to establish ownership is absent, that is an explicit `input` gap, not coverage.\n- `non_child_owners` is an optional **top-level array** beside `child_evidence`. It names the\n owners of epic AC verifications that no child carries out, such as a step in an owner-run\n release checklist. Each entry has:\n - `id` — for example `OR-1`. It is unique across the array and never equal to any child `id`.\n - `kind` — a closed vocabulary with one value: `owner_run`.\n - `who` — a named person or role, for example \"Release owner\".\n - `step_ref` — the step it points at, for example \"Release checklist step 6\".\n - `step_text` — the step's text, copied **verbatim** from `design_text`.\n - `records_result_in` — where the result is recorded, for example \"a comment on the epic\".\n - `owns_verifications` — a list of `epic_ac` plus `obligation` entries, the same shape\n children use.\n\n `step_text` is copied into the entry on purpose. `design_text` is not hashed, so a step that\n lived only there could change without changing the review's identity. The copied, hashed\n `step_text` is the reviewed commitment. An owner-run owner owns **coverage only**: it never\n fills a hand-off grid cell. When the array is absent, no AC has a non-child owner.\n\n**Local markdown drafts.** When an epic is drafted as local markdown files outside a drafting\nsurface, build this document with the packaged `epic-coherence-input build` subcommand of the\nBridge MCP package: `epic-coherence-input build --outline <outline-path> --out <output-path>`.\nDo not assemble it by hand. The outline names the epic draft, each child's draft, its\ndependencies, size band, `edits` (which become `touched_files`), `reads` (which become\n`read_files`), and `owns_verifications`, plus any `non_child_owners`. Every epic AC bullet in the\nepic draft must start with an explicit id such as `AC-3:`; the builder never numbers ACs by\nposition or matches them by keyword. `epic-coherence-input check <input-path>` validates an\nexisting document against the structural rules in \"Validation\" and prints the three digests. It\nnever judges coverage; that stays the reviewer's job. The three drafting surfaces keep their\nstructured normalization, and the on-demand `/review-epic-coherence` command keeps building its\ninput from ticket data.\n\n## Identities\n\nThree digests identify what was reviewed. All three use the same canonical JSON encoding.\n\n- `manifest_sha256` is the SHA-256 hex digest of\n `json.dumps(manifest, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")`,\n computed over the `manifest` array only. Compute it locally with this one line:\n\n ```bash\n python3 -c 'import hashlib,json,sys; manifest=json.load(open(sys.argv[1], encoding=\"utf-8\"))[\"manifest\"]; print(hashlib.sha256(json.dumps(manifest, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")).hexdigest())' <input-path>\n ```\n\n- `child_evidence_sha256` is the SHA-256 hex digest of the same encoding applied to the\n `child_evidence` array (an absent array hashes as an empty array).\n- `non_child_owners_sha256` is the SHA-256 hex digest of the same encoding applied to the\n `non_child_owners` array. An absent array hashes as an empty array, as `child_evidence`\n does. Compute it locally with this one line:\n\n ```bash\n python3 -c 'import hashlib,json,sys; owners=json.load(open(sys.argv[1], encoding=\"utf-8\")).get(\"non_child_owners\", []); print(hashlib.sha256(json.dumps(owners, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")).hexdigest())' <input-path>\n ```\n\nRecord **all three** digests, at the top level and in every round. A caller that wants to\nreuse a prior review must compare **all three** identities against the manifest, evidence,\nand non-child owners it is about to use. A matching manifest with changed evidence or changed\nowners is not the same review. The sibling integration's preflight must compare all three.\n\n`design_text` deliberately stays **outside** identity. The copied and hashed `step_text` is\nthe reviewed commitment of an owner-run step. There is no fourth digest, and no render-time\ncheck that `design_text` still contains the step.\n\n## Validation\n\nBefore any reviewer run, validate the input document. Every problem becomes a visible `input`\nfinding. Nothing is silently dropped.\n\n- Child ids are unique. Epic AC ids are unique. Every `owns_verifications.epic_ac` names a real\n epic AC, and every `child_evidence.id` names a real manifest child.\n- The document shapes and the key or slug syntax are valid.\n- Every `non_child_owners` entry is well formed. Its `id` is unique across the array and never\n collides with a child id. Its `kind` is `owner_run`. Its `who`, `step_ref`, `step_text`, and\n `records_result_in` are non-empty. Every one of its `owns_verifications.epic_ac` values names\n a real epic AC. A malformed entry, a colliding id, an invalid AC reference, and a missing\n required field are each a visible `input` finding.\n- Every named file path is repository-relative. Reject absolute paths and any path that\n traverses upward. A path listed in both `touched_files` and `read_files` of the same child is\n an `input` finding.\n- The output directory is beneath the `docs_dir` that `ping` resolved. Refuse to write anywhere\n else.\n- A missing, invalid, or unreadable named file is an `input` finding. So is inaccessible child\n material and a missing set of epic acceptance criteria. A nonexistent `touched_files` path can\n be a planned creation and is not, by itself, a finding. A nonexistent `read_files` path is an\n `input` finding.\n- A missing verification sentence alone is **advisory**. Any problem that prevents a meaningful\n review (no epic acceptance criteria, an empty manifest, an unreadable input document) makes\n the verdict `not_checked`. It never produces a complete-looking coverage map.\n\nDescribe variable paths in words or angle brackets, such as `<output-directory>`. Do not write\nany literal brace-wrapped identifier other than `docs_dir`, including in examples.\n\n## Reviewer run\n\nInvoke the `epic-coherence-reviewer` agent in a **fresh context**. Pass it only the input\ndocument path. Run it **once per round over the whole frozen manifest** — never per child and\nnever over a subset. The reviewer must not receive the drafting conversation.\n\nResolve the agent the way `draft-ticket.md` resolves `jira-ticket-writer`: through the Task tool\nor a host equivalent you have confirmed can resolve it. Cursor has no agent mirror for this\nagent, so on Cursor resolution normally fails.\n\n**Fresh-context fallback.** If no fresh context can be obtained, read the agent file\n`agents/src/epic-coherence-reviewer.md` (or its installed mirror) and execute its methodology\ninline. Set `independent: false` in the record and say so in the report. An inline run is never\na silent independent pass.\n\n- In an **attended** run, offer a fresh-context rerun when one becomes available.\n- In an **unattended drafting** run, an inline `pass` is not the independent gate. Halt.\n\n## Escalation rule\n\nEscalate to a council **only** when at least one of these is true:\n\n1. Any blocking finding has `confidence: uncertain`.\n2. The caller **disputes** a blocking finding instead of amending it. The caller is the drafting\n agent in drafting mode and the operator in report mode.\n3. The epic is **high risk**.\n\nHigh risk means one of the following. Count **children**, never edited files: a single child\nthat edits ten files under one prefix is still one child.\n\n- The epic has **six or more children**.\n- **One** child's `touched_files` falls under one of these paths. A single editing child is\n enough:\n - `db/migrations/` and `db/alembic/` (the design says `db/migrations/`; this repository's\n migrations live in `db/alembic/`, so match both)\n - `api/library/billing/`\n - `api/routes/setup/auth.py`\n - `api/library/auth/`\n- **Two or more distinct** children each edit under the same kind of path from the list below.\n One editing child alone does **not** make the epic high risk. This list is every remaining\n prefix in `_CONTAINMENT_HAZARD_PATHS` in `api/library/github/workflow_planning_prediction.py`\n (the list also carries `db/alembic/`, which is already a single-trigger path above):\n `mcp_server/src/executor/`, `mcp_server/src/agent-launchers/`,\n `mcp_server/src/claude-settings.ts`, `.github/workflows/`,\n `tests/integration/conductor_harness/`, and `api/library/db/`\n\n| Edits under the path | Risk tier |\n| --- | --- |\n| One child edits `api/library/db/` | low |\n| Two children each edit `api/library/db/` | high |\n| One child edits an executor prefix such as `mcp_server/src/executor/` | low |\n| Two children each edit executor-prefix paths | high |\n| One child edits `db/alembic/`, `api/library/billing/`, or auth paths | high |\n| Six or more children, whatever they edit | high |\n\nOnly edits count. A path a child lists in `read_files` never makes the epic high risk, even\nunder one of these prefixes, so a child that only reads `api/library/db/` does not trigger\nthis rule. Reads do not count toward the two-child threshold either.\n\nA low-risk epic with one child editing `api/library/db/` or an executor prefix still receives\nthe low-risk reviewer rounds in \"Drafting mode\". If a round raises a blocking finding with\n`confidence: uncertain`, the epic is promoted to high risk and the council flow starts. Any\nblocking findings that remain go to the operator at the create gate. This adds no other\nmitigation.\n\nWhen `touched_files` is absent for a child, apply the same path check to the concrete paths its\nscope and body name. When a child's file ownership is unknown, record **uncertainty**. Do not\ndeclare the epic low risk.\n\n**Classify the risk before round 1.** Decide the initial risk tier from the input alone,\nbefore the first reviewer run, using the child count and the edit paths. The tier is `high`\nwhen this rule matches, and also when unknown file ownership leaves the question open. It is\n`low` otherwise. Record it as `risk_tier` with its `risk_reasons`, for example \"Children 1 and 3\nboth edit `api/library/db/`\". Trigger 3 therefore fires **before round 1**. Triggers 1 and 2 still fire\nafter any later round.\n\nAn uncompleted required escalation leaves the result unresolved. It is never a quiet pass.\n\n## Council call\n\nBefore **every** billable council request, state:\n\n- the cost: **26–65 credits and about 15 minutes** (26 credits for standard models, 65 credits\n for frontier models; state the applicable tier if providers are pinned). The source is\n `api/library/billing/pricing.py`;\n- the escalation reason (which of the three triggers fired);\n- that council output cannot itself clear a blocker.\n\nAn **attended** run asks the operator to approve before proceeding. An **unattended** run states\nthe cost in its output and proceeds. This holds for every request, including a `mode: general`\nretry.\n\n**When the council starts.**\n\n- **High-risk trigger.** For an epic classified high risk before round 1, state the cost and,\n when attended, get the operator's approval **before** round 1 starts. Then request the\n council and start the round-1 reviewer run **in parallel**. Do not wait for a later round.\n How the approval is obtained depends on `council_preapproval`:\n - `approved`: state the cost, then start the initial council alongside round 1 without\n another ask.\n - **No value supplied:** keep the attended pre-round-1 ask above, unchanged.\n - `declined`: do not request that council. Record `council_id: null` and\n `council_outcome: \"declined_by_operator\"`.\n\n `council_preapproval` covers the **initial** high-risk council only. If its technical request\n needs the `mode: general` retry, state the cost again without asking again. A later uncertain\n or disputed council keeps its own cost statement and attended approval; the pre-approval never\n covers it.\n- **Uncertain or disputed trigger.** Request the council after the round that raised the\n finding.\n- **How many.** One council per drafting run for the initial high-risk trigger. A later\n qualifying trigger (an uncertain or disputed blocking finding) may start **at most one more**\n council, with its own cost statement and approval.\n\nCall `request_council` with:\n\n- `mode`: `technical`\n- `wait_for_result`: `false`\n- no `repo_name`\n- task text containing **only** the input document's artifacts and the contested findings.\n The early high-risk council has no reviewer findings yet, so its task text holds **only** the\n input document's artifacts. Never invent findings for it. Ask it to look for cross-child and\n release-level risks, such as concurrent writers, migration and rollback order, and rollout\n that runs ahead of the code.\n\nThen poll `get_council` for a bounded period. If technical mode cannot run because the\nrepository is not indexed, repeat the request with `mode: general`, and state the cost again\nbefore that request too.\n\n**The amendment freeze.** The round-1 amendment freezes at the moment the caller begins writing\nthe amended input document after collecting the round-1 findings. Poll `get_council` while the\nround-1 reviewer runs. Council conclusions received before the freeze join that **same**\namendment pass. Conclusions received after it join the next pass. A frozen round is never\nchanged after the fact.\n\nCouncil conclusions join the finding list as **separate** findings with `source: council`. The\noriginal reviewer finding is kept unchanged. It is never edited, downgraded, or removed because\na council failed, timed out, is still running, left the dispute unresolved, or concluded.\nRecord the `council_id` and the council outcome. Record still-running and failed outcomes as\nunresolved.\n\n**An unfinished required council.** A required council that is `still_running`, `failed`, or\n`timed_out` when the last round or confirmation pass ends is unresolved. The verdict is never\n`pass`. An **attended** operator, after being shown the council's id, cost, and elapsed time,\nmay accept it with a typed override of kind `accept` that names its `council_id`. An\n**unattended** run records `blocked` and halts before any ticket is created.\n\nA council declined through `council_preapproval: declined` is also unresolved. It has no\n`council_id`, so a null `council_id` is never a substitute target. Only an **attended**\noverride of kind `accept` with a non-empty reason and the exact target\n`council: \"declined_by_operator\"` clears it. Accepting a finding by `finding_id` never clears\nit. The reason may be typed by the operator or drafted by the agent and shown at the create\ngate where that gate applies. An **unattended** run records `blocked` and halts.\n\n## Drafting mode\n\nDrafting mode is for a drafting surface that can still change the manifest.\n\n- **Amend and recheck.** The reviewer only describes amendments. The **caller** amends the\n manifest (adds or rescopes children), re-freezes it, and writes a **new** input document. The\n procedure then reruns the reviewer in a fresh context. Record each round with its own\n `manifest_sha256`, `child_evidence_sha256`, and `non_child_owners_sha256`, blocking and\n advisory counts, whether a council ran, and whether it was a confirmation pass.\n- **Risk-scaled round limit.** The number of ordinary reviewer runs, `round_limit`, depends on\n the risk tier from \"Escalation rule\":\n - **Low risk** (the epic is not high risk and no blocking finding is `uncertain`):\n `round_limit` is **2**. Run one round, and one recheck only when blocking findings remain.\n That is at most **2 reviewer runs**.\n - **High risk**: `round_limit` is **3**. Allow at most **2 re-checks**, which is\n **3 reviewer runs in total**.\n - **Promotion.** A low-risk epic whose round produces a blocking finding with\n `confidence: uncertain` becomes high risk from that point. It gets the high-risk limit, and\n trigger 1 fires.\n\n Record the final `risk_tier`, `risk_reasons`, and `round_limit`. A confirmation pass is not an\n ordinary run and does not count toward `round_limit`.\n- **Confirmation pass.** When the caller changes the input after the last allowed ordinary\n round, run **one confirmation pass**. It is a fresh-context reviewer run over the **whole**\n amended manifest, never a subset. Its requested focus is narrower: the amended children, the\n hand-offs they touch, and whether the findings the amendment targeted are closed. It must\n still report **any** blocking finding it observes anywhere in the manifest; the narrower\n focus never suppresses one. When it cannot establish where an amendment's impact ends, it\n raises that as a blocking finding with `confidence: uncertain` instead of calling the\n amendment confirmed. Record it in `rounds[]` with `\"confirmation\": true`.\n - A confirmation pass **never** starts another automatic amendment loop. A new blocking\n finding goes to attended fix-or-accept, or to the unattended halt.\n - In an **attended** run, after the first confirmation pass, the operator may direct **at\n most one** fix of a confirmation blocker. Then run **one** further whole-manifest\n confirmation pass, so the last round bears the final digests. That is the whole budget\n across re-runs in the same attended `define-tickets` run.\n - Once that budget is spent, a remaining blocker is accepted with a non-empty reason or stays\n `blocked`. Do not add an \"unconfirmed fixed\" state, and do not start another automatic\n amendment loop. A typed acceptance stays available instead of the fix at any point.\n- **Final identity.** The top-level `manifest_sha256`, `child_evidence_sha256`, and\n `non_child_owners_sha256` equal the digests of the **last** entry in `rounds[]`. A record\n whose top-level digests no round saw does not clear. The sole exception is a confirmation\n pass that cannot run: an **attended** operator may record `amended_after_final_round: true`\n with an override of kind `bypass` carrying a typed reason. The verdict is then\n `not_checked`, never `pass` or `accepted_with_gaps`, and the top-level digests still describe\n the amended input. An **unattended** run halts instead.\n- **Attended resolution.** After the bound, each remaining blocking finding must be fixed within\n the same one-fix, one-further-confirmation-pass budget, or accepted with a non-empty reason.\n The reason may be typed by the operator or drafted by the agent and shown at the create gate\n where that gate applies. An accepted finding is an override with `kind: accept`, plus\n `finding_id`, `reason`, `accepted_by`, and `round`. Declining keeps the verdict `blocked`.\n When **every** remaining blocking finding is accepted, the verdict is `accepted_with_gaps`.\n- **Unattended halt.** If any blocking finding remains after the bound, the verdict is\n `blocked`. Return an explicit halt signal: **do not call `create_ticket`**. The caller must\n stop.\n\n## When the review cannot run\n\nIf the reviewer cannot run (a tool error, a timeout, or a host that lacks the capability while\ninline execution also fails), or the material prevents a meaningful review, the verdict is\n`not_checked`. It is never `pass`.\n\n- **Attended:** offer a retry, or a bypass with a typed reason. A bypass is recorded as an\n override with `kind: bypass`. The verdict stays `not_checked`.\n- **Unattended:** halt.\n\n## Report mode\n\nReport mode is diagnostic. It runs the reviewer **once**. There is no amendment loop and no halt\nsignal. Escalation still applies, with the cost stated and confirmation asked. The verdict is\n`pass`, `blocked`, or `not_checked`.\n\nA `blocked` or `not_checked` report is **diagnostic and not a clearance**.\n\n## The record: epic-coherence.json\n\nWrite `epic-coherence.json` into the output directory:\n\n```json\n{\n \"schema_version\": 2,\n \"checked_at\": \"2026-01-01T00:00:00Z\",\n \"mode\": \"drafting\",\n \"unattended\": false,\n \"manifest_sha256\": \"<64 hex characters, or null>\",\n \"child_evidence_sha256\": \"<64 hex characters, or null>\",\n \"non_child_owners_sha256\": \"<64 hex characters, or null>\",\n \"owns_verifications_source\": \"declared\",\n \"verdict\": \"pass\",\n \"independent\": true,\n \"risk_tier\": \"low\",\n \"risk_reasons\": [\"Three children; no child edits a high-risk path\"],\n \"round_limit\": 2,\n \"amended_after_final_round\": false,\n \"rounds\": [\n {\n \"manifest_sha256\": \"<64 hex characters>\",\n \"child_evidence_sha256\": \"<64 hex characters>\",\n \"non_child_owners_sha256\": \"<64 hex characters>\",\n \"blocking_count\": 0,\n \"advisory_count\": 0,\n \"council_ran\": false,\n \"confirmation\": false\n }\n ],\n \"owner_run_acs\": [\"AC-2\"],\n \"coverage_map\": [\n {\n \"epic_ac\": \"AC-1\",\n \"judgment_basis\": \"verification\",\n \"status\": \"covered\",\n \"owners\": [\n {\n \"owner_kind\": \"child\",\n \"child\": \"1\",\n \"obligation\": \"Concrete obligation\",\n \"child_ac\": \"The child AC id or a quoted child AC\",\n \"source\": \"declared\"\n }\n ]\n },\n {\n \"epic_ac\": \"AC-2\",\n \"judgment_basis\": \"verification\",\n \"status\": \"covered\",\n \"owners\": [\n {\n \"owner_kind\": \"owner_run\",\n \"child\": null,\n \"owner\": \"OR-1\",\n \"who\": \"Release owner\",\n \"step_ref\": \"Release checklist step 6\",\n \"records_result_in\": \"a comment on the epic\",\n \"obligation\": \"Run the production smoke test and post its output on the epic\",\n \"source\": \"declared\"\n }\n ]\n }\n ],\n \"handoff_grid\": [\n {\n \"thing\": \"The stored field, endpoint, flag, event, job payload, or scope\",\n \"consumer\": \"The consuming piece\",\n \"write_path\": \"child id or null\",\n \"read_path\": \"child id or null\",\n \"caller\": \"child id or null\",\n \"start\": \"child id or null\",\n \"steady_state\": \"child id or null\",\n \"wind_down\": \"child id or null\"\n }\n ],\n \"findings\": [\n {\n \"id\": \"F-1\",\n \"kind\": \"coverage\",\n \"severity\": \"blocking\",\n \"confidence\": \"certain\",\n \"concerns\": \"AC-1\",\n \"evidence\": \"What was observed, with file and line where code was read\",\n \"smallest_amendment\": \"The smallest change that would close it\",\n \"source\": \"reviewer\"\n }\n ],\n \"overrides\": [\n { \"kind\": \"accept\", \"finding_id\": \"F-1\", \"reason\": \"Typed reason\", \"accepted_by\": \"Operator\", \"round\": 3 }\n ],\n \"council_preapproval\": { \"value\": \"approved\", \"source\": \"define-tickets Stage 6 reply\" },\n \"council_id\": null,\n \"council_outcome\": \"not_run\",\n \"councils\": []\n}\n```\n\nRules:\n\n- `schema_version` is `2`. A reader also accepts a version `1` record: it has no\n `non_child_owners_sha256`, no owner-run owners, and no confirmation rounds, so it describes\n an input with no `non_child_owners`.\n- `verdict` is one of `pass`, `accepted_with_gaps`, `blocked`, `not_checked`.\n- `pass` requires **zero unresolved blocking findings**. A `pass` with `independent: false` is\n allowed but flagged prominently in both files.\n- An `accept` override accepts a known gap. A `bypass` override skips a review that could not\n run. Keep the two distinct.\n- `kind` on a finding is one of `coverage`, `handoff`, `fit`, `e2e_proof`, `input`.\n `severity` is `blocking` or `advisory`. `confidence` is `certain` or `uncertain`. `source`\n is `reviewer` or `council`.\n- Digests may be `null` **only** when unreadable input makes computing them impossible. Then\n add an explicit `input` finding and set the verdict to `not_checked`.\n- The top-level digests equal the last round's digests. The only record allowed to differ is\n an attended one with `amended_after_final_round: true`, a `bypass` override with a typed\n reason, and verdict `not_checked` (see \"Drafting mode\").\n- `rounds[].confirmation` is `true` only for a confirmation pass. `risk_tier` is `low` or\n `high`, `risk_reasons` lists why, and `round_limit` is the ordinary-run limit that applied\n (`2` or `3`; report mode runs once).\n- Each coverage owner carries `owner_kind`, which is `child` or `owner_run`. A child owner\n carries `child`, `obligation`, `child_ac`, and `source`. An owner-run owner carries\n `child: null`, `owner` (its `non_child_owners` id), `who`, `step_ref`, `records_result_in`,\n `obligation`, and `source: declared`.\n- `owner_run_acs` lists the id of every epic AC with at least one owner-run owner. It is empty\n when there are none. Owner-run coverage is always visible, never silently passed.\n- `council_id` is `null` when no council ran. `council_outcome` is one of `not_run`,\n `concluded`, `failed`, `timed_out`, `still_running`, or `declined_by_operator`. These two fields describe the first\n council. When a second council runs, `councils` lists **every** council in order, each with\n `council_id`, `trigger` (`uncertain`, `disputed`, or `high_risk`), and `outcome`. An override\n that accepts an unfinished council carries `council_id` in place of `finding_id`.\n An initial council declined through `council_preapproval` stays in `council_outcome` as\n `declined_by_operator` with `council_id: null`, and is retained there when a later council\n also needs an entry in `councils`. The override that accepts it carries\n `council: \"declined_by_operator\"` in place of `finding_id` or `council_id`.\n- `council_preapproval` is present only when the caller supplied it. It persists its `value`\n and `source`, and the record omits the field otherwise.\n- Unknown fields, such as a historical `council_note`, are outside this schema. Readers ignore\n them. They never authorize clearance.\n- A `not_checked` record must **never** carry a coverage map that looks complete. Leave the map\n empty, or mark every entry as not assessed.\n- Keep this schema in this file. Do not copy it into the ticket-authoring posture block.\n\n## The record: epic-coherence.md\n\nWrite `epic-coherence.md` into the same directory as a short human summary. Use **no tables**.\nIn this order:\n\n1. The **verdict**, as the very first line of the file (after the title heading). Nothing else comes\n before it.\n2. **Independent: yes** or **Independent: no**, prominently, on the line directly after the\n verdict, then the **next action**.\n3. The round count, the risk tier, and the round limit. Say which round, if any, was a\n confirmation pass.\n4. Directly after the round count, one line: \"Owner-run coverage: N of M epic ACs depend on\n steps a person runs outside the Epic Implementer.\"\n5. One **bullet per epic AC**: its id, the judgment basis (`verification` or `text only`), the\n status, the owning child or the absence of one, the obligation, and the cited child AC\n evidence. Mark derived ownership as derived. For an owner-run owner, say \"owner-run\" in\n words, with the person or role, the step reference, and where the result is recorded, for\n example: AC-4: covered, owner-run (Release owner, checklist step 5; result recorded in an\n epic comment).\n6. **Blocking findings**, each labelled `blocking` with its `certain` or `uncertain` label.\n Write each hand-off gap as a sentence naming the shared thing, the affected consumer, the\n missing lifecycle end, and the smallest amendment.\n7. **Advisory and input findings.**\n8. **Council state.**\n9. **Overrides**, each with its typed reason.\n\nState every meaning in words, never by color alone.\n\nIn both files:\n\n- Label a report-mode `blocked` or `not_checked` result as **diagnostic and not a clearance**.\n- In report mode, state that the result gates no drafting surface: each drafting surface runs\n its own drafting-mode check before creation.\n- Put `independent: false` at the top, directly after the verdict line, and offer an attended\n fresh-context rerun.\n\n## Writing the records safely\n\n- Render both files to temporary files first. Replace the real files from the temporary files\n where the host supports it. Both files must carry the same run identity (`checked_at` and\n all three final digests), and both describe the same final round.\n- Report a write failure explicitly. A write failure is an **incomplete review**.\n- When a newer attempt fails or is incomplete, **invalidate or remove** any older successful\n artifact in the output directory. Never leave an older `pass` beside a newer failed outcome.\n An incomplete newer attempt must not leave an older clearing record in place.\n\n## Return\n\nReport to the caller:\n\n- the verdict and whether the run was independent;\n- the next action;\n- the paths of `epic-coherence.json` and `epic-coherence.md`;\n- for drafting mode, the halt signal (`do not call create_ticket`) when one applies, and the\n amendments the caller must make before the next round.\n\nDo not describe a `blocked` or `not_checked` report-mode result as a clearance.\n",
|
|
8
|
+
"docs/install/preflight-and-readiness-procedure.md": "Initialize the idea-to-ticket run directory and classify the idea's readiness and scope.\n\n## Inputs\n\n- Idea: `{idea}`\n- Slug: `{slug}`\n- Run ID: `{run_id}`\n- Docs directory: `{docs_dir}`\n- Project standards: response from the immediately preceding `get_project_standards` step. If that step returned an error envelope or a 404, treat the project standards as unavailable and proceed; do not halt.\n\n## Callers\n\nTwo callers use this procedure. Everything below is the **recipe path** and applies unchanged when the `idea-to-ticket` recipe runs it.\n\n**Define-tickets path.** When `/define-tickets --auto` runs this procedure, the command names the artifact directory and a unique local run id in its invocation. Use that directory in place of the recipe run directory, and write the readiness artifact there. Do not write `run-manifest.json`, do not compute or look up any label, and do not query Jira. Classify readiness and scope exactly as below and report both axes to the command. Scope is recorded only; it does not choose the eventual ticket shape. A `too_vague_to_ticket` result is reported to the command, which halts before it explores the codebase.\n\n## Instructions\n\n1. Create the run directory:\n ```\n mkdir -p {docs_dir}/idea-to-ticket/{slug}-{run_id}\n ```\n Every artifact produced by this pipeline run lives under this run directory. No Jira mutation may occur in any later step until `run-manifest.json` has been written to this directory.\n\n2. Classify the idea on two independent axes:\n\n **Readiness** (one of):\n - `ready_to_draft` — the idea is concrete enough that a clear ticket draft can be produced.\n - `needs_clarification` — the idea is reasonable but missing key answers; clarifying questions must be raised in `open-questions.md` later.\n - `research_first` — drafting is blocked on external/codebase research; deep or narrow research must come first.\n - `too_vague_to_ticket` — the idea is not actionable yet; do not produce a ticket.\n\n **Scope** (one of):\n - `task` — a single Jira Task (default when ambiguous).\n - `spike` — a single Jira Spike. Resolve `spike` only on the objective\n condition that the idea has **no committed production-code deliverable** —\n the same trigger as exception E2 in `docs/bridge-ticket-authoring.md`. This\n is a deliverable that is absent, not a judgement about whether the work\n feels investigative. **Any uncertainty resolves to `task`.** This decision\n is made HERE and only here: the `scope` you write into the manifest is what\n selects the spike ticket format downstream, and `jira-ticket-writer` is\n explicitly forbidden from deciding it for itself.\n - `epic_candidate` — the idea decomposes into a Jira Epic plus multiple child tickets.\n\n3. Halt locally if readiness is `too_vague_to_ticket`. Write the manifest anyway (see step 4) so the local artifacts record the halt; then stop without continuing the rest of the pipeline. Do not attempt any Jira mutation.\n\n4. Write `run-manifest.json` to `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`. Required fields:\n - `idea` — the original `{idea}` text.\n - `slug` — `{slug}`.\n - `run_id` — `{run_id}`.\n - `run_dir` — `{docs_dir}/idea-to-ticket/{slug}-{run_id}/`.\n - `readiness` — one of the four readiness values above.\n - `scope` — one of the three scope values above.\n - `project_standards_available` — `true` if `get_project_standards` returned a usable result, `false` otherwise.\n - `idempotency_label` — `bapi-idea-to-ticket-{run_id}` (per-run label; lets downstream steps resume THIS run by label).\n - `stable_label` — `bapi-idea-hash-{idea_hash}` (stable across runs of the same idea; lets the duplicate-detection step catch a PRIOR run of the same idea by label, not just fuzzy text).\n - `created_at` — ISO 8601 timestamp.\n\n5. The manifest is the resumability artifact for the whole run. Do not include secrets or raw credentials. Keep the file under a few KB.\n\n## Return\n\nConfirm the run directory and `run-manifest.json` were created, and report the classified `readiness` and `scope`. If readiness is `too_vague_to_ticket`, also report that the pipeline must stop without Jira mutation.\n",
|
|
9
|
+
"docs/install/duplicate-and-context-scan-procedure.md": "Detect existing Jira tickets that duplicate or relate to this idea before any Jira mutation.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` (if produced).\n- Pipeline variable `allow_duplicate` controls override behavior (for this run, `allow_duplicate` = `{allow_duplicate}`). Treat the literal string `\"true\"` as override; any other value (including `\"false\"`, missing, or empty) is non-override.\n\n## Callers\n\nTwo callers use this procedure. The **recipe path** is everything under Instructions below and is unchanged. The **define-tickets path** is the separate branch at the end of that section; it is used only by `/define-tickets --auto`, and none of the recipe's label queries apply to it.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is part of the full-automation chain and is authorized to call `get_tickets` as directed below — performing an orchestrator-directed tool call is not \"re-orchestrating\".\n\n1. Build at least two Jira search queries from the manifest:\n - **Title/keyword query**: use the most salient nouns from `idea` and `slug` as title/text keywords. Prefer 2-4 concrete terms over long natural-language sentences. Run via `get_tickets`.\n - **Stable idea-hash query** (the reliable cross-run dedup): run `get_tickets` with its `labels` parameter set to `bapi-idea-hash-{idea_hash}`. This label is identical for every run of the same idea, so it catches a PRIOR run that already created a ticket for this idea — even one created days ago. A hit here is a strong `duplicate` signal.\n - **Idempotency-label query**: run `get_tickets` with its `labels` parameter set to `bapi-idea-to-ticket-{run_id}` (the tool builds the `labels in (...)` JQL for you — do not pass a raw JQL string). This per-run label only matches a partial run of THIS same run, so it supports resume behavior.\n\n2. For each returned ticket, capture: ticket key, summary, status, and a short reason it matched (which query, which keyword).\n\n3. Classify the overall verdict as one of:\n - `duplicate` — at least one returned ticket clearly describes the same work as `idea`.\n - `related` — returned tickets are adjacent or partial overlaps but not the same work.\n - `none_found` — no meaningful matches.\n - `unable_to_check` — the Jira search itself failed (network error, auth error, JQL rejection). Record the failure and pick this verdict.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json` with at minimum:\n - `verdict` — one of the four values above.\n - `matches` — array of `{ticket_key, summary, status, reason}` objects (may be empty).\n - `queries_used` — array of the actual JQL/search strings sent.\n - `allow_duplicate` — the resolved value of `{allow_duplicate}` for this run.\n\n5. Halt behavior:\n - If `verdict` is `duplicate` and `allow_duplicate` is not `\"true\"`, halt locally. Do not continue the pipeline. Tell the user that the duplicate halt is strict and that re-running with `--allow-duplicate` overrides it.\n - If `verdict` is `duplicate` and `allow_duplicate` is `\"true\"`, continue the pipeline but keep the duplicate evidence in the assessment file so downstream steps can reference it (e.g., to add a \"supersedes\" note to the draft).\n - For `related`, `none_found`, and `unable_to_check`, continue without halting.\n\n### Define-tickets path\n\nUsed only when `/define-tickets --auto` runs this procedure. The command names the artifact directory, a unique local run id, and the resolved `allow_duplicate` literal in its invocation. There is no run manifest and no label.\n\n1. Run at least two `get_tickets` searches using its `query` parameter: one keyword search built from the salient nouns of the task, and one search on the candidate ticket title. Never pass `labels`; the label queries above belong to the recipe path only.\n2. Capture each returned ticket's key, summary, status, and a short reason it matched. Classify the verdict as `duplicate`, `related`, `none_found`, or `unable_to_check`, using the same definitions as above.\n3. Write `duplicate-assessment.json` to the named artifact directory with `verdict`, `matches`, `queries_used`, and the resolved `allow_duplicate` value.\n4. Halt behavior for this path: a `duplicate` verdict halts the command unless `allow_duplicate` is the literal string `\"true\"`. That override applies only to a known `duplicate` verdict. An `unable_to_check` verdict is never overridden. The command halts as `stage_failed` and names the duplicate scan. Matches found here were not created by this run.\n\n## Return\n\nConfirm `duplicate-assessment.json` was written, report `verdict`, and report whether the run is halting or continuing.\n",
|
|
10
|
+
"docs/install/comp-analysis-procedure.md": "Perceive any attached/referenced design comp with your OWN vision and map it to the existing codebase BEFORE the `jira-ticket-writer` drafts. This is the pre-writer perception step: the orchestrating recipe agent (already a frontier vision model) opens the comp, classifies it against the shared fidelity taxonomy, researches the code, and writes a structured comp→codebase map the writer consumes. The writer stays text-only and never opens images — it only reads the map you produce here.\n\nThis step is gated and backend-safe. It runs the perception with the ORCHESTRATING agent's own vision — a local image via the Read tool, or an already-attached Jira comp fetched as raw bytes via the BAPI-562 binary-safe `attachment` download (operation `download`) into a worktree `file_path`. It does NOT call `describe_image()`, does NOT depend on `src/python/llms/vision.py`, and does NOT require the `CHEAP_MODEL` vision path or any vision-model upgrade or procurement. The heavy visual reasoning is done for free by the agent that already runs the recipe.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` (source of truth for `idea`, `scope`, `readiness`). Read whichever prior artifacts exist under `{docs_dir}/idea-to-ticket/{slug}-{run_id}/` (research pack, resolved uncertainties, goals-and-nfrs, standards checklist) and proceed without the ones that do not.\n- Output artifact (this step writes it): `{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json` — the structured comp→codebase map the drafting step reads.\n\n## Callers\n\nThe steps below are the **recipe path** and are unchanged for the `idea-to-ticket` recipe.\n\n**Define-tickets path.** When `/define-tickets --auto` runs this procedure, the command names an exploration-adjacent artifact directory. Read the exploration document and any materials the task references instead of run-manifest artifacts, and write `comp-analysis.json` in that named directory. Assume no run manifest and no label. Apply the same gate, classification, and mapping rules. If the analysis is degraded for any reason, write `applicable: false`, a degradation `reason`, and `warnings` that identify the material needing manual review. When no qualifying comp exists, write the clean not-applicable artifact with no degradation warning. Degraded analysis never blocks the command.\n\n## Instructions\n\n1. **Gate first — evaluate only text/material metadata, never open an image yet.** Proceed to any image work ONLY when BOTH conditions hold:\n - a design comp is **referenced or attached** (a local image path in the idea/materials, or an already-attached Jira comp with an `attachment_id`), AND\n - the requested work is **design/UI work** (a frontend/fullstack change with a visual surface).\n\n This gate is deliberately conservative. A **backend-only** request, a **no-comp** request, or a **non-design** request fails the gate.\n\n2. **Not-applicable branch (gate fails) — short-circuit immediately.** When either gate condition is not met, write `comp-analysis.json` with `applicable: false`, a short `reason`, and the `gate` evidence fields, then **return immediately** — do NOT open any image, do NOT download any attachment, do NOT inject any fidelity/comp/visual language, and add no measurable latency. A backend-only or no-comp authoring input must be byte-for-byte unaffected downstream. Concretely, the not-applicable artifact is:\n\n ```json\n { \"applicable\": false, \"reason\": \"backend-only work; no design comp referenced\", \"gate\": { \"comp_referenced\": false, \"design_ui_work\": false }, \"warnings\": [] }\n ```\n\n3. **Applicable branch — obtain and OPEN the comp with your own vision.** When the gate passes:\n - **Local image path** → open it directly with the Read tool.\n - **Already-attached Jira comp** → fetch it as raw bytes using the BAPI-562 binary-safe `attachment` capability with operation `download`, passing the comp's `attachment_id` and a worktree `file_path`; the download saves the PNG/JPEG bytes to that `file_path` inside the project root, then open the saved file with the Read tool.\n - You are the orchestrating vision model — reason over the actual pixels yourself. Do NOT call `describe_image()`, do NOT use `src/python/llms/vision.py` / `CHEAP_MODEL`, and do NOT delegate perception to the text-only `jira-ticket-writer` subagent.\n\n4. **Degraded paths are skip-clean and warn-not-halt.** If the comp is missing, unreadable, an unsupported format, external-only (an `http(s)` URL you cannot fetch into a worktree `file_path`), or turns out not to be a usable design comp, write a valid `comp-analysis.json` with `applicable: false`, a `reason`, and a populated `warnings` array describing what failed, then return. A missing, unreadable, or non-comp image NEVER blocks downstream drafting and NEVER requires further image work after the failure.\n\n5. **Classify the opened comp using the shared fidelity taxonomy (do not fork it).** Using your own vision, classify the comp as exactly one of these four classes — the same labels the downstream final plan reviewer uses (`src/python/llms/agents/planner_agent/final_plan_review_agent.py`, `_get_runtime_verification_instructions`), so authoring, planning, and implementation all agree:\n - `full comp`\n - `wireframe`\n - `annotated-screenshot-of-existing-UI`\n - `unknown`\n\n Record `fidelity_classification` with a `class` (one of the four), a `confidence` value, and a short `reasoning` string.\n\n6. **Apply class-appropriate mapping depth — no over-specification.** Match the per-class rules exactly:\n - **full comp** (confident) → map exact existing components, Jinja2 templates, SCSS/CSS tokens, and routes; strict/exact component + token depth is used ONLY here.\n - **wireframe** → map layout and structure only (regions, order, rough proportions, responsive behavior); defer color, type, spacing, and component polish to the repo design system, NOT to the wireframe.\n - **annotated-screenshot-of-existing-UI** → map ONLY the delta against the current UI; preserve everything outside the annotated region and do not reproduce the screenshot wholesale.\n - **unknown / low confidence** → fall back to the design-system floor rather than mapping pixels.\n\n **Hard rule:** strict/exact mapping depth is enabled ONLY for a confidently-classified full comp. Fail toward the design system, never toward reproducing an ambiguous image.\n\n7. **Research the codebase and map each region/element to concrete existing code.** Inspect the working tree (search, grep, file reads) for the existing UI implementation surfaces: reusable components, Jinja2 templates, plain CSS/SCSS tokens and design-system styles, routes, and reusable frontend patterns. Only cite files you actually inspected — do not invent file paths, component names, tokens, or routes. For each region/element of the comp, produce a `mappings[]` entry containing:\n - `region` — the comp region or element name.\n - `visual_description` — a short description of what it looks like.\n - `components` — mapped existing component file(s).\n - `templates` — mapped existing Jinja2 template(s).\n - `tokens` — style/token references (SCSS/CSS tokens or design-system styles).\n - `routes` — route reference(s) where the element lives or should wire.\n - `confidence` — confidence for this mapping.\n - `implementation_guidance` — concrete guidance (e.g. \"reuse component X\", \"extend template Y\", \"use token Z\", \"wire route R\").\n\n8. **Write the structured map to a stable schema.** Write `comp-analysis.json` at `{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json` with these fields:\n - `applicable` — boolean; `true` only after a comp was both detected AND successfully opened.\n - `reason` — short string explaining the applicability decision.\n - `gate` — the two-condition gate evidence (`comp_referenced`, `design_ui_work`).\n - `comp` — provenance of the opened comp (source kind, path or `attachment_id`, filename, MIME type when known).\n - `fidelity_classification` — `{ class, confidence, reasoning }`, present only when `applicable` is true.\n - `mappings` — array of the per-region entries defined in step 7 (present only when `applicable` is true).\n - `design_system_floor` — the design-system fallback guidance to use for wireframe/unknown/low-confidence regions.\n - `warnings` — array of degraded-path notes (may be empty).\n - `writer_guidance` — instructions for the downstream drafting step: when `applicable` is `true`, Requirements must cite the mapped components/templates/tokens/routes as concrete implementation guidance; when `applicable` is `false` or the map is missing, the writer must ignore this artifact and mention no comp analysis, design comp, or visual-fidelity language unless the original request independently requires it.\n\n9. **Never over-write.** Write the artifact exactly once at the path above. Downstream steps (`draft-and-critique.md`) read it; do not move it.\n\n## Return\n\nConfirm the path written (`{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json`), whether the analysis was `applicable`, the fidelity `class` when applicable, and any `warnings`.\n",
|
|
11
|
+
"docs/install/gather-and-attach-materials-procedure.md": "Post-create materials-completeness step. Gather the phase-eligible local materials a freshly-created ticket references and attach them via `attachment` (operations: `\"list\"`, `\"upload\"`, `\"delete\"`), while recording everything that is record-only. This is the POST-CREATE half of the upload-time materials-completeness pass (BAPI-423, phase-tagged since BAPI-643); the PRE-CREATE half — inventorying and writing the `## Materials & Access` section into the draft — already ran in the `jira-ticket-writer` agent.\n\n## Inputs\n\n- `{ticket_number}` — the real Jira key of the already-created ticket (e.g. `BAPI-423`). Attachment is a POST-CREATE step; never attempt to attach before the key exists.\n- `{draft_file_path}` — path to the draft markdown that carries the trailing `## Materials & Access` section.\n- `{auto_approve_external}` — the unattended-vs-interactive signal (named for consistency with `upload-and-track.md`). **Polarity is counter-intuitive: `\"true\"` means UNATTENDED, which is the MORE restrictive mode here** — skip all prompts AND keep external/auth-gated materials record-only (never auto-attach them). It does NOT grant permission to attach external materials. Any other value (including `\"false\"`, missing, or empty) means an interactive invocation that MAY prompt for external/auth-gated materials. Invocations from `write-ticket`, `idea-to-pr`, and `/define-tickets --auto` are always unattended (`\"true\"`) for this step. Attended `/define-tickets` is an interactive caller and passes `\"false\"`.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is authorized to call `attachment` (operations: `list`, `upload`, `delete`) and `update_ticket_description` as directed below.\n\n1. **Read the record.** Read `{draft_file_path}` and parse its trailing `## Materials & Access` section. Collect the inventoried items grouped under *Planning Assets*, *Downloadable Assets*, *Planning & Downloadable Assets* (each phase-eligible; present only when a material of that phase exists), *External/Auth-Gated Links*, and *Binary/Image Materials (Record-Only)*. If there is no `## Materials & Access` section, there is nothing to gather — return a no-op success.\n\n2. **Canonical phase-token mapping.** Every phase-eligible bullet maps to exactly one filename token:\n - *Planning Assets* → `planasset`\n - *Downloadable Assets* → `implasset`\n - *Planning & Downloadable Assets* → `bothasset`\n\n *External/Auth-Gated Links* and *Binary/Image Materials (Record-Only)* are **never** phase-tagged and are behaviorally unchanged by this step — they stay record-only, exactly as before.\n\n3. **Source classification (scheme-based, no network probe).** Honor the classification already recorded in the draft:\n - **Planning Assets / Downloadable Assets / Planning & Downloadable Assets** — a local filesystem path that is **NOT tracked in version control** is a **low-risk** material eligible for auto-attach — proceed to step 5. The pre-create inventory already excluded version-controlled files (source code and in-repo docs are already available in the repository and are never attached — they are cited inline as *Relevant code*). As a safety net, this step must **never attach a file that is available in version control**: if any phase-tagged item is a code file or otherwise clearly version-controlled, skip it and treat it as record-only. A bullet under *Planning & Downloadable Assets* recorded via an existing `attachment_id` (not a local file — e.g. a design comp already attached on this or another ticket) is **not** re-uploaded by this step; leave it recorded with its `attachment_id`/path so a later implementation agent can download it into its worktree via the Jira attachment download capability.\n - **External/Auth-Gated Links** (every `http(s)` URI, even if explicitly linked) are **record-only** on unattended paths. If `{auto_approve_external}` is `\"true\"` (or the invocation is from `write-ticket` / `idea-to-pr`), leave them record-only and never auto-attach. (Mind the polarity: `auto_approve_external = \"true\"` means we are in unattended mode, so external materials must stay record-only — `\"true\"` is NOT permission to attach them.) Only an explicitly interactive invocation (`auto_approve_external` is any non-`\"true\"` value) may prompt the user to confirm before attaching.\n - **Binary/Image Materials (Record-Only)** — ordinary/unrelated binaries (arbitrary screenshots, PDFs, ZIPs, and other binaries not design-relevant) stay **record-only** in this step; never attempt to upload them.\n\n4. **Deduplicate and detect stale phase reclassification.** Call the `attachment` MCP tool with `operation` set to `\"list\"` and `ticket_number` set to `{ticket_number}` BEFORE uploading anything, so a resumed or re-run invocation does not re-attach a material that is already present.\n - Compute each candidate's deterministic filename first (step 6), then compare the listed attachments by **`{ticket_number}/{hash}`** (the ticket prefix plus the 8-hex-char hash segment) against the phase-tagged filenames already on the ticket, using the exact grammar `{ticket}-{planasset|implasset|bothasset}-{hash}{ext}` — this comparison spans all three phase tokens for the same source.\n - If a listed attachment shares the same `{ticket_number}/{hash}` as a candidate but carries a **different phase token**, treat it as a **stale classification for the same source** — the material was reclassified to a different phase since it was last gathered.\n - If a listed attachment's filename **exactly matches** the candidate's newly computed filename (same ticket, same phase token, same hash, same extension), it is already attached — skip re-uploading it.\n - Legacy `{ticket_number}-material-{hash}{ext}` attachments (pre-BAPI-643) are a distinct, valid, untouched naming form — never treat them as stale phase-tagged attachments, and never delete or supersede them here.\n\n5. **Supersede a stale phase-tagged attachment before uploading the reclassified filename.** When step 4 identifies a stale same-source attachment under a different phase token:\n - Call the `attachment` MCP tool with `operation` set to `\"delete\"`, `ticket_number` set to `{ticket_number}`, and `file_name` set to the stale attachment's exact filename. This is a genuine removal (not `replace_existing`, which only matches an upload's *own* filename) — the stale filename differs from the new filename whenever the phase token changed, so `replace_existing` alone cannot supersede it.\n - **If the delete call fails** (or reports `deleted: false` for a reason other than \"already absent\" — e.g. a transient Jira error), **skip the replacement upload for that material entirely** this run, preserving the one-active-attachment invariant rather than leaving two phase-tagged copies of the same source live at once. Record the failure (see step 9) and continue with the next material; never halt the run.\n - **If the delete succeeds** (or the stale attachment was already absent), proceed to upload the newly classified filename in step 7.\n\n6. **Gather and size-tier each phase-eligible local material; compute deterministic filenames.**\n - For each phase-eligible local **text** material:\n - Read the local file from disk.\n - If the content exceeds **200,000 characters**, SKIP the upload and RECORD it (note the path and that it was skipped for size) — do not attach it.\n - If the content is **<= 200,000 characters**, upload it RAW via `attachment` (operation: `\"upload\"`). Do NOT summarize locally: the backend already summarizes attached text at plan time, so the size tiers are backend behavior this step defers to. The agent performs NO local summarization.\n - **Extension rule by phase**: a **planning-only** (`planasset`) text material normalizes to `.md` regardless of its original extension — the planner only needs prose content, not the original format. A **downloadable or both-phase** (`implasset`/`bothasset`) text material instead **preserves its lowercased original source extension** (e.g. `.json`, `.csv`, `.py`, `.yml`/`.yaml`) — the implementing agent needs the file back in its original, directly usable format. There is no blanket \"all text becomes `.md`\" rule; the extension is phase-dependent.\n - Use a deterministic, sanitized filename of the form `{ticket_number}-{phase-token}-{hash}{ext}` (using the `{ticket_number}` input from the Inputs section and the phase token from step 2), where `{hash}` is the first 8 lowercase hex characters of the SHA-256 digest of the sanitized absolute source path, and `{ext}` follows the extension rule above.\n - For each eligible local design/UI comp image (or other phase-tagged image, extension mapping to an allowlisted MIME type — `image/png`, `image/jpeg`, `image/webp`, `image/gif`), eligible for the allowlist-guarded binary upload path (distinct from the text upload path above):\n - Use a deterministic filename of the form `{ticket_number}-{phase-token}-{hash}{ext}`, where `{hash}` is computed the same way (first 8 lowercase hex characters of the SHA-256 digest of the sanitized absolute source path) and `{ext}` is the lowercased allowlisted source extension (`.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`) — the existing image-extension behavior is unchanged by phase.\n - Pin the hash algorithm exactly (SHA-256, first 8 lowercase hex chars, of the sanitized absolute path) — do NOT substitute another hash — so the same source always maps to the same `{ticket}/{hash}` identity and the dedup/stale-detection in step 4 works across separate sessions, re-runs, and phase reclassification. Compute filenames before the step 4 dedup/stale comparison is applied. Keep the sanitized source provenance inside the text attachment body (not applicable to binary comp uploads), not only in the filename.\n\n7. **`attachment` upload parameter discipline (Zod).**\n - For text materials, call `attachment` with `operation: \"upload\"`, `ticket_number`, the deterministic phase-tagged attachment filename, and the text `content`.\n - For design comp image uploads, call `attachment` with `operation: \"upload\"`, `ticket_number`, `file_path` (the local source path), and `file_name` set to the deterministic phase-tagged comp filename from step 6 — pass `file_path` rather than reading and UTF-8-encoding the bytes yourself, so `resolveUploadAttachment()` performs binary detection, the MIME allowlist check, and base64 encoding. Never UTF-8-encode image bytes locally.\n - In both cases, OMIT the optional parameters `link_type` and `replace_existing` entirely when they are unused — do NOT pass `null` or empty strings for them. The Zod schemas reject `null`/empty values, so an unused optional parameter must be omitted rather than nulled.\n\n8. **Persist destination provenance after successful uploads (one batched update).** For every material successfully uploaded in this run, record a stable, sanitized mapping from its generated Jira filename to the bullet's original repository-relative destination path (as recorded in the draft's `## Materials & Access` section). Write these mappings as bulleted lines under a dedicated trailing `### Asset Destinations` subsection of `## Materials & Access`, one line per mapping, in the exact form:\n ```markdown\n ### Asset Destinations\n\n - `{jira-filename}` → `{original/repo/relative/destination.ext}`\n ```\n This is the canonical, machine-parsed contract consumed by `extract_asset_destination_paths()` (`api/agent_flows/code_writer/library/ticket_asset_lib.py`) — do not vary the arrow character (`→`), the backtick wrapping, or the bullet form. Append new mappings to this subsection (creating it if absent) rather than replacing prior mappings from earlier gather runs. Apply **all** of this run's successful-upload provenance updates in **one single batched call** to `update_ticket_description` — never one Jira description update per asset. If the provenance update call fails, **retain the original bullet unchanged** in the ticket description and record a non-blocking warning that the destination mapping could not be persisted; this does not fail the run, and the filename token itself is still enough for plan generation to classify the asset even without a fully actionable destination.\n\n9. **Redact secrets everywhere.** Before writing any URL or access note ANYWHERE — the Jira `## Materials & Access` record, any warning or final-report output, and any local intermediate file — sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. Mirror the backend `_redact_forge_fields()` / `_sanitize_jira_error_message()` patterns. A location/access note must never expose a plaintext secret.\n\n10. **Warn, never halt (error handling).** This step must NEVER halt, prompt-to-fail, or fail the overarching command because a material could not be gathered, attached, or superseded. Follow the warn-not-halt convention:\n - If an `attachment` upload call fails (or a file disappeared between inventory and upload), warn gracefully and continue with the next material.\n - If a stale-attachment `delete` call fails, skip only that material's replacement upload (step 5) and continue with the rest — never halt the run.\n - This includes design comp image uploads: an unsupported/disallowed MIME type, an oversize image (`> 10 MB`), a missing local file, a malformed upload payload, or a Jira upload failure must all be warned and skipped, never halting the run.\n - On such a post-create attach/delete failure, call `update_ticket_description` to record the failure in the issue's `## Materials & Access` record (the material became unavailable, or its stale copy could not be superseded, only after the issue existed). `update_ticket_description` is an existing MCP tool, not a backend change.\n - Everything knowable PRE-CREATE was already written into the description at create time, so `update_ticket_description` is reserved for these rarer post-create attach/delete/provenance failures. This complements the existing `partial_success` recording convention in `upload-and-track.md`.\n - Apply the step 9 redaction to every warning and recorded note.\n\n## Return\n\nConfirm the outcome, reporting each category **by its user-facing phase name and original path** (not by the internal filename token, which is implementation metadata): which Planning Assets, Downloadable Assets, and Planning & Downloadable Assets were attached (noting their deterministic Jira filenames for cross-reference only), which stale phase-tagged attachments were superseded (and which supersession attempts failed and were skipped), which materials were skipped/recorded as record-only (over-size text, external/auth-gated links, ordinary/unrelated binaries, or non-local design references), whether the destination-provenance batched update succeeded, any attach/delete/provenance failures recorded via `update_ticket_description`, and that no failure halted the run.\n",
|
|
12
|
+
"docs/install/epic-implementer-install.md": "# Epic Implementer install guide (for agents)\n\nThis guide is for a coding agent (Claude Code, Cursor, or similar) working in a\ncustomer repository. Your human said \"set up the Epic Implementer\" and does not\nknow how. Follow the steps below in order. Each step says why it matters, the\nexact command, how to check it worked, what to do if it fails, and whether you\nmust ask the human first.\n\nBridge copies this file into every installed project at\n`docs/install/epic-implementer-install.md`. The same text ships in the npm\npackage as `docs/EPIC-IMPLEMENTER-INSTALL.md`. For the full reference, read\n`docs/EPIC-IMPLEMENTER.md` inside the `@bridge_gpt/mcp-server` package.\n\nIn this guide, `<launcher>` means `npx -y @bridge_gpt/mcp-server`. Always keep\nthe `-y`. Without it, `npx` stops to ask whether to install the package.\n\n---\n\n## 0. Rules for you, the agent\n\n1. **Read first, write later.** Run every read-only check in a step before any\n step that writes.\n2. **Ask before any write or push.** Ask the human before you install a GitHub\n App, open a pull request, create a secret, change an admin setting, or push a\n branch. Say what will change and why.\n3. **Replace every placeholder before you run a printed remedy.** The tools now\n print complete commands, with the `npx -y @bridge_gpt/mcp-server` prefix and\n every required flag. Some carry a placeholder that only a real value can fill:\n `<@org/team|@user>` (the one reviewer of the workflow file; Bridge cannot infer\n it) and `<check-name>` (the exact name of a CI check this repository requires;\n repeat `--required-check` for more than one). Older tool versions printed\n shorthand; the table in [§12](#12-troubleshooting-and-known-wrong-remedies)\n maps it to the full command.\n4. **Stop at human-only access.** Some steps need a browser, a GitHub admin, or\n the Bridge admin panel. Stop, say exactly what the human must do, and wait.\n5. **Interactive commands go to the human.** `install`, `connect-github`, and\n both `install conductor` forms ask questions on a terminal. A shell you run\n has no terminal (no TTY), so the answer is always \"no\":\n - `install conductor` prints `Aborted` and **exits `0`**. The exit code does\n not tell you it failed.\n - `install conductor --merge-workflow` prints `consent_declined` and exits `1`.\n\n Ask the human to run these commands in their own terminal, from the\n repository root. Then read what they paste back.\n6. **Never write a secret into the repository.** No token, API key, or merge\n credential goes into a tracked file, a command argument you print, or a\n commit.\n7. **Keep notes.** Record which step you finished. Step 5 asks the human to\n restart the MCP client, which may end this session.\n\n## 1. What you are setting up\n\nThe Epic Implementer drives a multi-ticket epic from an approved plan to merged\npull requests. It cuts an `epic/<EPIC-KEY>` branch, hands each child ticket to\nan agent on this machine, waits for CI and code review on each child PR, and\nmerges each child PR into the epic branch. A person merges the final\nintegration PR into the default branch.\n\nBy default a run with two or more tickets is **unattended**: it merges child\nPRs on its own. `--attended` is the opt-out: the run then has no permission to\nmerge by itself.\n\nBoth kinds of run still need CI and code review to actually run on child PRs\ninto `epic/<EPIC-KEY>`, or every ticket waits forever. Unattended runs also need\nthe server to confirm the setup (steps 6 to 9). Attended runs skip that\nconfirmation, but have their own launch path ([§11c](#11c-attended-runs)).\n\n## 2. Preflight checklist (read-only)\n\n**Why:** the executor that runs the jobs lives on this machine. It refuses to\nstart without certain tools, and several later steps need `gh` and push rights.\n\n**Ask first?** No. Everything here only reads.\n\n| Check | Command | Pass when |\n|---|---|---|\n| Node.js 18 or newer | `node --version` | `v18` or later. The optional git hooks want an even LTS line, `v20` or later. |\n| Operating system | `uname -s` | `Darwin` or `Linux`. The executor does not run on Windows. |\n| git | `git --version` | Prints a version. |\n| GitHub CLI, logged in | `gh auth status` | Logged in to github.com. |\n| Claude Code CLI | `claude --version` | Prints a version. The human must also be logged in to `claude`. |\n| Worktrunk | `wt --version` | Prints a version. The executor refuses to start without it. |\n| Push rights | `gh repo view --json viewerPermission -q .viewerPermission` | `WRITE`, `MAINTAIN`, or `ADMIN`. Setup pushes an `epic/<EPIC-KEY>` branch with the human's own git. |\n| No retired variables | The command below | Prints nothing. |\n\nRetired-variable check (prints names only, never values):\n\n```bash\nenv | grep -E '^(BAPI_CONDUCTOR_|CONDUCTOR_(NODE_PATH|DEAD_MAN_ONLY|DEADMAN_CHANNEL_TYPE|DEADMAN_DESTINATION_REF)=)' | cut -d= -f1\n```\n\nThese are old Epic Implementer variable names. If any is set, every\n`<launcher>` command exits `1` and lists each old name with its new one. Show\nthe names to the human and ask them to rename or remove them in their shell\nprofile, then open a fresh shell.\n\nIf a tool is missing, stop and tell the human which one. Do not install tools\nyourself without asking.\n\nThen run the three Bridge diagnostics. All three only read:\n\n```bash\nnpx -y @bridge_gpt/mcp-server doctor\nnpx -y @bridge_gpt/mcp-server epic-implementer doctor\nnpx -y @bridge_gpt/mcp-server epic-implementer readiness --json\n```\n\n- `doctor` checks the base install: repository identity, credentials, server\n connection, and whether the repository is indexed.\n- `epic-implementer doctor` checks this machine: the local ledger, git hooks,\n and the merge credential. It briefly starts a headless `claude` probe to check\n that permission denials are enforced.\n- `epic-implementer readiness --json` combines all of these with the server's\n view. It is the one report you will re-run after every step. It always exits\n `0`, even when things fail, so read the JSON.\n\n**How to read `readiness --json`:** look at the `checks` array. Each entry has an\n`id` (for example `server.github-credentials`), a `status`, a `label`, and\nsometimes a `detail` and a `remediation`.\n\n- `pass` — fine.\n- `warn` — works but degraded. Note it.\n- `fail` — not met, **or could not be checked**. Read the `detail` to tell which.\n- `skip` — does not apply here.\n\nRows whose `id` starts with `plane.` only matter when the human runs their own\nBridge API server. Against the hosted service you can ignore them. The\n`server.executor-readiness` row may fail before the first run. That is fine:\n`drive-epic` starts an executor for each run by itself.\n\nMap each failing row to a step below. Do not run the `remediation` text as a\ncommand; see rule 3.\n\n## 3. Base install (only if Bridge is not installed yet)\n\n**Why:** every Epic Implementer command needs the repository's Bridge identity\nand API key.\n\n**Check first:** if `.bridge/config` exists and `doctor` reports the credential\nand server connection as fine, skip this step.\n\n**Ask first?** Yes. It is interactive and writes files.\n\nAsk the human to run this in their own terminal, from the repository root:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\nIt asks for an email (new account) or a key or invite (existing account). It\nwrites `.bridge/config`, the editor's MCP config, and the slash commands. It\nstores the API key outside the repository.\n\n**How credentials are found.** Every command looks for the API key in this\norder: the `BAPI_API_KEY` environment variable, then the file\n`~/.config/bridge/credentials.json` under the key `bapi:<repo_name>`. A key\nthat lives only in the editor's MCP config is **not** visible to a shell you\nstart, so rely on the credentials file.\n\n**The `repo_name` rule.** `repo_name` must match byte for byte everywhere:\n`.bridge/config`, the `BAPI_REPO_NAME` in the MCP config, the `bapi:<repo_name>`\nkey, and any `--repo` flag. Never derive it from the folder or GitHub name. A\nhyphen where Bridge has an underscore gives a **403** that looks like a\npermissions problem. Read it:\n\n```bash\ngrep '^repo_name' .bridge/config\n```\n\n**Check it worked:** re-run `npx -y @bridge_gpt/mcp-server doctor`. The identity,\ncredential, and connectivity rows should pass.\n\n**If it fails:** a 403 almost always means a `repo_name` mismatch. A 401 means\nthe key was rejected or rotated. Ask the human for a fresh key.\n\n## 4. Connect GitHub\n\n**Why:** the server watches CI and review through the Bridge GitHub App. Without\nApp credentials, `drive-epic` refuses to start any run (`github_credentials`).\n\n**Check first:** in `readiness --json`, find `server.github-credentials`. If it\npasses, skip to the permission check below.\n\n**Ask first?** Yes. It opens a browser and installs a GitHub App.\n\nAsk the human to run, in their own terminal:\n\n```bash\nnpx -y @bridge_gpt/mcp-server connect-github --repo <repo_name>\n```\n\nIt opens the GitHub App install page. The human installs the App there, comes\nback to the terminal, and confirms the repository. No GitHub token is ever\ntyped. If someone else administers the GitHub organization, use\n`connect-github --handoff --repo <repo_name>` instead. It prints a link to send\nto that admin. After they finish, run `connect-github --resume --repo <repo_name>`.\n\n**Permission check.** Find `server.github-actions-permission` in\n`readiness --json`. It should read `actions: write`. Without it, the Epic\nImplementer cannot re-run a flaky workflow (the rerun fails with a 403). This is\na warning, not a blocker. If it shows a lower level, ask the human to check the\nBridge GitHub App's installation in their GitHub settings and accept any\npending permission request. If `detail` says the permission could not be\nchecked, re-run readiness after credentials resolve.\n\n**Check it worked:** `server.github-credentials` passes.\n\n## 5. Bootstrap the repository: `install conductor`\n\n**Why:** this writes the repository's default Epic Implementer settings on the\nserver: the required CI check each ticket must pass, merge approval, and the\nreview policy. `drive-epic` refuses a repository without them\n(`supervisor_setup`, `supervisor_config`). With extra flags it also writes the\n`claude-review` workflow that step 6 needs, and it adds the Epic Implementer MCP\ntools to your editor config.\n\nThe subcommand is still named `install conductor` (the Epic Implementer's old\nname). `install-bridge conductor` is the same command.\n\n**Ask first?** Yes. It asks several yes/no questions and writes to the server\nand to local files. It needs a real terminal.\n\n### 5a. Choose the required check\n\n`--required-check` is **required**. It must be the exact name of a CI check run\nthat GitHub reports on child PRs into `epic/<EPIC-KEY>`. A name that never\nreports leaves every ticket waiting forever.\n\n- **Unattended runs (the default):** the name must be a job `name:` written\n literally in `.github/workflows/epic-implementer-ci.yml` or\n `.github/workflows/claude-review.yml` on the default branch. At approval the\n server checks each required check against those two files. Any other name,\n including a real check from another workflow, is refused as\n `admission_evidence_unverifiable`. Use **`epic-implementer-ci / gate`**, the\n gate job in the step 6 template.\n- **Attended runs only:** any real check name works, as long as that check runs\n on pull requests into `epic/**`. List the names on a recent pull request:\n\n ```bash\n gh pr view <pr-number> --json statusCheckRollup \\\n | jq -r '.statusCheckRollup[] | .name // .context'\n ```\n\n**Never use `claude-review` as a required check.** It is the review signal, not\na test run. The review gate reads it separately.\n\n### 5b. Choose the review workflow values\n\n`.github/workflows/claude-review.yml` produces the review verdict the review\ngate waits for. For unattended runs it must also emit two markers (`run-id:`\nand `Reviewed-SHA:`). This command writes a correct file when you pass **all\nthree** of these flags. If one is missing, phase 3 prints a `skipped — …` line\nand writes nothing.\n\n| Flag | Value |\n|---|---|\n| `--allowed-bot` | The GitHub App bot whose PRs get reviewed. Child PRs are opened by the Bridge GitHub App. For hosted Bridge, use `bridge-gpt-ai-tools-for-sfcc[bot]`. A self-hosted Bridge uses its own App's `<app-slug>[bot]`. |\n| `--secret-name` | The **name** (not the value) of a GitHub Actions secret that holds a Claude Code OAuth token, for example `CLAUDE_CODE_OAUTH_TOKEN`. Upper-case letters, digits, and underscores only. |\n| `--branches` | The pull-request base branches to review: the default branch plus `epic/**`, for example `main,epic/**`. Child PRs target `epic/<EPIC-KEY>`, so without `epic/**` they are never reviewed. |\n\nThe review policy defaults to `verdict_protocol`, which is the one that uses\nthis workflow. You do not need to pass `--review-policy`. Passing it explicitly\nalso stores it as the repository default.\n\nIf the repository already has a `claude-review.yml`, the command asks before\noverwriting it. An older or hand-written file may lack the markers. Readiness\ntells you which marker is missing.\n\n### 5c. Run it\n\nFind the default branch:\n\n```bash\ngh repo view --json defaultBranchRef -q .defaultBranchRef.name\n```\n\nFirst ask the human to preview with `--dry-run`. The preview still asks one\nquestion (whether to use the shown Bridge API URL) because it reads from the\nserver. It writes nothing:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install conductor \\\n --required-check \"epic-implementer-ci / gate\" \\\n --allowed-bot \"bridge-gpt-ai-tools-for-sfcc[bot]\" \\\n --secret-name CLAUDE_CODE_OAUTH_TOKEN \\\n --branches \"main,epic/**\" \\\n --dry-run\n```\n\nReplace `main` with the real default branch and adjust the other values. Then\nask the human to run the same command **without** `--dry-run`, in their own\nterminal, from the repository root. It runs eight phases:\n\n1. A read-only doctor. It stops before any write if something fatal is missing.\n2. A preview of the server settings, then \"Write the safe project-default\n supervisor posture now?\". If the change would remove an existing\n requirement, it asks a second, separate question.\n3. The `claude-review` workflow (only with the three flags from 5b).\n4. **Tool visibility:** adds `epic_implementer` to `BRIDGE_MCP_PROFILE` in the\n Bridge entry of `.mcp.json`, `.vscode/mcp.json`, and `.cursor/mcp.json`. It\n only edits files that already have a Bridge entry. It never creates a file\n and never touches Codex or Copilot config.\n5. Local observability: the optional local git hooks. Safe to decline.\n6. A plan check, only with `--epic-key` and `--plan-file`. Skip it for now.\n7. An executor service, only with `--executor-id`. Skip it: `drive-epic` starts\n one for each run.\n8. A capability report and a final doctor.\n\nThe last line is `complete` or `complete-with-manual-steps`. Before the first\nrun, `complete-with-manual-steps` is normal, because no executor has been seen\nyet. Read the \"Manual actions\" list it prints.\n\n**After it finishes:**\n\n- The workflow file (phase 3) was only written locally. It lands in step 6.\n- **Restart the MCP client** (the editor or `claude` session). The server reads\n its tool groups at startup, so `get_epic_snapshot` and `explain_run` appear\n only after a restart. Save your notes first. After the restart, confirm those\n two tools are listed. If they are not, check that the Bridge entry's\n `BRIDGE_MCP_PROFILE` contains `epic_implementer`, for example\n `core,epic_implementer`.\n\n**Check it worked:** in `readiness --json`, `server.supervisor-setup`,\n`server.supervisor-config`, and `server.supervisor-required-checks` pass.\n\n**If it fails:** if the output says `Aborted` right after the URL question, it\nhad no terminal or the human answered no. Ask them to run it again in their own\nterminal. If phase 1 lists a fatal row, fix that row first (usually step 3 or 4).\n\n## 6. The two workflows, on the default branch\n\n**Why:** child PRs need CI and review that really run. For unattended runs, the\nserver also reads two files from the **default branch**, at fixed paths, before\nit creates or approves a run. If either is wrong, it refuses with\n`repository_readiness_unconfirmed`. **No install command fixes this.** The files\nmust be merged into the default branch.\n\n| File | Requirement |\n|---|---|\n| `.github/workflows/claude-review.yml` | Must emit the `run-id:` and `Reviewed-SHA:` markers. Written by step 5 with the three flags from 5b. |\n| `.github/workflows/epic-implementer-ci.yml` | Must exist and parse as a workflow. Nothing in the package writes it. Use the template below. |\n\nA different file name does not count, even if it runs the same tests.\n\n**Ask first?** Yes. You will create a branch and open a pull request.\n\n### 6a. The `epic-implementer-ci.yml` template\n\nThis is a minimal, generic starting point. It runs the repository's tests on the\nEpic Implementer's pull requests and reports one check,\n`epic-implementer-ci / gate`. That name is the required check from step 5.\n`gh pr view --json statusCheckRollup` shows it as `epic-implementer-ci / gate`.\nThe GitHub web page may show it as `epic-implementer-ci / epic-implementer-ci / gate`\n(workflow name, then job name). The required check is still\n`epic-implementer-ci / gate`.\n\nThe Epic Implementer names each child branch `feature/<TICKET-KEY>` and the\nepic branch `epic/<EPIC-KEY>`. Child PRs go from `feature/…` into `epic/…`. The\nfinal integration PR goes from `epic/…` into the default branch. The template\ntests every same-repository PR from a `feature/` or `epic/` branch. To test only\nthe Epic Implementer's tickets, you may narrow `feature/` to\n`feature/<PREFIX>-`, where `<PREFIX>` is the ticket prefix of the **child**\ntickets. Get that wrong and every child PR counts as out of scope, so the gate\npasses **without running any tests**.\n\n```yaml\nname: epic-implementer-ci\n\non:\n pull_request:\n # The default branch (change main if yours differs) and the epic branches\n # that child pull requests target.\n branches: [main, \"epic/**\"]\n\npermissions:\n contents: read\n\n# A new push cancels the run for the older head of the same pull request.\nconcurrency:\n group: epic-implementer-ci-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\njobs:\n tests:\n name: tests\n # Same-repository feature/ and epic/ branches only.\n if: >-\n ${{ github.event.pull_request.head.repo.full_name == github.repository &&\n (startsWith(github.event.pull_request.head.ref, 'feature/') ||\n startsWith(github.event.pull_request.head.ref, 'epic/')) }}\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n # Test the exact head commit of the pull request.\n ref: ${{ github.event.pull_request.head.sha }}\n persist-credentials: false\n # Add the repository's real setup steps here (language runtime,\n # dependency install), then its real test command.\n - name: Run tests\n run: |\n echo \"Replace this step with the repository's test command.\"\n exit 1\n\n gate:\n # This name IS the check-run name the done gate waits for. Keep it literal.\n name: epic-implementer-ci / gate\n needs: [tests]\n if: ${{ always() }}\n runs-on: ubuntu-latest\n steps:\n - name: Aggregate the result\n env:\n # Keep this identical to the tests job's condition.\n IN_SCOPE: ${{ github.event.pull_request.head.repo.full_name == github.repository && (startsWith(github.event.pull_request.head.ref, 'feature/') || startsWith(github.event.pull_request.head.ref, 'epic/')) }}\n TESTS_RESULT: ${{ needs.tests.result }}\n run: |\n if [ \"$IN_SCOPE\" != \"true\" ]; then\n echo \"Out-of-scope pull request: the gate passes without running tests.\"\n exit 0\n fi\n if [ \"$TESTS_RESULT\" = \"success\" ]; then\n echo \"Tests passed.\"\n exit 0\n fi\n echo \"Tests concluded '$TESTS_RESULT', so the gate fails.\"\n exit 1\n```\n\nFill in the `tests` job with the repository's real test command. Find it in the\nexisting CI workflows, `package.json` scripts, a `Makefile`, or the README. Ask\nthe human if it is unclear. Do not leave the placeholder step: it fails on\npurpose.\n\n**Things to keep**, in this file and in `claude-review.yml`:\n\n- `if: ${{ always() }}` on the gate. Without it, a failed or skipped test job\n would leave the gate check missing instead of red.\n- The gate is green only when the tests job concluded `success`. A skipped or\n cancelled test job is never treated as a pass.\n- `persist-credentials: false` and the checkout of the pull request's head SHA.\n- **Static job names.** For unattended runs the server must read every job's\n check name straight from both files. If either file has any of the following,\n the server cannot read the names, and approval refuses every required check:\n - a `strategy.matrix` on any job;\n - a job-level `uses:` (a reusable workflow call). A `uses:` inside a step is\n fine;\n - a `${{ … }}` expression in any job id or job `name:`.\n\n Put matrix or reusable-workflow jobs in a different workflow file if you need\n them.\n\n### 6b. The `migration-guard` job (only for Alembic repositories)\n\nCheck whether the repository has `alembic.ini` at its **root** on the default\nbranch:\n\n```bash\ngit fetch origin\ngit ls-tree --name-only origin/<default-branch> alembic.ini\n```\n\nIf that prints nothing, skip this part. If it prints `alembic.ini`, the server\nalso requires a job whose id is exactly `migration-guard`, with a step that runs\nexactly `python scripts/check_alembic_migration_graph.py`. The repository must\ncontain that script. It should fail when the Alembic revision graph has more\nthan one head, a duplicate revision id, a missing parent, or a cycle. Add the\njob to `needs:` in the gate and fail the gate unless it concluded `success`. If\nthe human does not want this, they can run attended instead.\n\n### 6c. Land both files\n\n1. Ask the human before you branch and push.\n2. Create a branch, for example `chore/epic-implementer-workflows`.\n3. Commit `.github/workflows/epic-implementer-ci.yml` and the\n `.github/workflows/claude-review.yml` that step 5 wrote.\n4. Open a pull request into the default branch with `gh pr create`.\n5. Ask the human to review and merge it. Do not merge it yourself.\n\n**Secret the human must add** (repository Settings, then Secrets and variables,\nthen Actions): a secret with the exact name you passed to `--secret-name`,\nholding a Claude Code OAuth token. The human can create one with\n`claude setup-token`. Never ask them to paste the token to you.\n\n**Check it worked:** after the merge, re-run `readiness --json`.\n`server.review-workflow`, `server.conductor-ci-workflow`, and\n`server.repository-workflow-readiness` should pass.\n\n**If it fails:** readiness names the missing item, for example \"does not emit\nthe Reviewed-SHA: marker\". A detail saying Bridge \"could not read\" a file means\nthe read failed, not that the file is wrong. Re-run readiness. If it keeps\nfailing, contact Bridge support.\n\n## 7. Index the repository, after the workflows merge\n\n**Why:** `setup-epic` cuts `epic/<EPIC-KEY>` at the exact commit the code index\nwas built from, not at the tip of the default branch. If that commit is older\nthan the merge from step 6, the epic branch has no workflows. Then child PRs into\nit get no CI and no review, and every ticket waits forever. Each child ticket's\nplan also uses the index to find code.\n\n**Ask first?** No. Indexing runs on the server and changes nothing in the\nrepository. It can take a while on a large repository.\n\nAfter the step 6 pull request has merged, run the `/parse-repository` slash\ncommand, even if the repository was indexed before. Then poll the\n`parse_repository` MCP tool with `action: \"status\"` until it reports completed.\n`doctor` also shows the indexing state.\n\nYou will confirm the cut commit in [§11a](#11a-plan-the-epic), once a plan\nexists.\n\n**If it fails:** report the status message to the human. Do not start a real\nrun until indexing has completed after the merge.\n\n## 8. Unattended prerequisites\n\n**Why:** an unattended run merges on its own, so three things are checked\nbefore a run is created. `--attended` skips them. `setup-epic` (which `/plan-epic`\nruns) and the `drive-epic` preview name each one differently.\n\n**Repository consent**\n- `setup-epic` refusal: `target_not_allowed`. `drive-epic` preview:\n `unattended_consent`. Readiness row: `server.unattended-consent`.\n- It is **on by default**. The repository owner switches \"Allow unattended Epic\n Implementer runs\" on the repository's Setup page in the Bridge admin panel.\n Only a human can change it. If it is already on, contact Bridge support.\n\n**An escalation channel** (where alerts go when the run needs a person)\n- `setup-epic` refusal: `webhook_unverified`. `drive-epic` preview:\n `unattended_notify_default`. Readiness row: `server.unattended-notify-default`.\n- The simplest channel is the email address on the Bridge account. It needs no\n extra step when the account has one.\n- A webhook also works, but only through a `--policy-file` that sets\n `notify.webhook_url`. `/plan-epic` and `/drive-epic-run` never pass a policy\n file, so the webhook route needs the shell `drive-epic` ([§11c](#11c-attended-runs)\n shows the command form). `notify.local_sink` does not count.\n\n ```json\n { \"notify\": { \"webhook_url\": \"https://example.com/your-webhook\" } }\n ```\n\n A policy file is the **complete** run policy, so keep it minimal. The server\n tests the webhook when the run activates, so it must be reachable from the\n internet.\n\n**The two workflows**\n- `setup-epic` refusal: `repository_readiness_unconfirmed`. `drive-epic`\n preview: `repository_readiness`. Readiness row:\n `server.repository-workflow-readiness`.\n- Finish step 6. The message names the missing file, marker, or job.\n\n**Readiness could not be read**\n- `setup-epic` refusal: `readiness_unreadable`.\n- The server did not report these facts. Retry. If it persists, contact Bridge\n support.\n\n**Ask first?** Yes for anything that changes an account or admin setting. Only\nthe human can change these.\n\n## 9. Merge credential and merge modes\n\n**Why:** in an unattended run, the executor on this machine merges each child PR\ninto the epic branch with `gh pr merge`. In the default local mode it uses\n**only** the token in `BAPI_LOCAL_MERGE_TOKEN`. (A run in machine-user mode uses\nonly `BAPI_MACHINE_MERGE_TOKEN` instead.) It never falls back to the human's `gh`\nlogin, `GH_TOKEN`, or `GITHUB_TOKEN`. Without the token, merges are refused. The\ndiagnostics show this only as a warning (`conductor.merge-credential`), so it is\neasy to miss.\n\n**Ask first?** Yes. This concerns the human's GitHub credential.\n\nThe executor inherits its environment from the process that runs `drive-epic`.\nSo the variable must be set in the shell that launches `drive-epic`. If you use\n`/drive-epic-run`, that is **your own session's** environment, so the human must\nset it before starting the editor or `claude`.\n\nThe simplest source is the human's own GitHub CLI token:\n\n```bash\nexport BAPI_LOCAL_MERGE_TOKEN=\"$(gh auth token)\"\n```\n\nTo make it last, suggest the human add that exact line (the command, not the\ntoken value) to their shell profile, for example `~/.zshrc`, then open a new\nterminal and restart the editor. Never write the token value into any file, and\nnever add it to the repository.\n\nIf an executor is already running for a run, it will not see a newly exported\nvalue. It must be restarted from a shell that has it.\n\n**Check it worked:** `npx -y @bridge_gpt/mcp-server epic-implementer doctor`\nno longer warns about the merge credential. Run it from the same shell.\n\n### Optional: GitHub Actions merge mode\n\nUse this only if the human wants merges done by a GitHub Actions workflow\ninstead of this machine. It applies only to a run whose policy file declares\n`\"merge_execution_mode\": \"github_actions\"`. Otherwise skip it.\n\nAsk the human to run, in their own terminal (it asks before writing):\n\n```bash\nnpx -y @bridge_gpt/mcp-server install conductor --merge-workflow --codeowner <@org/team|@user>\n```\n\n`--codeowner` is required: the one GitHub user or team who must review changes\nto the merge workflow. Add `--dry-run` first to preview. The command writes\n`.github/workflows/epic-implementer-merge.yml` and two helper files, pushes a\nbranch with the human's own git, and opens a pull request with their own `gh`.\nIt never merges it.\n\nAfter the human merges that pull request, three manual steps remain. The human\ndoes all three:\n\n1. Create a repository Actions secret named `BAPI_API_KEY` holding the\n repository's Bridge API key.\n2. Merge the rule from `.bridge/merge-workflow/CODEOWNERS.fragment` into the\n active CODEOWNERS file. The fragment does nothing on its own.\n3. Add a default-branch rule that requires a pull request and code-owner review.\n Until it exists, the workflow file is not protected.\n\n## 10. Verify\n\n**Before a plan exists**, use readiness only:\n\n```bash\nnpx -y @bridge_gpt/mcp-server epic-implementer readiness --json\n```\n\nThe `drive-epic` preview is not useful yet. Without a plan it only says\n`decision: stop`, with no reason, or a generic \"not ready\" refusal.\n\n**Done means:**\n\n- These rows pass: `server.supervisor-setup`, `server.supervisor-config`,\n `server.supervisor-required-checks`, and `server.github-credentials`.\n- For unattended runs, these pass too: `server.unattended-consent`,\n `server.unattended-notify-default`, and `server.repository-workflow-readiness`.\n If a `server.admission-required-checks` row is present, it passes too.\n- `server.worker-present` and `server.single-transition-authority` pass. These\n are the server's own health. If they fail, contact Bridge support.\n- `conductor.merge-credential` passes in the shell that will launch runs.\n- The `get_epic_snapshot` and `explain_run` tools are available.\n- Every other `fail` is explained, and the human accepted it.\n\n**After `/plan-epic` has written a plan**, preview the launch:\n\n```bash\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC> --plan-file <path to epic-plan.dag.json> --dry-run --json\n```\n\nIt changes nothing. Its `decision` is `create`, `attach`, `refuse`, or `stop`. A\n`refuse` carries `refusal.reason_id` and `refusal.remedy`. Map the `reason_id`:\n\n- `supervisor_setup`, `supervisor_config` — step 5.\n- `github_credentials` — step 4.\n- `unattended_consent`, `unattended_notify_default`, `repository_readiness` — step 8.\n- `remote-reconciler-not-live` — the Bridge server side. Contact Bridge support.\n- Anything else — read its `remedy`, then check the table in step 12 before you\n run anything.\n\nReport to the human what passed, what failed, and what they still need to do.\n\n## 11. First run\n\n### 11a. Plan the epic\n\n`/plan-epic` needs an **existing epic ticket**. Run `/plan-epic <EPIC>` with its\nkey, for example `SHOP-100`. Free text instead of a key gives no epic key, and\nno run can be created from it.\n\nIt explores the code, splits the epic into child tickets, and asks the human to\napprove the split. It writes its files to `<docs_dir>/epic-plans/<slug>/`, where\n`<docs_dir>` is the docs folder the `ping` MCP tool reports (often `docs/tmp`).\nThe plan file is `epic-plan.dag.json`.\n\n**Before the plan is approved, check every `ticket_key` in\n`epic-plan.dag.json`.** Each one must be a real ticket key. A placeholder such as\n`TBD-1` must not reach approval. If you see one, stop and create or link the\nreal tickets first.\n\n**The last step of `/plan-epic` creates and approves an unattended run** (it\nruns `setup-epic` without `--attended`). For an attended run, see §11c before\nyou reach that step.\n\nBefore that step runs `setup-epic` for real, run its dry run and check the cut\ncommit (plain output, not `--json`):\n\n```bash\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <EPIC> \\\n --plan-file <path to epic-plan.dag.json> --dry-run\ngit fetch origin\ngit ls-tree --name-only <sha from the \"Cut:\" line> .github/workflows/\n```\n\nThe listing must include `claude-review.yml` and `epic-implementer-ci.yml`. If\nit does not, the index is older than the step 6 merge. Go back to step 7 and\nre-index first.\n\n**If `setup-epic` fails, what happened depends on where it failed:**\n\n- **`setup-epic REFUSED (<reason>)`** — a check before creation failed.\n `target_not_allowed`, `webhook_unverified`, `repository_readiness_unconfirmed`,\n and `readiness_unreadable` are all this kind. **Nothing was created.** Fix the\n named item (step 8) and run it again.\n- **`The plan was NOT approved: …`** — the server refused at approval. By then\n the run **was** created, the epic branch **was** cut, and the plan **was**\n stored. The run waits in `planning`. The message always talks about unattended\n consent, whatever the real cause. Ignore that wording and read the `Detail:`\n line for `predicate` and `reason_code`:\n - `admission_evidence_unverifiable` or `admission_evidence_denied` with\n predicate `required_checks` — the required check is not a static job name in\n the two workflow files (step 5a, and \"Things to keep\" in 6a).\n - predicate `repository_readiness` — step 6.\n - predicate `webhook` or `notify_webhook` — step 8, escalation channel.\n - predicate `target_allowed` or `unattended_consent` — step 8, consent.\n - any other predicate — report the `predicate` and `reason_code` to the human.\n\n Fix the cause, then run the same `setup-epic` command again. It reuses the\n existing run instead of creating a second one.\n\n### 11b. Drive an unattended run\n\nIn Claude Code or Cursor:\n\n```text\n/drive-epic-run <EPIC> <path to epic-plan.dag.json>\n```\n\nIt previews the launch, asks the human once, creates the run (or attaches to the\none `/plan-epic` created), starts an executor, and watches the run to the end. It\naccepts no flags, so it cannot pass `--attended` or `--policy-file`. When it\ncreates a run, the run must be unattended.\n\nFrom a shell, the same launch is:\n\n```bash\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC> --plan-file <path to epic-plan.dag.json>\n```\n\nAdd `--policy-file <policy.json>` for a webhook (step 8). Run it from the\nrepository root, in a shell that has `BAPI_LOCAL_MERGE_TOKEN`. Always pass\n`--plan-file`. Without it, `drive-epic` searches only `docs/tmp/epic-plans/` (or\n`BAPI_DOCS_DIR`), which may not be where `/plan-epic` wrote. `drive-epic` starts\na run-scoped executor in the background. Its log is\n`.bridge/executors/<run>.log`. The executor exits by itself when the run is done\nor abandoned.\n\n### 11c. Attended runs\n\nAn attended run never merges by itself. It still needs a required check that\nreally runs on child PRs into `epic/<EPIC-KEY>` (step 5a, attended branch), and a\nreview verdict on those PRs (step 5b, `--branches` including `epic/**`).\n\n1. Plan with `/plan-epic <EPIC>`, but **do not let its last step run\n `setup-epic`**. That step would create an unattended run whenever the\n unattended prerequisites hold. Tell the human, and skip that step with their\n agreement. If it has already run and refused with `setup-epic REFUSED`,\n nothing was created and you can continue.\n2. Check every `ticket_key` in `epic-plan.dag.json`, as in §11a.\n3. Launch from a shell, from the repository root:\n\n ```bash\n npx -y @bridge_gpt/mcp-server drive-epic <EPIC> \\\n --plan-file <path to epic-plan.dag.json> --attended\n ```\n\n`/drive-epic-run` cannot create an attended run.\n\n### 11d. Monitor\n\n- MCP tools: `get_epic_snapshot` (the full run state) and `explain_run` (why a\n run or ticket is stuck). Both need the `epic_implementer` profile from step 5.\n- Notifications: `npx -y @bridge_gpt/mcp-server notifications --repo <repo_name> --run <EPIC> --follow`.\n- Quick health: `npx -y @bridge_gpt/mcp-server epic-implementer epic-status --epic-key <EPIC>`.\n\n### 11e. Stop and recover\n\nAlways diagnose with the `explain_run` MCP tool first. Then apply one verb. Ask\nthe human before any of them. Each runs as:\n\n```bash\nnpx -y @bridge_gpt/mcp-server epic-implementer <verb> --epic-run-id <EPIC>\n```\n\n`--epic-run-id` takes the run id, or the epic key when the epic has exactly one\nactive run.\n\nThe two you will use most:\n\n| Verb | Use it to |\n|---|---|\n| `unpark --ticket-key <key>` | Send a parked (`needs_human`) ticket back into its gates once the cause is fixed. |\n| `stop-run` | Stop the run now: no new work, queued work cancelled. The record stays. |\n\nAdvanced verbs. Use them only when `explain_run` points at them:\n\n| Verb | Use it to |\n|---|---|\n| `abandon-run` | End the run for good. **Terminal and irreversible.** |\n| `resume-run [--clear-quarantine]` | Resume a paused run. |\n| `adopt-current-head-and-unpark --ticket-key <key>` | Recover a ticket parked because someone pushed to its PR. |\n| `continue-child --ticket-key <key>` | Let a parked child carry on without resetting its attempt. |\n| `continuation-receipt --ticket-key <key>` | Check whether a `continue-child` request took effect (read-only). |\n| `resume-ci --ticket-key <key>` | Resume a child parked on a CI stop once CI is green. |\n| `dispatch-remediation --ticket-key <key> --context-file <path>` | Queue a repair job with your notes. |\n| `merge-child --pr <number>` | Merge the run's child PR through the shared merge service. |\n| `scope-reparse --scope-id <scope>` | Re-index the run's own index scope. |\n| `scope-catch-up --scope-id <scope>` | Replay merges the run's scope missed. |\n| `patch-policy --patch-file <path>` | Change part of the run's policy. |\n\nIn Claude Code or Cursor, `/drive-epic-run` applies these for you within limits,\nand asks when it must.\n\n## 12. Troubleshooting and known-wrong remedies\n\nCurrent tools print complete commands. Replace any placeholder in them with a\nreal value before you run them. Older tool versions printed the shorthand in the\nleft-hand column below, which does not work as typed. Use the right-hand column\ninstead.\n\n| Printed remedy | Problem | Run this instead |\n|---|---|---|\n| `install-bridge connect-github` | `install` accepts no such argument, and there is no `npx` prefix. | `npx -y @bridge_gpt/mcp-server connect-github --repo <repo_name>` (step 4) |\n| `install-bridge conductor` or `install conductor` with no flags | Missing the `npx` prefix and the required `--required-check`. | The full command in step 5c |\n| `install-bridge conductor --merge-workflow` (older tools) | Missing the `npx` prefix and the required `--codeowner`. | `npx -y @bridge_gpt/mcp-server install conductor --merge-workflow --codeowner <@org/team\\|@user>`; replace the placeholder with the one reviewer (step 9) |\n| `npx @bridge_gpt/mcp-server install` | Missing `-y`, so `npx` stops to ask. | `npx -y @bridge_gpt/mcp-server install` (step 3) |\n| Any remedy for `repository_readiness_unconfirmed` that names an install command | No install command creates the workflows. | Step 6, then step 7 |\n| `The plan was NOT approved: this repository is no longer authorized for unattended runs` | Printed for every approval refusal, whatever the cause. | Read `Detail:` (§11a) |\n| `install conductor --executor-id <id> --repo-wide` for executor readiness | Not needed to launch. | Nothing. `drive-epic` starts an executor per run. |\n\nOther common problems:\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Every `<launcher>` command exits `1` with a list of variable names | Old Epic Implementer variable names are set. | Step 2, retired-variable check. |\n| `install conductor` prints `Aborted` and exits `0` | No terminal, or the human answered no. | Rule 5: the human runs it in their own terminal. |\n| `--merge-workflow` prints `consent_declined` | No terminal, or the human answered no. | Rule 5. |\n| 403 on any command | `repo_name` does not match byte for byte. | Step 3. |\n| `get_epic_snapshot` is not available | The MCP client was not restarted, or the profile lacks `epic_implementer`. | Step 5, \"After it finishes\". |\n| Child PRs get no CI or no review at all | The epic branch was cut before the workflows merged, or `--branches` / the CI trigger lacks `epic/**`. | Steps 5b, 6a, and 7. |\n| Tickets wait forever at CI | The required check name does not match a check that runs on child PRs. | Step 5a. Compare with `gh pr view <n> --json statusCheckRollup`. |\n| Tickets pass CI but no tests ran | A narrowed `feature/<PREFIX>-` does not match the child branches. | Step 6a. |\n| Approval refused: `admission_evidence_unverifiable` on `required_checks` | The required check is not a static job name in the two workflow files. | Steps 5a and 6a. |\n| Child PRs are never merged | `BAPI_LOCAL_MERGE_TOKEN` was not set for the executor. | Step 9. |\n| `merge_execution_ineligible` | The policy declares `github_actions` merge mode, but the merge workflow is missing or not ready. | Step 9, optional part. Or remove that line from the policy. |\n| `drive-epic` cannot find the plan | It searched `docs/tmp/epic-plans/` only. | Pass `--plan-file`. |\n| The reconciler or worker rows fail | The Bridge server side is not healthy. | Nothing to fix locally. Contact Bridge support. |\n"
|
|
8
13
|
};
|