@bridge_gpt/mcp-server 0.2.56 → 0.2.58

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.
@@ -58,9 +58,11 @@ import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copy
58
58
  import os from "node:os";
59
59
  import { createHash } from "node:crypto";
60
60
  import path from "path";
61
+ import { fileURLToPath } from "node:url";
62
+ import { validateHttpBaseUrl } from "./base-url.js";
61
63
  import { VERSION } from "./version.generated.js";
62
64
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
63
- import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
65
+ import { LEGACY_SERVER_NAMES, MCP_PACKAGE_NAME, MCP_SERVER_NAME } from "./mcp-identity.js";
64
66
  // BAPI-941: the shared drift diagnostic formatter. The leaf drift module imports
65
67
  // nothing from here, so this direction stays acyclic.
66
68
  import { formatClaudeReviewWorkflowDriftDiagnostic } from "./claude-review-workflow-drift.js";
@@ -560,7 +562,7 @@ resumeMode = false, workflow = "implement", reviewRounds,
560
562
  // conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
561
563
  // resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
562
564
  // command.
563
- baseBranch) {
565
+ baseBranch, bridgeApiBaseUrlOverride) {
564
566
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
565
567
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
566
568
  }
@@ -571,6 +573,7 @@ baseBranch) {
571
573
  // conductor-only — a plain worker's prompt stays bare and learns the base from
572
574
  // the env plus the create-PR instruction files.
573
575
  const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
576
+ const bridgeApiBaseUrl = bridgeApiBaseUrlOverride ?? resolveSpawnBridgeApiBaseUrl(deps.env);
574
577
  return {
575
578
  ok: true,
576
579
  config: {
@@ -580,7 +583,7 @@ baseBranch) {
580
583
  // falls back to the basename-derived repo name (the 403 root cause), and
581
584
  // (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
582
585
  // against it. The base assignment stays outermost.
583
- buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs), repoName, platform), prBaseBranch, platform),
586
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl), repoName, platform), prBaseBranch, platform),
584
587
  spawnTerminalTab: deps.spawnTerminalTab,
585
588
  },
586
589
  };
@@ -610,6 +613,51 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
610
613
  }
611
614
  return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
612
615
  }
616
+ /** Carry the parent's non-secret Bridge endpoint into a newly opened terminal. */
617
+ export function prependBridgeApiBaseUrlEnvAssignment(command, baseUrl, platform = "darwin") {
618
+ if (!baseUrl)
619
+ return command;
620
+ if (platform === "win32") {
621
+ return `$env:BAPI_BASE_URL = ${powershellSquote(baseUrl)}; ${command}`;
622
+ }
623
+ return `export BAPI_BASE_URL='${shSquoteInner(baseUrl)}' && ${command}`;
624
+ }
625
+ /** Validate before placing an endpoint in a launch script or MCP registration. */
626
+ export function resolveSpawnBridgeApiBaseUrl(env) {
627
+ const raw = env.BAPI_BASE_URL;
628
+ if (!raw || !raw.trim())
629
+ return undefined;
630
+ const result = validateHttpBaseUrl(raw);
631
+ return result.ok ? result.baseUrl : undefined;
632
+ }
633
+ /**
634
+ * A shell-spawned CLI cannot see the parent MCP process's environment. Read only
635
+ * the non-secret endpoint from the selected host's project registration when the
636
+ * shell did not inherit BAPI_BASE_URL (common for Cursor launched from the GUI).
637
+ */
638
+ export async function resolveStartTicketsBridgeApiBaseUrl(deps, agent) {
639
+ const inherited = resolveSpawnBridgeApiBaseUrl(deps.env);
640
+ if (inherited)
641
+ return inherited;
642
+ const configPath = agent.name === "cursor-agent"
643
+ ? path.join(deps.cwd, ".cursor", "mcp.json")
644
+ : path.join(deps.cwd, ".mcp.json");
645
+ try {
646
+ const raw = await (deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")))(configPath);
647
+ const parsed = JSON.parse(raw);
648
+ const bridge = parsed.mcpServers?.[MCP_SERVER_NAME] ?? parsed.mcpServers?.[LEGACY_SERVER_NAMES[0]];
649
+ if (!bridge)
650
+ return undefined;
651
+ const args = Array.isArray(bridge.args) ? bridge.args : [];
652
+ const flagIndex = args.indexOf("--base-url");
653
+ const candidate = bridge.env?.BAPI_BASE_URL ?? (flagIndex >= 0 ? args[flagIndex + 1] : undefined);
654
+ const validated = validateHttpBaseUrl(candidate);
655
+ return validated.ok ? validated.baseUrl : undefined;
656
+ }
657
+ catch {
658
+ return undefined;
659
+ }
660
+ }
613
661
  /**
614
662
  * BAPI-801/BAPI-847: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell
615
663
  * should carry — the single rule shared by the real spawn builder
@@ -1316,7 +1364,12 @@ export function buildAgentInvocationArgv(agent, prompt, modelAlias, launchArgsOr
1316
1364
  if (agent.name === "codex" && Array.isArray(launchArgsOrCursorWorkspace))
1317
1365
  argv.push(...launchArgsOrCursorWorkspace);
1318
1366
  if (agent.name === "cursor-agent" && typeof launchArgsOrCursorWorkspace === "string" && launchArgsOrCursorWorkspace) {
1319
- argv.push("-p", "--output-format", "text", "--trust", "--approve-mcps", "--force", "--workspace", launchArgsOrCursorWorkspace);
1367
+ // Cursor's text mode can buffer the entire headless turn. Route NDJSON
1368
+ // through our progress renderer, which prints assistant prose and tool
1369
+ // names without dumping tool arguments or results into the terminal.
1370
+ const progressScript = fileURLToPath(new URL("./cursor-agent-progress.js", import.meta.url));
1371
+ argv.splice(0, 1, "node", progressScript, "--", agent.command);
1372
+ argv.push("-p", "--output-format", "stream-json", "--trust", "--approve-mcps", "--force", "--workspace", launchArgsOrCursorWorkspace);
1320
1373
  }
1321
1374
  if (agent.supportsModelOverride &&
1322
1375
  typeof modelAlias === "string" &&
@@ -1347,14 +1400,14 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
1347
1400
  }
1348
1401
  }
1349
1402
  /** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
1350
- export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = []) {
1403
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1351
1404
  const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1352
- return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
1405
+ return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
1353
1406
  }
1354
1407
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
1355
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = []) {
1408
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1356
1409
  const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1357
- return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
1410
+ return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
1358
1411
  }
1359
1412
  /**
1360
1413
  * Build the shell command run inside each spawned tab/session, dispatched by
@@ -1364,10 +1417,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
1364
1417
  * injected as `--model` at the spawn boundary. `conductorEnabled` appends the
1365
1418
  * BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
1366
1419
  */
1367
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = []) {
1420
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1368
1421
  if (platform === "win32")
1369
- return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs);
1370
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs);
1422
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1423
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1371
1424
  }
1372
1425
  /**
1373
1426
  * Build the shell command run inside a spawned tab/session for an ARBITRARY
@@ -2008,7 +2061,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
2008
2061
  // buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
2009
2062
  // dry-run has no created worktree to bind, so its preview omits the
2010
2063
  // process-local -C/-c MCP scope that the real spawn resolves later.
2011
- buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
2064
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env)), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
2012
2065
  };
2013
2066
  }
2014
2067
  /**
@@ -2017,7 +2070,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
2017
2070
  * the `bapi` target. Lists both registration files and the version-pinned shim
2018
2071
  * command. Pure formatting — implies no credentials and writes no files.
2019
2072
  */
2020
- export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation) {
2073
+ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation, bridgeApiBaseUrl) {
2021
2074
  const api = platform === "win32" ? path.win32 : path.posix;
2022
2075
  const mcpJson = api.join(worktreePath, ".mcp.json");
2023
2076
  const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
@@ -2038,6 +2091,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
2038
2091
  `DRY-RUN: ${mcpJson}`,
2039
2092
  `DRY-RUN: ${cursorJson}`,
2040
2093
  `DRY-RUN: ${shim}`,
2094
+ ...(bridgeApiBaseUrl ? [`DRY-RUN: bapi shim adds --base-url ${bridgeApiBaseUrl}`] : []),
2041
2095
  ];
2042
2096
  }
2043
2097
  /**
@@ -2062,7 +2116,7 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
2062
2116
  `DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
2063
2117
  `DRY-RUN: ${agentInvocation}`,
2064
2118
  ...(prBaseBranch ? [`DRY-RUN: PR base: ${prBaseBranch}`] : []),
2065
- ...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
2119
+ ...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation, resolveSpawnBridgeApiBaseUrl(env)),
2066
2120
  ];
2067
2121
  }
2068
2122
  /**
@@ -2136,13 +2190,15 @@ export function formatSummaryReport(rows) {
2136
2190
  * Provisioning needs real `fs/promises` ops (not part of the command-runner DI
2137
2191
  * boundary), but inherits the platform and cwd from the orchestration deps.
2138
2192
  */
2139
- export function buildMcpProvisioningDeps(deps) {
2193
+ export function buildMcpProvisioningDeps(deps, bridgeApiBaseUrlOverride) {
2140
2194
  return {
2195
+ env: deps.env,
2141
2196
  readFile: deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")),
2142
2197
  writeFile: deps.writeFile ?? ((filePath, data) => writeFile(filePath, data, "utf-8")),
2143
2198
  mkdir: deps.mkdir ?? ((dirPath, options) => mkdir(dirPath, options)),
2144
2199
  platform: deps.platform,
2145
2200
  cwd: deps.cwd,
2201
+ bridgeApiBaseUrl: bridgeApiBaseUrlOverride ?? resolveSpawnBridgeApiBaseUrl(deps.env),
2146
2202
  // Resolve how the worker MCP shim launches at dispatch time from the running
2147
2203
  // conductor's own on-disk build (absolute-build-path primary, npm-channel
2148
2204
  // fallback) — never from process.cwd(). `nodeExecutable` is kept as "node"
@@ -3199,6 +3255,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3199
3255
  // Fail-open: an unresolved name is warned about loudly but never aborts the run.
3200
3256
  const resolveRepoNameFn = overrides.resolveRepoName ?? resolveStartTicketsRepoName;
3201
3257
  const resolvedRepoName = await resolveRepoNameFn(deps);
3258
+ const bridgeApiBaseUrl = await resolveStartTicketsBridgeApiBaseUrl(deps, agent);
3202
3259
  if (!resolvedRepoName) {
3203
3260
  overrides.repoNameWarningLog?.("Warning: could not resolve the repo name (set BAPI_REPO_NAME or add a valid " +
3204
3261
  ".bridge/config). Spawned worktrees may fall back to the directory name and " +
@@ -3211,7 +3268,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3211
3268
  // epic.base_branch override applied above) so the spawned worker gets
3212
3269
  // BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
3213
3270
  // is non-default.
3214
- options.baseBranch);
3271
+ options.baseBranch, bridgeApiBaseUrl);
3215
3272
  if (!platformConfig.ok)
3216
3273
  return { ok: false, error: platformConfig.error };
3217
3274
  // BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
@@ -3261,7 +3318,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3261
3318
  const provisionCommandsFn = overrides.provisionCommands ??
3262
3319
  ((rows, d) => provisionCommandsForCreatedWorktrees(rows, buildCommandProvisioningDeps(d), agent.name === "codex"));
3263
3320
  const provisionFn = overrides.provisionMcpRegistrations ??
3264
- ((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d)));
3321
+ ((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d, bridgeApiBaseUrl)));
3265
3322
  const materializeFn = overrides.materializeFileCredentials ?? materializeFileCredentialsForCreatedWorktrees;
3266
3323
  const detectTerminalFn = overrides.detectTerminal ?? detectTerminal;
3267
3324
  const spawnTabsFn = overrides.spawnTabsForCreatedWorktrees ?? spawnTabsForCreatedWorktrees;
@@ -3376,14 +3433,20 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3376
3433
  let scoped = stampIndexScopeOnRows(routed, indexScope);
3377
3434
  if (agent.name === "codex") {
3378
3435
  const mcpDeps = buildMcpProvisioningDeps(deps);
3379
- scoped = await Promise.all(scoped.map(async (row) => {
3380
- if (row.status !== "created" || !row.path)
3381
- return row;
3436
+ // Scope resolution can refresh one shared Codex profile. Serialize these
3437
+ // reads and writes so sibling worktrees cannot observe a half-written update.
3438
+ const resolved = [];
3439
+ for (const row of scoped) {
3440
+ if (row.status !== "created" || !row.path) {
3441
+ resolved.push(row);
3442
+ continue;
3443
+ }
3382
3444
  const scope = await (overrides.resolveCodexScope ?? resolveCodexLaunchScope)(row.path, mcpDeps);
3383
- return scope.ok
3445
+ resolved.push(scope.ok
3384
3446
  ? { ...row, codexLaunchArgs: scope.argv }
3385
- : { ...row, status: "spawn-failed", error: scope.error };
3386
- }));
3447
+ : { ...row, status: "spawn-failed", error: scope.error });
3448
+ }
3449
+ scoped = resolved;
3387
3450
  }
3388
3451
  const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
3389
3452
  return { ok: true, rows };
@@ -3427,7 +3490,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
3427
3490
  return 1;
3428
3491
  }
3429
3492
  const options = parsed.options;
3430
- const deps = overrides.deps ?? createDefaultStartTicketsDeps();
3493
+ const rawDeps = overrides.deps ?? createDefaultStartTicketsDeps();
3431
3494
  const orchestrate = overrides.orchestrate ?? orchestrateStartTickets;
3432
3495
  // The parser validates `--agent`, so this resolves to a real spec; the guard
3433
3496
  // is defensive only.
@@ -3436,6 +3499,14 @@ export async function runStartTicketsCli(argv, overrides = {}) {
3436
3499
  errorLog(`Error: Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`);
3437
3500
  return 1;
3438
3501
  }
3502
+ // The editor's MCP registration can have a local endpoint that its Bash tool
3503
+ // does not inherit. Use that endpoint for the CLI's own routing as well as for
3504
+ // its generated worktree registration and terminal command. Never mutate the
3505
+ // caller's environment or read/copy its API key from the registration.
3506
+ const bridgeApiBaseUrl = await resolveStartTicketsBridgeApiBaseUrl(rawDeps, agent);
3507
+ const deps = bridgeApiBaseUrl
3508
+ ? { ...rawDeps, env: { ...rawDeps.env, BAPI_BASE_URL: bridgeApiBaseUrl } }
3509
+ : rawDeps;
3439
3510
  // BAPI-642: a malformed `--tier` value is fail-open — emit ONE concise,
3440
3511
  // secret-free warning and continue (routing applies the premium fallback). It
3441
3512
  // never aborts the launch or changes the numeric exit code.
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.56";
3
- export const BUILD_COMMIT = "ba95aad09dd4";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.56", "serve"];
2
+ export const VERSION = "0.2.58";
3
+ export const BUILD_COMMIT = "e29c3f46e4eb";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.58", "serve"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
4
4
  "description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,11 +14,11 @@
14
14
  "tool": "request_plan_generation",
15
15
  "params": {
16
16
  "ticket_number": "{ticket_key}",
17
- "wait_for_result": true,
17
+ "wait_for_result": false,
18
18
  "save_locally": true,
19
19
  "provider": "{provider}"
20
20
  },
21
- "description": "Generate implementation plan. Re-entry-safe: a non-forced request against a fresh existing plan returns reused: true and runs nothing new — the expected fast path on recovery re-entry. force is reserved for deliberate regeneration; this step never sets it. On a GATEWAY_TIMEOUT/504 response, poll get_plan (or the response's recovery_get URL) until a terminal result — never reissue this step."
21
+ "description": "Submit implementation-plan generation without waiting through an MCP client deadline. Re-entry-safe: the backend reuses a fresh existing plan (reused: true, no new run). force is reserved for deliberate regeneration; this step never sets it. On a dropped connection or other ambiguous submission outcome, use get_plan to inspect the existing run — never reissue this step automatically."
22
22
  },
23
23
  {
24
24
  "type": "agent_task",
@@ -30,7 +30,7 @@
30
30
  {
31
31
  "type": "agent_task",
32
32
  "id": "wait-for-ticket-review",
33
- "instruction": "FIRST, check whether a review was actually submitted. The previous step runs with `on_error: warn_and_continue`, so a rejected submission (a 402, 403 or 500 from the generate endpoint, or a dropped connection) lets the pipeline continue into this step. If that step returned an error message rather than its submission confirmation or a `recovery_get` envelope, do NOT wait: nothing is running, and `wait_for_ticket_review` cannot tell \"never submitted\" from \"still running\" \u2014 it would read `pending` in every window until the caller's own timeout. Treat a failed submission as a terminal failure, report the error the previous step gave, and take the verdict contract's `revise` path.\n\nOtherwise, wait for the review submitted by the previous step to finish, using repeated BOUNDED windows.\n\nCall `wait_for_ticket_review` with `ticket_number` set to the ticket key. Each call occupies at most 240 seconds and returns a JSON body with a `state` field:\n\n- `state: \"pending\"` \u2014 NORMAL and expected. The review is still running. Call the tool again immediately. Do NOT sleep, do NOT poll in a shell loop, and do NOT re-run `request_ticket_review`: re-submitting starts a second billable multi-model review and abandons the one already in flight.\n- `state: \"ready\"` \u2014 the review is over. BAPI-1121: a ready result has ALREADY persisted exactly the accepted documents and reconciled the canonical source files \u2014 it writes every leg in `saved_legs` to its canonical path and removes the stale canonical file for every leg in `withheld_legs`. Do NOT call `get_clarifying_questions` or `get_ticket_critique` after a ready result; those retrieval calls were removed from this recipe because reading them again could reintroduce exactly the stale content the wait just withheld. Treat `saved_legs` as the authoritative source set for the next steps, not file presence.\n - `freshness_basis: \"generation\"` is the normal case: `advanced_legs`/`saved_legs` are current-review evidence, proven newer than the submission. Any leg in `withheld_legs` was delivered but NOT proven current \u2014 surface its `reason` (`generation_not_advanced` or `identity_unreadable`) as a visible warning; do not describe the result as an ordinary cost-saved one-leg review when a withholding occurred. A leg in `absent_legs` with `reason: \"not_delivered\"` is an ordinary policy omission (e.g. an Easy-ticket clarify-only review) unless its status also flags it as policy-expected, in which case call that out too.\n - `freshness_basis: \"terminality_only\"` means no submission baseline was available (e.g. a resumed session) \u2014 `advanced_legs` is empty by construction and `saved_legs` was accepted on terminal delivery alone. Surface this prominently as a DEGRADED-freshness warning: the saved artifact(s) may be indistinguishable from a prior attachment, and the downstream identity-footer agreement check is the remaining backstop. Retain this warning when reporting to the next steps rather than treating a `terminality_only` ready result as equivalent to a `generation`-backed one.\n - If the result instead carries `state: \"error\"` alongside a persistence failure (an `ARTIFACT_PERSISTENCE_FAILED` `error`/`status`/`message` envelope), HALT rather than proceeding with whatever files happen to exist on disk \u2014 report the failure; the accepted legs already on disk before the failure are named in the envelope and are not lost, but nothing new was persisted for this window.\n- `state: \"error\"` \u2014 terminal. Report the `reason` and stop; the review will not complete on its own. `UPSTREAM_TERMINAL` means the review failed or expired server-side \u2014 take the verdict contract's `revise` path; do NOT open another wait window and do NOT re-run `request_ticket_review`. `INVALID_RESPONSE` means the delivered document could not be read.\n\nKeep looping on `pending` for as long as this session's own budget allows. There is no iteration cap here: the bound is the caller's \u2014 for the conductor's spec-review worker that is the existing 120-minute job timeout, which produces the ordinary liveness outcome if the review never lands. Never fabricate a verdict or a review result from a pending state.",
33
+ "instruction": "FIRST, check whether a review was actually submitted. The previous step runs with `on_error: warn_and_continue`, so a rejected submission (a 402, 403 or 500 from the generate endpoint, or a dropped connection) lets the pipeline continue into this step. If that step returned an error message rather than its submission confirmation or a `recovery_get` envelope, do NOT wait: nothing is running, and `wait_for_ticket_review` cannot tell \"never submitted\" from \"still running\" \u2014 it would read `pending` in every window until the caller's own timeout. Treat a failed submission as a terminal failure, report the error the previous step gave, and take the verdict contract's `revise` path.\n\nOtherwise, wait for the review submitted by the previous step to finish, using repeated BOUNDED windows.\n\nCall `wait_for_ticket_review` with `ticket_number` set to the ticket key. Each call uses the default 20-second window (set `budget_ms: 20000` explicitly if needed); do not use a longer window, because some MCP clients time out before a pending result arrives. The tool returns a JSON body with a `state` field:\n\n- `state: \"pending\"` \u2014 NORMAL and expected. The review is still running. Call the tool again immediately. Do NOT sleep, do NOT poll in a shell loop, and do NOT re-run `request_ticket_review`: re-submitting starts a second billable multi-model review and abandons the one already in flight.\n- `state: \"ready\"` \u2014 the review is over. BAPI-1121: a ready result has ALREADY persisted exactly the accepted documents and reconciled the canonical source files \u2014 it writes every leg in `saved_legs` to its canonical path and removes the stale canonical file for every leg in `withheld_legs`. Do NOT call `get_clarifying_questions` or `get_ticket_critique` after a ready result; those retrieval calls were removed from this recipe because reading them again could reintroduce exactly the stale content the wait just withheld. Treat `saved_legs` as the authoritative source set for the next steps, not file presence.\n - `freshness_basis: \"generation\"` is the normal case: `advanced_legs`/`saved_legs` are current-review evidence, proven newer than the submission. Any leg in `withheld_legs` was delivered but NOT proven current \u2014 surface its `reason` (`generation_not_advanced` or `identity_unreadable`) as a visible warning; do not describe the result as an ordinary cost-saved one-leg review when a withholding occurred. A leg in `absent_legs` with `reason: \"not_delivered\"` is an ordinary policy omission (e.g. an Easy-ticket clarify-only review) unless its status also flags it as policy-expected, in which case call that out too.\n - `freshness_basis: \"terminality_only\"` means no submission baseline was available (e.g. a resumed session) \u2014 `advanced_legs` is empty by construction and `saved_legs` was accepted on terminal delivery alone. Surface this prominently as a DEGRADED-freshness warning: the saved artifact(s) may be indistinguishable from a prior attachment, and the downstream identity-footer agreement check is the remaining backstop. Retain this warning when reporting to the next steps rather than treating a `terminality_only` ready result as equivalent to a `generation`-backed one.\n - If the result instead carries `state: \"error\"` alongside a persistence failure (an `ARTIFACT_PERSISTENCE_FAILED` `error`/`status`/`message` envelope), HALT rather than proceeding with whatever files happen to exist on disk \u2014 report the failure; the accepted legs already on disk before the failure are named in the envelope and are not lost, but nothing new was persisted for this window.\n- `state: \"error\"` \u2014 terminal. Report the `reason` and stop; the review will not complete on its own. `UPSTREAM_TERMINAL` means the review failed or expired server-side \u2014 take the verdict contract's `revise` path; do NOT open another wait window and do NOT re-run `request_ticket_review`. `INVALID_RESPONSE` means the delivered document could not be read.\n\nKeep looping on `pending` for as long as this session's own budget allows. There is no iteration cap here: the bound is the caller's \u2014 for the conductor's spec-review worker that is the existing 120-minute job timeout, which produces the ordinary liveness outcome if the review never lands. Never fabricate a verdict or a review result from a pending state.",
34
34
  "description": "Wait for the submitted review in repeated bounded windows; a ready result has already persisted the accepted documents and reconciled the canonical files (BAPI-1121). Resumable: a pending result is a normal observation, not a failure.",
35
35
  "on_error": "halt"
36
36
  },