@bridge_gpt/mcp-server 0.2.36 → 0.2.38
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 +48 -8
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +70 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +185 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +57 -34
- package/build/sfcc/reads-site-preference.js +86 -33
- package/build/sfcc/reads-system-object.js +50 -38
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +71 -15
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- package/pipelines/implement-ticket.json +15 -5
package/README.md
CHANGED
|
@@ -397,7 +397,7 @@ An entry with no trailing `serve` still starts the server — bare invocation me
|
|
|
397
397
|
<summary><strong>Upgrading Bridge</strong></summary>
|
|
398
398
|
|
|
399
399
|
```bash
|
|
400
|
-
npx -y @bridge_gpt/mcp-server upgrade
|
|
400
|
+
npx -y @bridge_gpt/mcp-server@latest --upgrade
|
|
401
401
|
```
|
|
402
402
|
|
|
403
403
|
`upgrade` fetches the latest published version, refreshes your scaffolded slash
|
|
@@ -405,8 +405,34 @@ commands, agents, and pipelines, updates the version pin in your MCP config, and
|
|
|
405
405
|
opens a session so you can reconnect. It is also available as the
|
|
406
406
|
`/upgrade-bridge` slash command.
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
Use the `@latest` form. It applies to the short-lived *upgrader* process: without
|
|
409
|
+
it, npx may reuse a cached older copy of the package and "upgrade" you with the
|
|
410
|
+
build you are trying to replace. The exact `MAJOR.MINOR.PATCH` pin the upgrader
|
|
411
|
+
writes into your MCP config is deliberately different — host configs stay pinned
|
|
412
|
+
to an exact release so a project's server is reproducible.
|
|
413
|
+
|
|
414
|
+
`upgrade` reports **per config file**, because a project can have several
|
|
415
|
+
(`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) and they can disagree:
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
Launcher pins:
|
|
419
|
+
.mcp.json: 0.2.16 -> 0.2.36
|
|
420
|
+
.cursor/mcp.json: already 0.2.36
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
When every applicable launcher pin was already at the target, it prints
|
|
424
|
+
`Already up-to-date.` — that status comes from comparing your configs, not from
|
|
425
|
+
the version of the CLI process. A non-zero exit means the upgrade did **not**
|
|
426
|
+
converge (an unreadable config, a launcher carrying a version range the upgrader
|
|
427
|
+
must not rewrite, a competing local install it could not remove, or a pin that
|
|
428
|
+
failed post-write verification); nothing is reported as complete in that case.
|
|
429
|
+
|
|
430
|
+
The server checks for updates on startup. The check is cached for a day and never
|
|
431
|
+
blocks startup. When a newer version is known, it surfaces in two places you do
|
|
432
|
+
not have to go looking for: a one-line warning on the server's **stderr**, and a
|
|
433
|
+
short advisory attached to the ordinary `tools/list` response so the agent in the
|
|
434
|
+
session can see that some tools may be missing or renamed in the older build.
|
|
435
|
+
Neither requires calling `ping` or `doctor`.
|
|
410
436
|
|
|
411
437
|
Re-running `install` on an already-configured project is safe: it refreshes the
|
|
412
438
|
scaffolded files without overwriting your stored credential unless you pass
|
|
@@ -559,6 +585,12 @@ Good to know, but not needed every day.
|
|
|
559
585
|
- **How to use it:** Ask your agent to resume the full-automation chain, naming the run to continue.
|
|
560
586
|
- **Flags:** None.
|
|
561
587
|
|
|
588
|
+
**15. Update Ticket Description**
|
|
589
|
+
- **What it does:** Rewrites a ticket's description with AI, using the ticket's own content and its reference material. A rewrite that changes more than 60% of the description is held for review instead of applied.
|
|
590
|
+
- **When it's useful:** (Refinement) When a ticket has accumulated comments, attachments, or links and its description no longer reflects them.
|
|
591
|
+
- **How to use it:** Ask your agent — "Update the description for BAPI-123."
|
|
592
|
+
- **Flags:** None. Poll the ticket's state for the outcome; if the update was held for review, read the proposal instead of applying it blind.
|
|
593
|
+
|
|
562
594
|
### Now and then
|
|
563
595
|
|
|
564
596
|
Useful once in a while.
|
|
@@ -611,7 +643,13 @@ Useful once in a while.
|
|
|
611
643
|
- **How to use it:** Ask your agent — "Download the design mockups attached to BAPI-123 into my docs folder," or "Attach build-log.txt to BAPI-123."
|
|
612
644
|
- **Flags:** Choose the direction (download from the ticket, or upload to it) and, for a download, where the files should land.
|
|
613
645
|
|
|
614
|
-
**9. Estimate
|
|
646
|
+
**9. Estimate Ticket**
|
|
647
|
+
- **What it does:** Estimates the development effort for one ticket. Use Estimate Epic instead for a whole epic or a named group of tickets.
|
|
648
|
+
- **When it's useful:** (Refinement) When you need a size for a single ticket before committing to it.
|
|
649
|
+
- **How to use it:** Ask your agent — "Estimate BAPI-123."
|
|
650
|
+
- **Flags:** Ask for a fresh estimate to regenerate rather than reuse a stored one.
|
|
651
|
+
|
|
652
|
+
**10. Estimate Epic**
|
|
615
653
|
- **What it does:** Estimates an epic, or an explicit group of tickets you name.
|
|
616
654
|
- **When it's useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.
|
|
617
655
|
- **How to use it:** `/estimate-epic BAPI-123`
|
|
@@ -756,7 +794,9 @@ Without this, only the diagnostic tools are registered.
|
|
|
756
794
|
|
|
757
795
|
### Tools
|
|
758
796
|
|
|
759
|
-
All SFCC tools are read-only and target a developer sandbox.
|
|
797
|
+
All SFCC tools are read-only and target a developer sandbox. An oversized response is saved in full to `BAPI_DOCS_DIR/sfcc/` and replaced by a parseable JSON descriptor — `truncated: true`, the `saved_path` it was written to, and the `page` metadata (`returned`, `total` when OCAPI supplied one, `has_more`) — so the collection metadata survives even though the data itself is on disk. If that save fails, the complete payload is returned inline instead, still as parseable JSON.
|
|
798
|
+
|
|
799
|
+
Attribute-definition reads and writes can return an attribute's `default_value` at `projection: "full"`, and Bridge withholds it — every key is preserved except that one, whose value becomes `[REDACTED_BY_BRIDGE]` — from the inline response, the saved file, and a successful write echo alike. Attribute defaults are intentionally unavailable through this MCP surface; Business Manager is the supported path to read one.
|
|
760
800
|
|
|
761
801
|
**Diagnostics** (always available, no profile needed)
|
|
762
802
|
- `sfcc_setup_status` — report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.
|
|
@@ -764,12 +804,12 @@ All SFCC tools are read-only and target a developer sandbox. Oversized responses
|
|
|
764
804
|
|
|
765
805
|
**System object model** (needs the `sfcc` profile)
|
|
766
806
|
- `system_object_list` — list system object types (Product, Order, Customer, …).
|
|
767
|
-
- `system_object_get` — fetch one type's definition
|
|
807
|
+
- `system_object_get` — fetch one system object type's definition.
|
|
768
808
|
- `system_object_attribute_search` — search a type's attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.
|
|
769
809
|
|
|
770
810
|
**Custom object definitions** (needs the `sfcc` profile)
|
|
771
|
-
- `custom_object_definition_attributes_get` — fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object
|
|
772
|
-
- `custom_object_definition_attribute_search` — search attribute definitions within a known custom object type. Read-only — creating a custom object *type* isn't possible via OCAPI; that's a future v2 metadata-import capability.
|
|
811
|
+
- `custom_object_definition_attributes_get` — fetch attribute definitions for a known custom object type (`default_value` withheld). OCAPI cannot enumerate custom object type *IDs* directly, so `object_type` must be known — but it is discoverable: call `system_object_list` at `projection: "full"` for each custom type's `display_name` and `attribute_definition_count`, derive a candidate id (e.g. strip spaces from `"Product Quality Result"` → `ProductQualityResult`), and confirm it by checking that this tool's returned attribute count matches that row's `attribute_definition_count`.
|
|
812
|
+
- `custom_object_definition_attribute_search` — search attribute definitions within a known custom object type (`default_value` withheld). Read-only — creating a custom object *type* isn't possible via OCAPI; that's a future v2 metadata-import capability. Same discovery path as above applies to `object_type`.
|
|
773
813
|
- `custom_object_definition_attribute_create` — **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.
|
|
774
814
|
- `custom_object_definition_attribute_update` — **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH …/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.
|
|
775
815
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Bridge API base-URL normalization and validation (BAPI-727).
|
|
3
|
+
*
|
|
4
|
+
* Extracted so the executor's credential resolution and the `mcp-invoke` shim can
|
|
5
|
+
* share ONE definition of "normalized base URL" without `mcp-invoke` importing
|
|
6
|
+
* executor runtime behavior (`executor/credentials.ts` pulls in the executor deps
|
|
7
|
+
* types and the credential store, neither of which the shim should depend on).
|
|
8
|
+
*
|
|
9
|
+
* Everything here is pure: no filesystem, no process, no network, no environment.
|
|
10
|
+
* Validation messages name the flag and the reason ONLY — never the supplied URL,
|
|
11
|
+
* which can carry embedded credentials in its userinfo component.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Trim, strip every trailing slash, and treat an empty result as absent.
|
|
15
|
+
*
|
|
16
|
+
* This is the executor's long-standing normalization, moved verbatim so the value
|
|
17
|
+
* a worker registration carries is byte-identical to the one the executor itself
|
|
18
|
+
* uses for its HTTP calls.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeBaseUrlCandidate(raw) {
|
|
21
|
+
if (typeof raw !== "string")
|
|
22
|
+
return null;
|
|
23
|
+
const normalized = raw.trim().replace(/\/+$/, "");
|
|
24
|
+
return normalized.length > 0 ? normalized : null;
|
|
25
|
+
}
|
|
26
|
+
/** Fixed, secret-free explanation for each rejection reason. */
|
|
27
|
+
const REJECTION_TEXT = {
|
|
28
|
+
empty: "value is empty",
|
|
29
|
+
"not-absolute-http-url": "value is not an absolute URL",
|
|
30
|
+
"unsupported-scheme": "only http: and https: URLs are supported",
|
|
31
|
+
"missing-hostname": "URL has no hostname",
|
|
32
|
+
"embedded-credentials": "URL must not embed a username or password",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Human-readable rejection message for a flag. Interpolates only the flag name and
|
|
36
|
+
* a fixed reason — deliberately NOT the rejected value, so a URL containing
|
|
37
|
+
* `https://user:secret@host` can never reach an error string, usage text, or log.
|
|
38
|
+
*/
|
|
39
|
+
export function describeBaseUrlRejection(flag, reason) {
|
|
40
|
+
return `Invalid ${flag}: ${REJECTION_TEXT[reason]}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Strictly validate a base URL supplied on a command line: it must be a non-empty,
|
|
44
|
+
* absolute `http:`/`https:` URL with a hostname and no embedded user information.
|
|
45
|
+
* The accepted value is returned normalized by
|
|
46
|
+
* {@link normalizeBaseUrlCandidate} so it matches the executor's own form.
|
|
47
|
+
*/
|
|
48
|
+
export function validateHttpBaseUrl(raw) {
|
|
49
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
50
|
+
return { ok: false, reason: "empty" };
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = new URL(raw.trim());
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// A relative value ("/api", "api.example.test/x") lands here: the WHATWG URL
|
|
58
|
+
// constructor requires an absolute URL when no base is supplied.
|
|
59
|
+
return { ok: false, reason: "not-absolute-http-url" };
|
|
60
|
+
}
|
|
61
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
62
|
+
return { ok: false, reason: "unsupported-scheme" };
|
|
63
|
+
}
|
|
64
|
+
// Defensive: for http/https the WHATWG parser already rejects an empty
|
|
65
|
+
// authority above (`http://` throws), so this branch is not reachable through
|
|
66
|
+
// those schemes today. It is kept because "must have a hostname" is the real
|
|
67
|
+
// invariant, and relying on a parser implementation detail to enforce it would
|
|
68
|
+
// be fragile.
|
|
69
|
+
if (parsed.hostname.length === 0) {
|
|
70
|
+
return { ok: false, reason: "missing-hostname" };
|
|
71
|
+
}
|
|
72
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
73
|
+
return { ok: false, reason: "embedded-credentials" };
|
|
74
|
+
}
|
|
75
|
+
const normalized = normalizeBaseUrlCandidate(raw);
|
|
76
|
+
if (normalized === null)
|
|
77
|
+
return { ok: false, reason: "empty" };
|
|
78
|
+
return { ok: true, baseUrl: normalized };
|
|
79
|
+
}
|
package/build/bridge-api-urls.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The base URL is supplied by the caller (never hardcoded here) and its
|
|
11
11
|
* trailing slashes are trimmed so no doubled slash is produced.
|
|
12
|
+
*
|
|
13
|
+
* The `/jira` segment is the Bridge API's TRANSPORT namespace — the router mount
|
|
14
|
+
* that serves these routes in BOTH ticket backend modes. It is not, and must not
|
|
15
|
+
* become, an agent-visible ticket link: a ticket's URL is whatever the server
|
|
16
|
+
* returns on the create/read response (`https://…` in Jira mode,
|
|
17
|
+
* `local://tickets/…` in local mode) and is passed through verbatim. Renaming
|
|
18
|
+
* this mount is explicitly out of scope (no renaming of Jira-shaped surfaces),
|
|
19
|
+
* and synthesizing a ticket URL from it would produce a link to an API route
|
|
20
|
+
* rather than to a ticket.
|
|
12
21
|
*/
|
|
13
22
|
/**
|
|
14
23
|
* Create the URL builders bound to `baseUrl`. Trailing slashes on the base are
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { runPipeline, resumePipeline, peekPipelineRun } from "./pipeline-orchestrator.js";
|
|
27
27
|
import { toStringVariable, substituteChainValue } from "./chain-utils.js";
|
|
28
|
+
import { isValidTicketKey, validateTicketKeys } from "./ticket-key-utils.js";
|
|
29
|
+
import { isUnsupportedInLocalModeEnvelope, parseToolErrorEnvelope, } from "./tool-error-envelope.js";
|
|
28
30
|
const CHAIN_NAME = "full-automation";
|
|
29
31
|
const START_TICKETS_PIPELINE = "start-tickets";
|
|
30
32
|
// Display titles for preambles, keyed by child pipeline name.
|
|
@@ -166,13 +168,22 @@ function normalizeAutoApprove(value) {
|
|
|
166
168
|
return true;
|
|
167
169
|
return false;
|
|
168
170
|
}
|
|
169
|
-
const JIRA_KEY_EXACT = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
170
171
|
/**
|
|
171
|
-
* Extract created
|
|
172
|
+
* Extract created ticket keys from a STRUCTURED payload (an object/array
|
|
172
173
|
* holding `created_ticket_keys` / `ticket_key` / etc.). Collects values from the
|
|
173
|
-
* preferred key fields in a fixed preference order, validates each against
|
|
174
|
-
* `
|
|
175
|
-
*
|
|
174
|
+
* preferred key fields in a fixed preference order, validates each against the
|
|
175
|
+
* canonical `TICKET_KEY_PATTERN`, and de-dupes in encounter order.
|
|
176
|
+
*
|
|
177
|
+
* BAPI-710: the validation used to run against a private `/^[A-Z][A-Z0-9]+-\d+$/`
|
|
178
|
+
* that rejected keys the server mints (`MY_PROJ-248`, `A-1`), and this helper
|
|
179
|
+
* DROPPED every non-match silently. Silent dropping is now impossible for the
|
|
180
|
+
* chain — {@link extractCreatedTicketPayload} rejects the whole payload and names
|
|
181
|
+
* every offending key. This flat helper still filters (its `string[]` contract
|
|
182
|
+
* has no failure channel), but it is a non-chain compatibility path only and the
|
|
183
|
+
* grammar it filters on is now the correct one.
|
|
184
|
+
*
|
|
185
|
+
* There is deliberately NO free-text regex fallback: pass the parsed
|
|
186
|
+
* upload-step payload (via
|
|
176
187
|
* `extractCreatedKeysFromCompletedChild`), never a whole pipeline transcript, so
|
|
177
188
|
* keys merely *mentioned* by an earlier step (e.g. duplicate-detection search
|
|
178
189
|
* hits) can never be mistaken for this run's creations. A string / no-field
|
|
@@ -226,7 +237,7 @@ export function extractCreatedTicketKeys(source) {
|
|
|
226
237
|
const ordered = [];
|
|
227
238
|
for (const key of preferredKeys) {
|
|
228
239
|
for (const candidate of buckets[key]) {
|
|
229
|
-
if (
|
|
240
|
+
if (isValidTicketKey(candidate))
|
|
230
241
|
ordered.push(candidate);
|
|
231
242
|
}
|
|
232
243
|
}
|
|
@@ -278,7 +289,7 @@ function tryParseJson(raw) {
|
|
|
278
289
|
}
|
|
279
290
|
}
|
|
280
291
|
function isValidJiraKey(value) {
|
|
281
|
-
return
|
|
292
|
+
return isValidTicketKey(value);
|
|
282
293
|
}
|
|
283
294
|
/**
|
|
284
295
|
* Normalize a PARSED, STRUCTURED upload payload into a {@link CreatedTicketPayload}.
|
|
@@ -316,7 +327,7 @@ export function extractCreatedTicketPayload(source) {
|
|
|
316
327
|
if (!isValidJiraKey(parent)) {
|
|
317
328
|
return {
|
|
318
329
|
ok: false,
|
|
319
|
-
error: `Epic payload "epic_parent_key" must be a valid
|
|
330
|
+
error: `Epic payload "epic_parent_key" must be a valid ticket key; got ${JSON.stringify(parent)}.`,
|
|
320
331
|
};
|
|
321
332
|
}
|
|
322
333
|
const rawChildren = obj.child_ticket_keys;
|
|
@@ -327,14 +338,20 @@ export function extractCreatedTicketPayload(source) {
|
|
|
327
338
|
"implementable child ticket key.",
|
|
328
339
|
};
|
|
329
340
|
}
|
|
341
|
+
// Validate the WHOLE child list first and report every rejected key in one
|
|
342
|
+
// error (BAPI-710). Failing on the first bad key made a caller fix its
|
|
343
|
+
// payload one key per round trip, and the previous grammar rejected keys the
|
|
344
|
+
// server legitimately mints — so a mixed-validity list must fail atomically,
|
|
345
|
+
// never advance with the valid subset.
|
|
346
|
+
const childKeyCheck = validateTicketKeys(rawChildren, "child_ticket_keys entry");
|
|
347
|
+
if (!childKeyCheck.ok) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
error: `Epic payload "child_ticket_keys" must contain only valid implementable ticket keys. ${childKeyCheck.message}`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
330
353
|
const children = [];
|
|
331
354
|
for (const c of rawChildren) {
|
|
332
|
-
if (!isValidJiraKey(c)) {
|
|
333
|
-
return {
|
|
334
|
-
ok: false,
|
|
335
|
-
error: `Epic payload "child_ticket_keys" must contain only valid implementable Jira keys; got ${JSON.stringify(c)}.`,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
355
|
if (c === parent) {
|
|
339
356
|
return {
|
|
340
357
|
ok: false,
|
|
@@ -361,7 +378,7 @@ export function extractCreatedTicketPayload(source) {
|
|
|
361
378
|
return {
|
|
362
379
|
ok: false,
|
|
363
380
|
error: 'Single-ticket payload "created_ticket_keys" must contain exactly ' +
|
|
364
|
-
"one valid implementable
|
|
381
|
+
"one valid implementable ticket key. A multi-key list is malformed " +
|
|
365
382
|
'(the legacy "first key is the Epic parent" convention is not accepted).',
|
|
366
383
|
};
|
|
367
384
|
}
|
|
@@ -431,6 +448,36 @@ function failedEnvelopeMentions(env, token) {
|
|
|
431
448
|
}
|
|
432
449
|
return false;
|
|
433
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Find a recognized ``UNSUPPORTED_IN_LOCAL_MODE`` refusal anywhere in a child
|
|
453
|
+
* pipeline envelope (BAPI-710).
|
|
454
|
+
*
|
|
455
|
+
* Scans the top-level halt error and each step's ``error`` / string ``result``,
|
|
456
|
+
* matching only the exact ``(error, status)`` pair via the strict parser — never
|
|
457
|
+
* a substring of prose, so an echoed input or a message that merely mentions the
|
|
458
|
+
* marker cannot terminate a chain.
|
|
459
|
+
*
|
|
460
|
+
* A ``completed`` envelope is scanned too, not just a ``failed`` one: a
|
|
461
|
+
* ``warn_and_continue`` step in an older persisted run can have swallowed the
|
|
462
|
+
* refusal into a completed envelope, and treating that as success is exactly the
|
|
463
|
+
* partial-success outcome this check exists to prevent.
|
|
464
|
+
*/
|
|
465
|
+
function findUnsupportedLocalModeRefusal(env) {
|
|
466
|
+
const topLevel = parseToolErrorEnvelope(env.error);
|
|
467
|
+
if (isUnsupportedInLocalModeEnvelope(topLevel))
|
|
468
|
+
return topLevel;
|
|
469
|
+
const results = env.results;
|
|
470
|
+
if (!Array.isArray(results))
|
|
471
|
+
return null;
|
|
472
|
+
for (const step of results) {
|
|
473
|
+
for (const candidate of [step?.error, step?.result]) {
|
|
474
|
+
const parsed = parseToolErrorEnvelope(candidate);
|
|
475
|
+
if (isUnsupportedInLocalModeEnvelope(parsed))
|
|
476
|
+
return parsed;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
434
481
|
/**
|
|
435
482
|
* Heuristic, no-LLM one-line stage summary. idea-to-ticket passes a structured
|
|
436
483
|
* {@link CreatedTicketPayload} so the summary distinguishes the Epic parent from
|
|
@@ -782,6 +829,37 @@ async function handleChildPipelineEnvelope(persistence, recipe, row, childEnv, o
|
|
|
782
829
|
}),
|
|
783
830
|
};
|
|
784
831
|
}
|
|
832
|
+
// BAPI-710 terminal gate. Runs BEFORE the failed-mapping branch, before the
|
|
833
|
+
// fan-out advance, and before the single-child finalize — i.e. before review
|
|
834
|
+
// fan-out, worktree preparation, or any start-tickets command can be emitted.
|
|
835
|
+
// A backend that cannot perform the operation will not start being able to on
|
|
836
|
+
// the next stage, so continuing would only produce a partial success that
|
|
837
|
+
// looks like a real one.
|
|
838
|
+
const refusal = findUnsupportedLocalModeRefusal(childEnv);
|
|
839
|
+
if (refusal) {
|
|
840
|
+
const stages = cloneStages(row.stages);
|
|
841
|
+
if (stages[idx])
|
|
842
|
+
stages[idx].status = "failed";
|
|
843
|
+
try {
|
|
844
|
+
await persistence.patchRun(row.chain_run_id, { stages, status: "failed" });
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
// best-effort — the terminal failure is what matters
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
kind: "fail",
|
|
851
|
+
envelope: {
|
|
852
|
+
...failedEnvelope("TOOL_ERROR", refusal.message, {
|
|
853
|
+
chain_run_id: row.chain_run_id,
|
|
854
|
+
chain_stage: stageRecipe.pipeline_name,
|
|
855
|
+
chain_step: idx + 1,
|
|
856
|
+
chain_total: total,
|
|
857
|
+
}),
|
|
858
|
+
backend_error: refusal.error,
|
|
859
|
+
...(refusal.supported_in ? { supported_in: refusal.supported_in } : {}),
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
785
863
|
if (childEnv.status === "failed") {
|
|
786
864
|
// Stage 0 vagueness or no-keys produced => VALIDATION. Anchor the sentinel
|
|
787
865
|
// check to the failure-text fields (the halt error + per-step error/result
|