@bridge_gpt/mcp-server 0.2.59 → 0.2.60
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 +173 -51
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +53 -2
- package/build/conductor/doctor.js +255 -53
- package/build/conductor/epic-runtime.js +11 -2
- package/build/conductor/install-doctor.js +24 -2
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/readiness-sections.js +159 -2
- package/build/conductor/readiness.js +95 -14
- package/build/conductor/recovery-operations.js +8 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +183 -9
- package/build/drive-epic.js +23 -0
- package/build/epic-implementer-bin.js +10 -10
- package/build/epic-implementer-claude-hook-bin.js +1 -1
- package/build/epic-implementer-env.js +43 -14
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +60 -0
- package/build/executor/index.js +21 -4
- package/build/executor/job-runner.js +126 -0
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +11 -2
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +194 -179
- package/build/install-bridge-conductor.js +201 -8
- package/build/install-bridge.js +356 -30
- package/build/mcp-install-state.js +42 -4
- package/build/merge-pull-request.js +103 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/pipelines.generated.js +2 -2
- package/build/plane/cli.js +24 -2
- package/build/plane/preflight.js +68 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +163 -9
- package/build/version.generated.js +3 -3
- package/package.json +3 -3
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/bridge-client.js +0 -773
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/cut-protocol.js +0 -392
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- package/build/plan-epic-conductor-eligibility.js +0 -213
package/build/docs.generated.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
2
|
// This file is produced by scripts/bundle-docs.js
|
|
3
3
|
export const DOCS = {
|
|
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 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### 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 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",
|
|
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
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/explore-ticket.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: `/explore-ticket` 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## 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/explore-ticket.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"
|
|
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## 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
7
|
};
|
package/build/doctor.js
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* found/missing plus the exact per-OS manual install command for each missing one.
|
|
11
11
|
*
|
|
12
12
|
* The audience is security-conscious/corporate, so the doctor is STRICTLY
|
|
13
|
-
* read-only: no `--fix`, no auto-install, no npm `postinstall`, no
|
|
14
|
-
* spawning, no MCP server startup.
|
|
15
|
-
* (`which`/`where`, `bash --version`,
|
|
13
|
+
* read-only: no `--fix`, no auto-install, no npm `postinstall`, no interactive
|
|
14
|
+
* terminal spawning, no MCP server startup. Every child process it starts is a
|
|
15
|
+
* QUERY: read-only PATH probes (`which`/`where`, `bash --version`,
|
|
16
|
+
* `git rev-parse`) and, on a host that has generated executor service units, the
|
|
17
|
+
* one read-only session probe that section issues (`loginctl show-user
|
|
18
|
+
* --property=Linger` / `launchctl print gui/<uid>`, BAPI-1137). Nothing here can
|
|
19
|
+
* reach a mutating lifecycle verb — `enable-linger`, `bootstrap`, `start` and
|
|
20
|
+
* their kin exist in this file only as remediation TEXT the operator runs.
|
|
16
21
|
*/
|
|
17
22
|
import { readdir, readFile, stat } from "fs/promises";
|
|
18
23
|
import { spawn } from "child_process";
|
|
@@ -39,7 +44,7 @@ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
|
39
44
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
40
45
|
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
41
46
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
42
|
-
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
47
|
+
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, inspectExecutorServiceDurability, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
43
48
|
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
44
49
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
|
|
45
50
|
import { validateBranchName } from "./base-ref.js";
|
|
@@ -1005,6 +1010,53 @@ export function formatToolSurfaceDiagnosticReport(diag) {
|
|
|
1005
1010
|
lines.push("to observe surface changes; no project MCP configuration change is required.");
|
|
1006
1011
|
return lines.join("\n");
|
|
1007
1012
|
}
|
|
1013
|
+
// ---------------------------------------------------------------------------
|
|
1014
|
+
// Executor provisioning diagnostics (BAPI-688) — STRICTLY read-only.
|
|
1015
|
+
//
|
|
1016
|
+
// Enumerates the deterministic, convention-named executor service units this
|
|
1017
|
+
// package generates (`executor install-service`) and reports their non-secret
|
|
1018
|
+
// configuration plus credential readiness. It never writes, never runs a
|
|
1019
|
+
// MUTATING `launchctl`/`systemctl`/`loginctl` verb, and — like every advisory
|
|
1020
|
+
// section — never influences the doctor exit code.
|
|
1021
|
+
//
|
|
1022
|
+
// It does spawn ONE child process, and only on a host that already has such a
|
|
1023
|
+
// unit: the read-only session-durability query added by BAPI-1137, through
|
|
1024
|
+
// `defaultReadOnlyLifecycleRunner` below. That is a real change to this
|
|
1025
|
+
// section's behavior and is stated here rather than left to be discovered —
|
|
1026
|
+
// with no injected runner it issues none, and the fact degrades to
|
|
1027
|
+
// `unverifiable` rather than being guessed.
|
|
1028
|
+
//
|
|
1029
|
+
// Discovery is derived SOLELY from the fixed OS directory + filename prefix, not
|
|
1030
|
+
// from `.bridge/install-state.json`: install state is conductor install history,
|
|
1031
|
+
// not an artifact registry, so a unit written by hand or by another machine's
|
|
1032
|
+
// operator is still found, and a stale state entry never invents one.
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
/**
|
|
1035
|
+
* Shell-free runner for the ONE read-only durability query this section issues
|
|
1036
|
+
* (BAPI-1137).
|
|
1037
|
+
*
|
|
1038
|
+
* `shell: false` plus an argument ARRAY, matching every other service-manager
|
|
1039
|
+
* invocation in this package: a uid or identifier is data here, never syntax.
|
|
1040
|
+
* The probes it runs — `loginctl show-user --property=Linger`,
|
|
1041
|
+
* `launchctl print gui/<uid>` — are queries; nothing in this file can reach a
|
|
1042
|
+
* mutating lifecycle command. Output is captured only so the lifecycle module
|
|
1043
|
+
* can map a recognized state token, and is never surfaced.
|
|
1044
|
+
*/
|
|
1045
|
+
function defaultReadOnlyLifecycleRunner(executable, args) {
|
|
1046
|
+
return new Promise((resolve, reject) => {
|
|
1047
|
+
const child = spawn(executable, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
1048
|
+
let stdout = "";
|
|
1049
|
+
let stderr = "";
|
|
1050
|
+
child.stdout?.on("data", (chunk) => {
|
|
1051
|
+
stdout += chunk.toString("utf-8");
|
|
1052
|
+
});
|
|
1053
|
+
child.stderr?.on("data", (chunk) => {
|
|
1054
|
+
stderr += chunk.toString("utf-8");
|
|
1055
|
+
});
|
|
1056
|
+
child.on("error", reject);
|
|
1057
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1008
1060
|
/** Where generated units live, and how their filenames encode the executor id. */
|
|
1009
1061
|
function resolveExecutorServiceConvention(platform, homeDir) {
|
|
1010
1062
|
if (platform === "darwin") {
|
|
@@ -1097,6 +1149,36 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1097
1149
|
const pathApi = path.posix;
|
|
1098
1150
|
const units = [];
|
|
1099
1151
|
const warnings = [];
|
|
1152
|
+
// BAPI-1137 — the host-level durability prerequisite, probed ONCE per run
|
|
1153
|
+
// rather than once per unit: lingering and the GUI login domain are properties
|
|
1154
|
+
// of the host and the user, not of any particular unit. Without an injected
|
|
1155
|
+
// read-only runner no command is issued at all and the fact stays
|
|
1156
|
+
// `unverifiable`, so a caller that supplies no runner keeps a zero-probe run.
|
|
1157
|
+
let durability = {
|
|
1158
|
+
prerequisite: "unverifiable",
|
|
1159
|
+
remediation: null,
|
|
1160
|
+
};
|
|
1161
|
+
if (deps.runLifecycleCommand) {
|
|
1162
|
+
try {
|
|
1163
|
+
const inspection = await inspectExecutorServiceDurability({
|
|
1164
|
+
platform: deps.platform === "darwin" ? "darwin" : "linux",
|
|
1165
|
+
// The durability probe addresses the USER's session/linger state, not
|
|
1166
|
+
// a unit, so it needs neither a unit path nor a service identifier.
|
|
1167
|
+
unitPath: "",
|
|
1168
|
+
serviceIdentifier: "durability-probe",
|
|
1169
|
+
uid: deps.uid ?? null,
|
|
1170
|
+
}, deps.runLifecycleCommand);
|
|
1171
|
+
durability = {
|
|
1172
|
+
prerequisite: inspection.prerequisite,
|
|
1173
|
+
remediation: inspection.remediation,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
// Sanitized: a probe that throws leaves the fact unverifiable rather than
|
|
1178
|
+
// surfacing exception text or inventing a state.
|
|
1179
|
+
durability = { prerequisite: "unverifiable", remediation: null };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1100
1182
|
for (const name of matching) {
|
|
1101
1183
|
const unitPath = pathApi.join(convention.dir, name);
|
|
1102
1184
|
const executorId = convention.idFromFilename(name) ?? name;
|
|
@@ -1120,12 +1202,33 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1120
1202
|
baseUrlClass: "missing",
|
|
1121
1203
|
repos: [],
|
|
1122
1204
|
credentials: [],
|
|
1123
|
-
|
|
1205
|
+
// A unit whose bytes could not be read establishes NOTHING about what it
|
|
1206
|
+
// declares. `unverifiable`, never `confirmed-absent`.
|
|
1207
|
+
noStandingGithubWriteCredential: "unverifiable",
|
|
1208
|
+
warnings: [
|
|
1209
|
+
"unit could not be read; its configuration cannot be verified.",
|
|
1210
|
+
"regenerate it through durable-host mode (`install conductor`) if this persists.",
|
|
1211
|
+
],
|
|
1124
1212
|
});
|
|
1125
1213
|
continue;
|
|
1126
1214
|
}
|
|
1127
1215
|
const inspection = inspectExecutorServiceArtifact(convention.kind, content);
|
|
1128
1216
|
const unitWarnings = [...inspection.problems];
|
|
1217
|
+
// BAPI-1137 — the classification comes from the GENERATOR's shared helper,
|
|
1218
|
+
// applied to the unit read off disk. doctor maintains no second notion of
|
|
1219
|
+
// what "declares no GitHub write credential" means, so a unit this section
|
|
1220
|
+
// calls compliant is exactly a unit the generator would agree to write.
|
|
1221
|
+
const credentialDeclaration = inspection.noStandingGithubWriteCredential;
|
|
1222
|
+
unitWarnings.push(...inspection.credentialDeclarationProblems);
|
|
1223
|
+
if (credentialDeclaration === "present") {
|
|
1224
|
+
unitWarnings.push("this unit declares something outside the closed durable-host contract — regenerate it " +
|
|
1225
|
+
"through durable-host mode (`install conductor`) and remove the hand edit.");
|
|
1226
|
+
}
|
|
1227
|
+
else if (credentialDeclaration === "unverifiable") {
|
|
1228
|
+
unitWarnings.push("this unit could only be parsed partially, so the no-standing-GitHub-write-credential " +
|
|
1229
|
+
"property could not be established — regenerate it through durable-host mode " +
|
|
1230
|
+
"(`install conductor`).");
|
|
1231
|
+
}
|
|
1129
1232
|
let baseUrlClass;
|
|
1130
1233
|
if (!inspection.baseUrl) {
|
|
1131
1234
|
baseUrlClass = "missing";
|
|
@@ -1153,10 +1256,24 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1153
1256
|
baseUrlClass,
|
|
1154
1257
|
repos: inspection.repos,
|
|
1155
1258
|
credentials,
|
|
1259
|
+
noStandingGithubWriteCredential: credentialDeclaration,
|
|
1156
1260
|
warnings: unitWarnings,
|
|
1157
1261
|
});
|
|
1158
1262
|
}
|
|
1159
|
-
return { status: "collected", units, warnings };
|
|
1263
|
+
return { status: "collected", units, warnings, durability };
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* The rendered name of the BAPI-1137 credential diagnostic.
|
|
1267
|
+
*
|
|
1268
|
+
* Named for the NARROW claim it actually makes — the unit's own declarations —
|
|
1269
|
+
* rather than something broader like "host holds no GitHub credential", which
|
|
1270
|
+
* the check cannot support and an operator would reasonably read as covering
|
|
1271
|
+
* their SSH keys, keychain, and `gh` login.
|
|
1272
|
+
*/
|
|
1273
|
+
export const EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME = "unit-declares-no-github-write-credential";
|
|
1274
|
+
/** How each classification renders, once, so the three spellings cannot drift. */
|
|
1275
|
+
function formatCredentialDeclarationLine(classification) {
|
|
1276
|
+
return ` ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME}: ${classification}`;
|
|
1160
1277
|
}
|
|
1161
1278
|
/**
|
|
1162
1279
|
* Render the advisory "Executor provisioning" section (pure formatting — no I/O).
|
|
@@ -1179,6 +1296,7 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
|
1179
1296
|
lines.push(` executor id: ${unit.executorId}`);
|
|
1180
1297
|
lines.push(` ${EXECUTOR_SERVICE_BASE_URL_ENV}: ${unit.baseUrl ?? "(absent)"} [${unit.baseUrlClass}]`);
|
|
1181
1298
|
lines.push(` repos: ${unit.repos.length > 0 ? unit.repos.join(", ") : "(none parsed)"}`);
|
|
1299
|
+
lines.push(formatCredentialDeclarationLine(unit.noStandingGithubWriteCredential));
|
|
1182
1300
|
for (const cred of unit.credentials) {
|
|
1183
1301
|
lines.push(cred.resolved
|
|
1184
1302
|
? ` credential ${cred.target}: resolved (source: ${cred.source})`
|
|
@@ -1188,11 +1306,52 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
|
1188
1306
|
lines.push(`WARN ${unit.path}: ${warning}`);
|
|
1189
1307
|
}
|
|
1190
1308
|
}
|
|
1309
|
+
// The host-level durability prerequisite, rendered SEPARATELY from every unit
|
|
1310
|
+
// above: it is a property of this host and user, and attaching it to a unit
|
|
1311
|
+
// would imply the two were one fact.
|
|
1312
|
+
lines.push("");
|
|
1313
|
+
if (diagnostics.durability.prerequisite === "configured") {
|
|
1314
|
+
lines.push("found durable session: configured — services keep running without an active login.");
|
|
1315
|
+
}
|
|
1316
|
+
else if (diagnostics.durability.prerequisite === "session-bound") {
|
|
1317
|
+
// Deliberately NOT a "found" line. The prerequisite this platform can check
|
|
1318
|
+
// is satisfied, but what it establishes is a login session, not logout
|
|
1319
|
+
// durability, and saying otherwise would contradict launchd.
|
|
1320
|
+
lines.push("WARN durable session: session-bound — a login session exists, but it is NOT logout-durable.");
|
|
1321
|
+
if (diagnostics.durability.remediation) {
|
|
1322
|
+
lines.push(` operator action: ${diagnostics.durability.remediation}`);
|
|
1323
|
+
}
|
|
1324
|
+
lines.push(" A LaunchAgent stops with the login it runs under; closing the lid or logging out", " stops the executor. doctor performed a READ-ONLY query and changed nothing.");
|
|
1325
|
+
}
|
|
1326
|
+
else if (diagnostics.durability.prerequisite === "not-configured") {
|
|
1327
|
+
lines.push("WARN durable session: NOT durably configured for this host.");
|
|
1328
|
+
if (diagnostics.durability.remediation) {
|
|
1329
|
+
lines.push(` remediation (operator-run): ${diagnostics.durability.remediation}`);
|
|
1330
|
+
}
|
|
1331
|
+
lines.push(" doctor performed a READ-ONLY query only: it did not run `loginctl enable-linger`,", " change any session setting, or alter the exit code.");
|
|
1332
|
+
}
|
|
1333
|
+
else if (diagnostics.durability.prerequisite === "not-applicable") {
|
|
1334
|
+
lines.push("SKIP durable session: this platform has no session/linger prerequisite.");
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
lines.push("SKIP durable session: unverifiable — the read-only probe produced no recognized answer.", " That is not evidence that this host is, or is not, durably configured.");
|
|
1338
|
+
}
|
|
1339
|
+
lines.push("");
|
|
1340
|
+
lines.push("Unit-content compliance and service-manager state are INDEPENDENT facts: the manager may");
|
|
1341
|
+
lines.push("still be running a previously loaded definition rather than the bytes currently on disk, so a");
|
|
1342
|
+
lines.push("compliant unit file is not evidence about the running process, and neither is evidence that");
|
|
1343
|
+
lines.push("Bridge has observed an executor claim.");
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
lines.push(`The ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME} fact covers the unit's OWN declarations —`);
|
|
1346
|
+
lines.push("its environment keys and its arguments. Host-wide SSH keys and agents, login keychains, `gh`");
|
|
1347
|
+
lines.push("login state, git credential helpers, and independently managed Claude worker authentication");
|
|
1348
|
+
lines.push("are outside it and are never inspected or asserted here.");
|
|
1191
1349
|
lines.push("");
|
|
1192
1350
|
lines.push("Generated units carry only the base URL, the generating shell's PATH, repositories, and", "executor id — credentials resolve at");
|
|
1193
1351
|
lines.push("launch time from BAPI_API_KEY or the user-scoped bapi:<repo> store, so no key value is stored on");
|
|
1194
1352
|
lines.push("disk or printed here. Starting, stopping, and enabling a unit stay operator-managed. This section");
|
|
1195
|
-
lines.push("is advisory and
|
|
1353
|
+
lines.push("is advisory — including the credential-declaration and durable-session facts, which are excluded");
|
|
1354
|
+
lines.push("from required-prerequisite computation — and never changes the doctor exit code.");
|
|
1196
1355
|
return lines.join("\n");
|
|
1197
1356
|
}
|
|
1198
1357
|
export async function collectPlaneCredentialDiagnostic(deps) {
|
|
@@ -1370,6 +1529,12 @@ export async function collectOperationalAdvisories(deps) {
|
|
|
1370
1529
|
}
|
|
1371
1530
|
}
|
|
1372
1531
|
// BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
|
|
1532
|
+
// (BAPI-1137) still ONLY a path-presence check: the helpers' CONTENTS are
|
|
1533
|
+
// never read, they are never executed, and neither file is ever deleted. The
|
|
1534
|
+
// guidance below names durable-host mode as their replacement; removing them
|
|
1535
|
+
// stays an operator step, because they live outside this repository and
|
|
1536
|
+
// product code that deleted files in `~/.local/bin` would be a far larger
|
|
1537
|
+
// authority than a read-only diagnostic should hold.
|
|
1373
1538
|
// `homedir` seams every other advisory here uses, so it stays deterministic
|
|
1374
1539
|
// under test and reaches no real filesystem in one. A `stat` that throws is
|
|
1375
1540
|
// "absent"; it is never retried, and a permission error is not reported as a
|
|
@@ -1437,8 +1602,10 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1437
1602
|
for (const helper of diagnostic.legacyDriveHelpers) {
|
|
1438
1603
|
lines.push(` ${helper}`);
|
|
1439
1604
|
}
|
|
1440
|
-
lines.push(" These predate the scoped two-phase bring-up and are retired.
|
|
1441
|
-
"
|
|
1605
|
+
lines.push(" These predate the scoped two-phase bring-up and are retired. The supported\n" +
|
|
1606
|
+
" replacement is DURABLE-HOST MODE: run `install conductor` to plan, write, and\n" +
|
|
1607
|
+
" start the executor as a persistent service, then confirm\n" +
|
|
1608
|
+
" `drive-epic <EPIC> --plan-file <dag>` works for you, and remove them.\n" +
|
|
1442
1609
|
" doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
|
|
1443
1610
|
" modify either file, and it cannot delete them for you.");
|
|
1444
1611
|
}
|
|
@@ -1688,6 +1855,13 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1688
1855
|
((p) => readFile(p, "utf-8")),
|
|
1689
1856
|
stat: overrides.executorService?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
1690
1857
|
resolveCredentials: overrides.executorService?.resolveCredentials,
|
|
1858
|
+
// BAPI-1137 — the durability prerequisite needs a uid and a read-only
|
|
1859
|
+
// runner. `process.getuid` is absent on win32, where a null uid simply
|
|
1860
|
+
// leaves the fact unverifiable rather than interpolating `undefined`
|
|
1861
|
+
// into a domain target.
|
|
1862
|
+
uid: overrides.executorService?.uid ??
|
|
1863
|
+
(typeof process.getuid === "function" ? process.getuid() : null),
|
|
1864
|
+
runLifecycleCommand: overrides.executorService?.runLifecycleCommand ?? defaultReadOnlyLifecycleRunner,
|
|
1691
1865
|
};
|
|
1692
1866
|
const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
|
|
1693
1867
|
log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
|
package/build/drive-epic.js
CHANGED
|
@@ -161,6 +161,29 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
161
161
|
remedy: "Run `install-bridge conductor` to install them, or pass --attended.",
|
|
162
162
|
satisfied: (r) => r.unattended?.repository_readiness_confirmed === true,
|
|
163
163
|
},
|
|
164
|
+
// BAPI-1135 — merge-execution eligibility is deliberately NOT a requirement here.
|
|
165
|
+
//
|
|
166
|
+
// It was one, and it made `drive-epic` refuse EVERY repository the moment it
|
|
167
|
+
// shipped. The requirement could only be keyed on repository facts, because
|
|
168
|
+
// this layer keeps the composed run policy opaque to itself — and no
|
|
169
|
+
// repository carries the merge workflow until epic child 4 lands it. So both
|
|
170
|
+
// states resolved the same way: an upgraded server reported
|
|
171
|
+
// `merge_workflow_present: false` (`not_eligible`), an older one reported no
|
|
172
|
+
// block at all (`could_not_verify`), and either made `selectConductor` return
|
|
173
|
+
// `not_ready` and `classifyV2Readiness` return `operator_blocked` — for
|
|
174
|
+
// `--attended`, `--into-base` and `local`-mode runs alike, with a remedy
|
|
175
|
+
// naming a flag that does not exist yet.
|
|
176
|
+
//
|
|
177
|
+
// That is exactly the failure `selectConductor`'s own docstring warns about,
|
|
178
|
+
// and it contradicts the compatibility guarantee the rest of this slice keeps:
|
|
179
|
+
// nothing changes for a run that does not declare `github_actions`.
|
|
180
|
+
//
|
|
181
|
+
// Eligibility is still enforced, at the three layers that can actually see the
|
|
182
|
+
// declared mode: `setup-epic`'s pre-create refusal (which reads the composed
|
|
183
|
+
// policy), the approval route's server-side gate, and — authoritatively — the
|
|
184
|
+
// before-merge preflight. Readiness still REPORTS the facts as diagnostic
|
|
185
|
+
// rows, which inform without blocking. Reinstating a requirement here belongs
|
|
186
|
+
// with child 4, once a repository can actually satisfy it.
|
|
164
187
|
];
|
|
165
188
|
/**
|
|
166
189
|
* The readiness facts a `plane up` can actually repair (BAPI-1054).
|