@bridge_gpt/mcp-server 0.2.42 → 0.2.43
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 +321 -182
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/commands.generated.js +4 -3
- package/build/conduct-epic/cli.js +195 -0
- package/build/conductor/bridge-api-client.js +121 -0
- package/build/conductor/cli.js +63 -0
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor-bin.js +9 -5
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +13 -3
- package/build/drive-epic.js +375 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-runner.js +50 -3
- package/build/executor/observation.js +105 -13
- package/build/executor/runner.js +219 -0
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index.js +2156 -98
- package/build/install-bridge.js +23 -9
- package/build/pipelines.generated.js +304 -14
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +14 -4
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +19 -0
- package/build/plane/shutdown.js +71 -3
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +75 -5
- package/build/setup-epic.js +82 -8
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/package.json +1 -1
- package/pipelines/greenfield-setup.json +286 -0
package/build/docs.generated.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
// This file is produced by scripts/bundle-docs.js
|
|
3
3
|
export const DOCS = {
|
|
4
4
|
"docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_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 brainstorms. 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`materialize_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 brainstorming never\nqueries the index and is 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`, `get_my_role`, `persist_routing_credential`,\n`get_docs_dir`, and the bootstrap-invite exchange) are always available — they\nare how you configure everything else.\n",
|
|
5
|
-
"docs/install/sfcc-integration.md": "# Installing the SFCC Integration (OCAPI)\n\nBridge's Salesforce B2C Commerce (SFCC) tools give an AI coding agent read access to\na sandbox's object model, custom object definitions, and site preferences — plus a\nsmall set of sandbox-only writes — through the **OCAPI Data API**. This guide covers\nsetting up the OCAPI client that those tools authenticate against.\n\n> **Sandbox / local development only.** This integration is intended for a **developer\n> sandbox**, and that restriction is **enforced in code**: before any SFCC tool runs,\n> Bridge validates the hostname your credentials actually resolve to — from `dw.json`\n> or `SFCC_*` — against the sandbox forms listed below. An unrecognized host is refused\n> with a `403` (`error.code: \"TARGET_NOT_SANDBOX\"`) before any request leaves your\n> machine. The check reads the resolved hostname, never the `instance` tool argument,\n> so omitting `instance` or passing `\"sandbox\"` cannot bypass it.\n>\n> Accepted sandbox hostname forms:\n>\n> - `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com`\n> - `<realm>-<nnn>.dx.commercecloud.salesforce.com`\n>\n> Anything else — a `production-`/`staging-`/`development-` prefixed host, or any\n> `*.demandware.net` host — is rejected.\n>\n> Still do not configure the grants below on an instance that holds real data.\n> Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to\n> Bridge.\n\nFor the full per-tool list and what each SFCC tool depends on, see\n[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference\nand the `BRIDGE_MCP_PROFILE` gating, see the SFCC section of the\n[package README](../../README.md).\n\n## Prerequisites\n\n- A running SFCC **developer sandbox** and its hostname\n (e.g. `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com`).\n- An **Account Manager API client** — a `client-id` and `client-secret`. This is the\n OCAPI client the tools use to obtain an OAuth token. Create one in Account Manager\n (**API Client** → *Add API Client*) if you don't already have it, and note its\n `client_id`.\n- Business Manager access to the sandbox with permission to edit **Open Commerce API\n Settings**.\n\n## 1. Grant the OCAPI client access in Business Manager\n\nIn Business Manager for the sandbox:\n\n**Administration → Site Development → Open Commerce API Settings → Data API** tab.\n\nAdd the client entry below to the `clients` array of the Data API settings, then\n**Save**. It grants only the resource families and HTTP methods Bridge's SFCC tools\nactually call — not a global `/**` grant. `check_permissions` prints the same JSON on\na 401/403, split into the two blocks.\n\n**READ/SEARCH TOOL GRANTS** — required by the `sfcc` read tools. (`post` is OCAPI's\nconvention for its `*_search` endpoints, not a mutation.)\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\n**MUTATION GRANTS** — required **only if you enable `BRIDGE_MCP_PROFILE=sfcc-write`**,\nwhich registers the nine destructive write tools. These are shipped capabilities, not\nfuture work. No `delete` is granted, because no shipped write tool performs one; the\n`get` entries are needed for the If-Match ETag round trip that precedes each `PATCH`.\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"put\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"patch\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n}\n```\n\nNotes:\n\n- The `client_id` **must match** the Account Manager API client whose credentials you\n put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if\n it differs.\n- If the Data API settings are empty, wrap the entries in the standard settings\n envelope. Merge the resource lists from the block(s) above into one `resources`\n array — do not substitute a global `\"resource_id\": \"/**\"` grant:\n\n ```json\n {\n \"_v\": \"23.2\",\n \"clients\": [\n {\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n { \"resource_id\": \"/system_object_definitions\", \"methods\": [\"get\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/system_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/site_preferences/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" },\n { \"resource_id\": \"/custom_object_definitions/**\", \"methods\": [\"get\", \"post\"], \"read_attributes\": \"(**)\", \"write_attributes\": \"(**)\" }\n ]\n }\n ]\n }\n ```\n\n- `check_permissions` (below) prints a ready-to-paste grant JSON on a 401/403, so you can\n also let the tool tell you exactly what to add.\n\n## 2. Provide credentials locally\n\nCreate a `dw.json` in your project root (auto-added to git exclude — never commit it):\n\n```json\n{\n \"hostname\": \"zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com\",\n \"client-id\": \"<your-client-id-here>\",\n \"client-secret\": \"<account-manager-client-secret>\"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,\n`client-secret`/`clientSecret`/`client_secret`. Prefer a single config — a multi-entry\n`configs[]` array forces an explicit `instance` on every call. Alternatively, export\n`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n## 3. Set the repo `version` config field\n\nSet the repo's `version` config to your SFCC project type — one of\n`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;\na non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your\nnormal config path, the `config_field` MCP tool (operation `update`, field `version`),\nor the `/teach-bridge` skill.\n\n## 4. Enable the SFCC tools\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always\nregistered. Everything else is gated, behind **two independent profile groups**:\n\n| Group | Registers |\n|---|---|\n| `sfcc` | the 8 OCAPI read tools + `sfcc_log_query` — read-only |\n| `sfcc-write` | the 9 destructive write tools |\n\nNeither implies the other. Add what you need to `BRIDGE_MCP_PROFILE` in the MCP server\n`env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n\"env\": { \"BRIDGE_MCP_PROFILE\": \"sfcc\" }\n```\n\nFor reads plus writes, use `\"sfcc,sfcc-write\"`. `full` expands to every group and is\ntherefore write-capable.\n\n> **Migration.** `sfcc` used to register the nine write tools too. It no longer does.\n> If you were relying on SFCC writes through `BRIDGE_MCP_PROFILE=sfcc`, change it to\n> `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`. `full` users keep write access and need no\n> change.\n\n## 5. Verify\n\nAsk your agent to run:\n\n1. `sfcc_setup_status` — expect all prerequisite checks ✓ (Bridge API key, repo name,\n `version` config, `dw.json` presence/uniqueness, AM/OCAPI token acquisition).\n2. `check_permissions` — probes OCAPI via `GET /system_object_definitions`. A 200 (with\n the OCAPI version) confirms the grant. On 401/403 it prints the exact grant JSON to\n paste back in step 1.\n\nRestart the MCP client after any credential, grant, or env change — a running session\ndoes not pick them up.\n\n## Notes\n\n- **WebDAV logs are separate.** `sfcc_log_query` authenticates with a Business Manager\n username + a 40-character **WebDAV access key** over HTTP Basic auth — *not* the OCAPI\n OAuth token configured here. `sfcc_setup_status` reports OCAPI (step 5) and WebDAV\n (step 6) independently; one can be green while the other is not.\n- **Writes are sandbox-only.** The write tools (attribute/preference create/update) target\n a developer sandbox and echo a paste-ready grant JSON on a 403.\n"
|
|
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\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## 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
7
|
};
|
package/build/doctor.js
CHANGED
|
@@ -19,7 +19,7 @@ import { spawn } from "child_process";
|
|
|
19
19
|
import os from "os";
|
|
20
20
|
import path from "path";
|
|
21
21
|
import { createDefaultStartTicketsDeps } from "./start-tickets.js";
|
|
22
|
-
import { VERSION } from "./version.generated.js";
|
|
22
|
+
import { VERSION, BUILD_COMMIT } from "./version.generated.js";
|
|
23
23
|
import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
|
|
24
24
|
import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
|
|
25
25
|
import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
|
|
@@ -224,6 +224,14 @@ export function buildDoctorReportHeader(platform, agent) {
|
|
|
224
224
|
const activeGroups = Array.from(resolveProfiles(process.env.BRIDGE_MCP_PROFILE)).join(", ");
|
|
225
225
|
return [
|
|
226
226
|
DOCTOR_REPORT_TITLE,
|
|
227
|
+
// BAPI-873: doctor now names the build it is diagnosing. Both values are
|
|
228
|
+
// GENERATED constants — doctor runs no git subprocess and reads no
|
|
229
|
+
// repository state to report them, which keeps it read-only. `Build commit`
|
|
230
|
+
// renders the literal `unknown` when the build had no git metadata; it is
|
|
231
|
+
// never blank and never omitted. Neither line feeds launcher-pin drift,
|
|
232
|
+
// which stays a `VERSION`-only comparison.
|
|
233
|
+
`Package version: ${VERSION}`,
|
|
234
|
+
`Build commit: ${BUILD_COMMIT}`,
|
|
227
235
|
`Platform: ${platform}`,
|
|
228
236
|
`Selected agent: ${agent.name} (command: ${agent.command})`,
|
|
229
237
|
`Active MCP Groups: \`${activeGroups}\``,
|
|
@@ -597,8 +605,10 @@ export function formatLauncherCacheReport(inspections) {
|
|
|
597
605
|
// `BAPI_MCP_CLI` lets a packaged command shell out to a LOCAL build instead of
|
|
598
606
|
// `npx -y @bridge_gpt/mcp-server`, so an executor or CLI change can be verified
|
|
599
607
|
// before an npm publish. That is useful precisely because it is invisible: the
|
|
600
|
-
// override lives in one environment variable
|
|
601
|
-
// 0.2.x from a local build at the
|
|
608
|
+
// override lives in one environment variable. Since BAPI-873 the build commit
|
|
609
|
+
// reported above does distinguish a published 0.2.x from a local build at the
|
|
610
|
+
// same version — but it identifies THIS process, not the launcher a packaged
|
|
611
|
+
// command would shell out to, so an override still needs naming here.
|
|
602
612
|
//
|
|
603
613
|
// The probe is deliberately narrow. It runs the configured launcher ONCE with a
|
|
604
614
|
// single appended `--version` argument, through the injected array-based command
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drive-epic` — the one universal conductor entry point (BAPI-887).
|
|
3
|
+
*
|
|
4
|
+
* Bridge has two conductors and a standing rule that they must never operate on
|
|
5
|
+
* the same epic: two transition authorities on one epic wedge it permanently.
|
|
6
|
+
*
|
|
7
|
+
* - **v2 engine** — bootstrapped by `setup-epic`, which creates a durable
|
|
8
|
+
* `epic_run` row, stores a plan DAG, and approves it.
|
|
9
|
+
* - **LLM-conductor pilot** — `/loop 5m /conduct-epic <EPIC>`, which creates no
|
|
10
|
+
* run row and keeps its whole memory in a local checkpoint plus GitHub.
|
|
11
|
+
*
|
|
12
|
+
* Asking a prompt to pick correctly every time is not a control. This subcommand
|
|
13
|
+
* makes the choice STRUCTURAL: it reads server-side conductor readiness and
|
|
14
|
+
* routes to exactly one path, so no prompt names either underlying conductor and
|
|
15
|
+
* no prompt can present both. Every ticket-authoring surface hands off to
|
|
16
|
+
* `drive-epic` and to nothing else.
|
|
17
|
+
*
|
|
18
|
+
* ## The output invariant
|
|
19
|
+
*
|
|
20
|
+
* Every branch — including every error path — emits **at most one** conductor
|
|
21
|
+
* invocation. An escalation carries NO alternative conductor command: "v2 failed,
|
|
22
|
+
* try the pilot instead" is precisely the dual-authority hazard this subcommand
|
|
23
|
+
* exists to remove. {@link assertSingleConductorInvocation} enforces this on the
|
|
24
|
+
* way out rather than trusting each branch to have got it right.
|
|
25
|
+
*
|
|
26
|
+
* ## Three-state routing, fail-closed
|
|
27
|
+
*
|
|
28
|
+
* - readiness green -> delegate to the `setup-epic` (v2) flow
|
|
29
|
+
* - readiness read, not green -> print the pilot instruction
|
|
30
|
+
* - readiness UNKNOWN -> escalate to the operator, print no conductor
|
|
31
|
+
*
|
|
32
|
+
* Unknown is never treated as not-ready, and selection never happens by
|
|
33
|
+
* exception handling. An unreachable, unauthorized, or malformed readiness
|
|
34
|
+
* response means we do not know which authority owns this epic, and guessing is
|
|
35
|
+
* the failure mode.
|
|
36
|
+
*
|
|
37
|
+
* ## Consolidation day
|
|
38
|
+
*
|
|
39
|
+
* Two conductors is a transitional state; one is to be eliminated at the end of
|
|
40
|
+
* the current evaluation period. Consolidation is:
|
|
41
|
+
*
|
|
42
|
+
* 1. set {@link DRIVE_EPIC_PREFERRED_CONDUCTOR} to the surviving conductor;
|
|
43
|
+
* 2. delete the losing branch in {@link renderConductorHandoff}.
|
|
44
|
+
*
|
|
45
|
+
* No prompt, bundled doc, command mirror, or posture test changes as a result —
|
|
46
|
+
* that is the whole reason the duality is confined to this one subcommand and
|
|
47
|
+
* this one constant. Epics already assigned to the eliminated conductor must be
|
|
48
|
+
* drained first; that drain is a conductor-lifecycle concern outside this file.
|
|
49
|
+
*
|
|
50
|
+
* No MCP tool is registered here. The `tools/list` token budget is deliberately
|
|
51
|
+
* ratcheted down, and a tool would be useless in a bare-chat session anyway,
|
|
52
|
+
* where the model has no tool call to make.
|
|
53
|
+
*/
|
|
54
|
+
import { resolveConductorBridgeApiAccess, fetchConductorReadiness, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
|
|
55
|
+
import { runSetupEpicCli } from "./setup-epic.js";
|
|
56
|
+
// BAPI-806: mcp-identity.ts is the SOLE source of the package-name literal, so
|
|
57
|
+
// the printed invocation interpolates it rather than repeating it.
|
|
58
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
59
|
+
/** Jira epic key shape. Matches `conduct-epic`'s positional rule exactly. */
|
|
60
|
+
const EPIC_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
|
|
61
|
+
/** The two conductor paths. Derived types below come from this constant. */
|
|
62
|
+
export const DRIVE_EPIC_CONDUCTORS = ["v2", "pilot"];
|
|
63
|
+
/**
|
|
64
|
+
* THE consolidation-day edit point.
|
|
65
|
+
*
|
|
66
|
+
* When readiness is green, v2 is preferred and the pilot is the fallback for
|
|
67
|
+
* projects that cannot run it. This is expressed as one named constant rather
|
|
68
|
+
* than as branching scattered through the routing logic precisely so that
|
|
69
|
+
* eliminating a conductor is a one-line change plus one deleted branch.
|
|
70
|
+
*/
|
|
71
|
+
export const DRIVE_EPIC_PREFERRED_CONDUCTOR = "v2";
|
|
72
|
+
/**
|
|
73
|
+
* The v2 readiness predicate, as a list so a failure can name what is missing.
|
|
74
|
+
*
|
|
75
|
+
* These are the facts v2 actually needs to drive an epic: a stored supervisor
|
|
76
|
+
* posture, a reconciler that is ticking, a provisioned and live executor, and
|
|
77
|
+
* complete GitHub App credentials. Each is read from the SERVER's report and
|
|
78
|
+
* never re-derived locally — two implementations of one classification drift,
|
|
79
|
+
* and a CLI that disagreed with the server about whether v2 can run would be
|
|
80
|
+
* worse than no check at all.
|
|
81
|
+
*/
|
|
82
|
+
export const V2_READINESS_REQUIREMENTS = [
|
|
83
|
+
{
|
|
84
|
+
id: "supervisor_setup",
|
|
85
|
+
describe: "supervisor setup stored for this repository",
|
|
86
|
+
satisfied: (r) => r.supervisor.setup_present,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "supervisor_config",
|
|
90
|
+
describe: "supervisor configuration stored for this repository",
|
|
91
|
+
satisfied: (r) => r.supervisor.config_present,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "github_credentials",
|
|
95
|
+
describe: "GitHub App credentials that resolve completely",
|
|
96
|
+
satisfied: (r) => r.github.credentials_complete,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "reconciler_live",
|
|
100
|
+
describe: "a reconciler that is ticking and not stale",
|
|
101
|
+
satisfied: (r) => r.reconciler.liveness_readable && !r.reconciler.stale,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "executor_live",
|
|
105
|
+
describe: "an executor provisioned and reporting ready",
|
|
106
|
+
satisfied: (r) => r.executor.liveness_readable && r.executor.ready === true,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
export function getDriveEpicUsage() {
|
|
110
|
+
return [
|
|
111
|
+
"Usage: mcp-server drive-epic [options] <EPIC>",
|
|
112
|
+
"",
|
|
113
|
+
"Drives one epic with the conductor this project can actually run. Reads",
|
|
114
|
+
"conductor readiness from Bridge API and routes to exactly one path — it",
|
|
115
|
+
"never asks you to choose.",
|
|
116
|
+
"",
|
|
117
|
+
"Arguments:",
|
|
118
|
+
" <EPIC> Jira epic key, matches [A-Z]+-[0-9]+ (e.g. BAPI-885)",
|
|
119
|
+
"",
|
|
120
|
+
"Options:",
|
|
121
|
+
" --plan-file <path> Plan DAG sidecar. When supplied and the v2 path is",
|
|
122
|
+
" selected, drive-epic runs that bootstrap directly",
|
|
123
|
+
" instead of printing the command to run.",
|
|
124
|
+
" --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)",
|
|
125
|
+
" -h, --help Show this help",
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse argv. Rejects malformed input BEFORE any side effect — no readiness
|
|
130
|
+
* read, no credential resolution, no delegation.
|
|
131
|
+
*/
|
|
132
|
+
export function parseDriveEpicArgs(argv) {
|
|
133
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
134
|
+
return { status: "help", usage: getDriveEpicUsage() };
|
|
135
|
+
}
|
|
136
|
+
const positionals = [];
|
|
137
|
+
let planFile;
|
|
138
|
+
let repo;
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
if (arg === "--plan-file" || arg === "--repo") {
|
|
142
|
+
const value = argv[i + 1];
|
|
143
|
+
if (value === undefined || value.startsWith("-")) {
|
|
144
|
+
return { status: "error", message: `${arg} requires a value.` };
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--plan-file")
|
|
147
|
+
planFile = value;
|
|
148
|
+
else
|
|
149
|
+
repo = value;
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const eq = arg.match(/^(--plan-file|--repo)=(.*)$/);
|
|
154
|
+
if (eq) {
|
|
155
|
+
const value = eq[2];
|
|
156
|
+
if (value.trim().length === 0) {
|
|
157
|
+
return { status: "error", message: `${eq[1]} requires a value.` };
|
|
158
|
+
}
|
|
159
|
+
if (eq[1] === "--plan-file")
|
|
160
|
+
planFile = value;
|
|
161
|
+
else
|
|
162
|
+
repo = value;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (arg.startsWith("-")) {
|
|
166
|
+
return { status: "error", message: `Unsupported flag: ${arg}` };
|
|
167
|
+
}
|
|
168
|
+
positionals.push(arg);
|
|
169
|
+
}
|
|
170
|
+
if (positionals.length === 0) {
|
|
171
|
+
return { status: "error", message: "Missing required epic key." };
|
|
172
|
+
}
|
|
173
|
+
if (positionals.length > 1) {
|
|
174
|
+
// Rejected rather than ignored: a second positional means the caller
|
|
175
|
+
// believes something we did not parse, and driving the wrong epic is not
|
|
176
|
+
// recoverable by re-reading the output.
|
|
177
|
+
return {
|
|
178
|
+
status: "error",
|
|
179
|
+
message: `Expected exactly one epic key, got ${positionals.length}: ${positionals.join(", ")}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const epicKey = positionals[0];
|
|
183
|
+
if (!EPIC_KEY_PATTERN.test(epicKey)) {
|
|
184
|
+
return { status: "error", message: `Malformed epic key '${epicKey}'. Expected e.g. BAPI-885.` };
|
|
185
|
+
}
|
|
186
|
+
const options = { epicKey, ...(planFile ? { planFile } : {}), ...(repo ? { repo } : {}) };
|
|
187
|
+
return { status: "ok", options };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Route a readiness report to exactly one conductor.
|
|
191
|
+
*
|
|
192
|
+
* Pure and total: it takes the already-read report and always returns a
|
|
193
|
+
* decision. The unknown case is produced by the caller, which is the only place
|
|
194
|
+
* that can observe a failed read — selection itself never happens by exception
|
|
195
|
+
* handling, and so never needs to report that it could not decide.
|
|
196
|
+
*/
|
|
197
|
+
export function selectConductor(readiness) {
|
|
198
|
+
const missing = V2_READINESS_REQUIREMENTS.filter((req) => !req.satisfied(readiness));
|
|
199
|
+
if (missing.length === 0) {
|
|
200
|
+
return {
|
|
201
|
+
kind: "selected",
|
|
202
|
+
conductor: DRIVE_EPIC_PREFERRED_CONDUCTOR,
|
|
203
|
+
reason: "conductor readiness is green",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
kind: "selected",
|
|
208
|
+
conductor: "pilot",
|
|
209
|
+
reason: `the engine path still needs ${missing.map((m) => m.describe).join("; ")}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Render the selected path and only the selected path.
|
|
214
|
+
*
|
|
215
|
+
* Precondition: the caller has already ruled out delegation. The `v2` branch
|
|
216
|
+
* reads as "green, but no plan file was supplied", which is only true once
|
|
217
|
+
* {@link runDriveEpicCli} has declined to delegate — so route first, render
|
|
218
|
+
* second.
|
|
219
|
+
*
|
|
220
|
+
* CONSOLIDATION DAY: delete the branch of the conductor being eliminated and
|
|
221
|
+
* point {@link DRIVE_EPIC_PREFERRED_CONDUCTOR} at the survivor. Nothing outside
|
|
222
|
+
* this file moves.
|
|
223
|
+
*/
|
|
224
|
+
export function renderConductorHandoff(selection, epicKey) {
|
|
225
|
+
if (selection.conductor === "v2") {
|
|
226
|
+
const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
|
|
227
|
+
return {
|
|
228
|
+
conductor: "v2",
|
|
229
|
+
invocation,
|
|
230
|
+
lines: [
|
|
231
|
+
`${epicKey}: ${selection.reason}, but no plan file was supplied.`,
|
|
232
|
+
"",
|
|
233
|
+
"The engine path needs a plan DAG, and nothing derives one from an epic key:",
|
|
234
|
+
"the dependency edges and per-ticket touched_files it carries do not exist",
|
|
235
|
+
"anywhere else. Produce one, then re-run this command with it.",
|
|
236
|
+
"",
|
|
237
|
+
` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`,
|
|
238
|
+
" sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json.",
|
|
239
|
+
" If the epic's tickets already exist, run emit-conductor-bundle",
|
|
240
|
+
" finalize instead — it resolves the placeholder keys in an existing",
|
|
241
|
+
" sidecar and attaches touched_files.",
|
|
242
|
+
"",
|
|
243
|
+
` 2. ${invocation}`,
|
|
244
|
+
"",
|
|
245
|
+
`That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,
|
|
246
|
+
"something you invoke directly.",
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const invocation = `/loop 5m /conduct-epic ${epicKey}`;
|
|
251
|
+
return {
|
|
252
|
+
conductor: "pilot",
|
|
253
|
+
invocation,
|
|
254
|
+
lines: [
|
|
255
|
+
`${epicKey}: ${selection.reason}. Drive it from an interactive session with:`,
|
|
256
|
+
"",
|
|
257
|
+
` ${invocation}`,
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Tokens that identify a conductor path in user-facing output. Used by the
|
|
263
|
+
* output guard and by its test — one list, so a new spelling cannot be added to
|
|
264
|
+
* the renderer without the guard seeing it.
|
|
265
|
+
*/
|
|
266
|
+
export const CONDUCTOR_INVOCATION_TOKENS = ["setup-epic", "conduct-epic"];
|
|
267
|
+
/** The v2 token, named once so prose and the guard cannot drift apart. */
|
|
268
|
+
const SETUP_EPIC_SUBCOMMAND = CONDUCTOR_INVOCATION_TOKENS[0];
|
|
269
|
+
/**
|
|
270
|
+
* Fail closed if a rendered branch names more than one conductor.
|
|
271
|
+
*
|
|
272
|
+
* This is a guard, not decoration: the whole value of a single entry point is
|
|
273
|
+
* that a reader is never handed a choice, and a well-meaning "you could also
|
|
274
|
+
* try..." line added later would silently reintroduce the dual-authority
|
|
275
|
+
* hazard. Checking on the way out costs nothing and cannot be forgotten.
|
|
276
|
+
*/
|
|
277
|
+
export function assertSingleConductorInvocation(text) {
|
|
278
|
+
const named = CONDUCTOR_INVOCATION_TOKENS.filter((token) => text.includes(token));
|
|
279
|
+
if (named.length > 1) {
|
|
280
|
+
throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). ` +
|
|
281
|
+
"Exactly one path may ever be presented.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export function createDefaultDriveEpicDeps() {
|
|
285
|
+
return {
|
|
286
|
+
resolveAccess: async (repo) => {
|
|
287
|
+
const result = await resolveConductorBridgeApiAccess(repo ? { repoName: repo } : {});
|
|
288
|
+
return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
|
|
289
|
+
},
|
|
290
|
+
readReadiness: (access) => fetchConductorReadiness(access, globalThis.fetch),
|
|
291
|
+
runSetupEpic: (argv) => runSetupEpicCli(argv),
|
|
292
|
+
// stdout is safe here: `drive-epic` is dispatched BEFORE MCP server
|
|
293
|
+
// construction, so nothing has claimed stdout for the protocol transport.
|
|
294
|
+
log: (message) => console.log(message),
|
|
295
|
+
errorLog: (message) => console.error(message),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Run the CLI. Returns `0` on a successful handoff and `1` on any validation,
|
|
300
|
+
* readiness, prerequisite, or execution failure. Never throws: the caller
|
|
301
|
+
* dispatches this at module top level, where an escaping exception would take
|
|
302
|
+
* down the process before any diagnostic reached the operator.
|
|
303
|
+
*/
|
|
304
|
+
export async function runDriveEpicCli(argv, overrides = {}) {
|
|
305
|
+
const deps = { ...createDefaultDriveEpicDeps(), ...overrides };
|
|
306
|
+
const parsed = parseDriveEpicArgs(argv);
|
|
307
|
+
if (parsed.status === "help") {
|
|
308
|
+
deps.log(parsed.usage);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
if (parsed.status === "error") {
|
|
312
|
+
deps.errorLog(parsed.message);
|
|
313
|
+
deps.errorLog("");
|
|
314
|
+
deps.errorLog(getDriveEpicUsage());
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
const { epicKey, planFile, repo } = parsed.options;
|
|
318
|
+
try {
|
|
319
|
+
// --- Resolve access. A missing credential is UNKNOWN, not not-ready. -----
|
|
320
|
+
const accessResult = await deps.resolveAccess(repo);
|
|
321
|
+
if (!accessResult.ok) {
|
|
322
|
+
return escalate(deps, epicKey, `conductor readiness could not be read: ${accessResult.error}`);
|
|
323
|
+
}
|
|
324
|
+
// --- Read readiness. Any failure here is UNKNOWN. ------------------------
|
|
325
|
+
let readiness;
|
|
326
|
+
try {
|
|
327
|
+
readiness = await deps.readReadiness(accessResult.access);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
return escalate(deps, epicKey, `conductor readiness could not be read: ${safeDiagnosticMessage(err, "readiness request failed")}`);
|
|
331
|
+
}
|
|
332
|
+
// --- Route. Exactly one path from here on. -------------------------------
|
|
333
|
+
const selection = selectConductor(readiness);
|
|
334
|
+
// Decide delegation BEFORE rendering. The v2 branch's text is specifically
|
|
335
|
+
// about a missing plan file, and rendering it on the path that has one
|
|
336
|
+
// would produce a message contradicting what is about to happen.
|
|
337
|
+
if (selection.conductor === "v2" && planFile) {
|
|
338
|
+
// Delegate. `setup-epic` owns branch, run-row, plan, and approval
|
|
339
|
+
// behavior; drive-epic reimplements none of it and prints nothing of its
|
|
340
|
+
// own alongside it, so its output remains the only conductor output.
|
|
341
|
+
const setupArgv = ["--epic-key", epicKey, "--plan-file", planFile, ...(repo ? ["--repo", repo] : [])];
|
|
342
|
+
return await deps.runSetupEpic(setupArgv);
|
|
343
|
+
}
|
|
344
|
+
const handoff = renderConductorHandoff(selection, epicKey);
|
|
345
|
+
const text = handoff.lines.join("\n");
|
|
346
|
+
assertSingleConductorInvocation(text);
|
|
347
|
+
deps.log(text);
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
// Last resort. Sanitized, on stderr, converted to an exit code — a
|
|
352
|
+
// credential must never reach this line, and a stringified unknown is
|
|
353
|
+
// exactly how one escapes.
|
|
354
|
+
deps.errorLog(`drive-epic failed: ${safeDiagnosticMessage(err, "unexpected error")}`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The escalation branch. Prints NO conductor invocation at all — naming a
|
|
360
|
+
* fallback here is the dual-authority hazard, and an operator who does not know
|
|
361
|
+
* which authority owns an epic must not be nudged toward either one.
|
|
362
|
+
*/
|
|
363
|
+
function escalate(deps, epicKey, reason) {
|
|
364
|
+
const lines = [
|
|
365
|
+
`Cannot determine which conductor owns ${epicKey}: ${reason}`,
|
|
366
|
+
"",
|
|
367
|
+
"This is not a not-ready result — it is an unknown one, and starting the wrong",
|
|
368
|
+
"conductor on an epic wedges it permanently. Restore Bridge API access and run",
|
|
369
|
+
"drive-epic again, or ask an operator to resolve conductor readiness.",
|
|
370
|
+
];
|
|
371
|
+
const text = lines.join("\n");
|
|
372
|
+
assertSingleConductorInvocation(text);
|
|
373
|
+
deps.errorLog(text);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
@@ -34,6 +34,45 @@ function parseStopRequested(bodyText) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The claim response header carrying server-derived dispatcher liveness
|
|
39
|
+
* (BAPI-871). Exported so the client and its tests name it once.
|
|
40
|
+
*/
|
|
41
|
+
export const RECONCILER_LIVENESS_HEADER = "X-BAPI-Reconciler-Liveness";
|
|
42
|
+
/** The only values the server is documented to send. */
|
|
43
|
+
const RECONCILER_LIVENESS_VALUES = [
|
|
44
|
+
"fresh",
|
|
45
|
+
"stale",
|
|
46
|
+
"never_seen",
|
|
47
|
+
"unknown",
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Read the liveness header off a claim response, admitting ONLY documented
|
|
51
|
+
* values.
|
|
52
|
+
*
|
|
53
|
+
* Absent, blank, or unrecognized all map to `unknown` — the compatibility path.
|
|
54
|
+
* An executor talking to a backend from before this header existed must keep
|
|
55
|
+
* claiming exactly as it did, and "I have no verdict" is what `unknown` means.
|
|
56
|
+
* A value the server never promised is treated the same way rather than being
|
|
57
|
+
* passed through: an unrecognized string reaching the runner's availability
|
|
58
|
+
* logic could silently become a fifth state nobody handles.
|
|
59
|
+
*
|
|
60
|
+
* Header lookup is case-insensitive via `Headers.get`, which is required by the
|
|
61
|
+
* spec — HTTP header names are not case-sensitive, and proxies do re-case them.
|
|
62
|
+
*/
|
|
63
|
+
export function parseReconcilerLivenessHeader(headers) {
|
|
64
|
+
let raw = null;
|
|
65
|
+
try {
|
|
66
|
+
raw = headers.get(RECONCILER_LIVENESS_HEADER);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
71
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
72
|
+
return RECONCILER_LIVENESS_VALUES.includes(value)
|
|
73
|
+
? value
|
|
74
|
+
: "unknown";
|
|
75
|
+
}
|
|
37
76
|
/**
|
|
38
77
|
* Map a mutation (heartbeat/complete/fail) HTTP response onto the fencing
|
|
39
78
|
* outcome. `allowInvalidResult` maps 422 to `invalid_job_result` (complete only).
|
|
@@ -68,7 +107,9 @@ export function createExecutorHttpClient(config) {
|
|
|
68
107
|
body: JSON.stringify(body),
|
|
69
108
|
});
|
|
70
109
|
const text = await res.text();
|
|
71
|
-
|
|
110
|
+
// `headers` is surfaced (optional, so the many response fakes that omit it
|
|
111
|
+
// stay assignable) purely for the claim path's liveness annotation.
|
|
112
|
+
return { status: res.status, text, headers: res.headers };
|
|
72
113
|
}
|
|
73
114
|
async function mutate(pathSuffix, body, allowInvalidResult, repoName) {
|
|
74
115
|
try {
|
|
@@ -85,24 +126,33 @@ export function createExecutorHttpClient(config) {
|
|
|
85
126
|
async claim(manifest) {
|
|
86
127
|
let status;
|
|
87
128
|
let text;
|
|
129
|
+
let reconcilerLiveness = "unknown";
|
|
88
130
|
try {
|
|
89
131
|
const res = await post("/claim", manifest, manifest.repo_name);
|
|
90
132
|
status = res.status;
|
|
91
133
|
text = res.text;
|
|
134
|
+
// Parsed on EVERY response, then attached only to the successful
|
|
135
|
+
// variants below. A response fake that omits `headers` degrades to
|
|
136
|
+
// `unknown` rather than throwing.
|
|
137
|
+
if (res.headers)
|
|
138
|
+
reconcilerLiveness = parseReconcilerLivenessHeader(res.headers);
|
|
92
139
|
}
|
|
93
140
|
catch (err) {
|
|
94
141
|
const message = err instanceof Error ? err.message : String(err);
|
|
95
142
|
return { kind: "retryable", error: `claim network error: ${boundedDetail(message)}` };
|
|
96
143
|
}
|
|
144
|
+
// Both successful outcomes carry the verdict. Nothing about the liveness
|
|
145
|
+
// value changes the classification below: a 204 is still "no job", and the
|
|
146
|
+
// retryable/fatal split is untouched.
|
|
97
147
|
if (status === 204)
|
|
98
|
-
return { kind: "none" };
|
|
148
|
+
return { kind: "none", reconcilerLiveness };
|
|
99
149
|
if (status === 200) {
|
|
100
150
|
try {
|
|
101
151
|
const job = JSON.parse(text);
|
|
102
152
|
if (!job || typeof job !== "object" || typeof job.claim_token !== "string") {
|
|
103
153
|
return { kind: "fatal", error: "claim returned a malformed job payload" };
|
|
104
154
|
}
|
|
105
|
-
return { kind: "claimed", job };
|
|
155
|
+
return { kind: "claimed", job, reconcilerLiveness };
|
|
106
156
|
}
|
|
107
157
|
catch {
|
|
108
158
|
return { kind: "fatal", error: "claim returned invalid JSON" };
|
|
@@ -142,6 +192,24 @@ export function createExecutorHttpClient(config) {
|
|
|
142
192
|
result.stop_requested = true;
|
|
143
193
|
return result;
|
|
144
194
|
},
|
|
195
|
+
/**
|
|
196
|
+
* Publish per-process liveness (BAPI-871).
|
|
197
|
+
*
|
|
198
|
+
* Authenticated with the manifest's PRIMARY repo key. The server verifies
|
|
199
|
+
* every entry in `repo_names` before writing anything, so a key that does not
|
|
200
|
+
* hold one of them gets a 4xx — reported here as `failed` and nothing more.
|
|
201
|
+
* Delivery is non-critical monitoring: it never throws, never classifies a
|
|
202
|
+
* fencing outcome, and never stops the caller from claiming or running work.
|
|
203
|
+
*/
|
|
204
|
+
async processHeartbeat(request) {
|
|
205
|
+
try {
|
|
206
|
+
const { status, text } = await post("/process-heartbeat", request, request.repo_names[0] ?? "");
|
|
207
|
+
return status >= 200 && status < 300 && isOkBody(text) ? "delivered" : "failed";
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return "failed";
|
|
211
|
+
}
|
|
212
|
+
},
|
|
145
213
|
complete(job, completion) {
|
|
146
214
|
return mutate(`/${job.id}/complete`, {
|
|
147
215
|
repo_name: job.repo_name,
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* opened a PR whose base branch is not the run base, so `claude-review.yml`
|
|
21
21
|
* (triggered only on `branches: [main]`) never fires and the `code_review`
|
|
22
22
|
* gate would hang forever; see `worker-finalization.ts`.
|
|
23
|
+
* - `WorkerFinalizationPrNotAttached` — an implementation-style job (BAPI-862)
|
|
24
|
+
* exited cleanly and its exact HEAD is durable on origin, but no usable pull
|
|
25
|
+
* request exists for that branch: none is visible, its URL is blank, its
|
|
26
|
+
* description is blank (BAPI-824 folded in), or it does not yet carry the
|
|
27
|
+
* pushed head. The work is COMPLETE and PUSHED — only the attachment is
|
|
28
|
+
* missing — so this kind is deliberately distinct from every other
|
|
29
|
+
* finalization failure, and its guidance names attachment and warns against
|
|
30
|
+
* deleting or rebasing the branch; see `worker-finalization.ts`.
|
|
23
31
|
* - `WorkerFinalizationSavedButUnfinalized` — an implementation-style job
|
|
24
32
|
* (BAPI-762) exited cleanly, and an origin branch exists at an authoritative
|
|
25
33
|
* tip that does NOT match the worker's own HEAD — durable work exists on
|
|
@@ -98,6 +106,7 @@ export const WorktreeLostBeforePush = "WorktreeLostBeforePush";
|
|
|
98
106
|
export const BranchMismatch = "BranchMismatch";
|
|
99
107
|
export const WorkerFinalizationMissingRemoteBranchAndPr = "WorkerFinalizationMissingRemoteBranchAndPr";
|
|
100
108
|
export const WorkerFinalizationPrBaseMismatch = "WorkerFinalizationPrBaseMismatch";
|
|
109
|
+
export const WorkerFinalizationPrNotAttached = "WorkerFinalizationPrNotAttached";
|
|
101
110
|
export const WorkerFinalizationSavedButUnfinalized = "WorkerFinalizationSavedButUnfinalized";
|
|
102
111
|
export const WorkerConfigIsolation = "ContractError.WorkerConfigIsolation";
|
|
103
112
|
export const PreSpawnVerification = "ContractError.PreSpawnVerification";
|