@bridge_gpt/mcp-server 0.2.66 → 0.2.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +252 -77
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +43 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/commands.generated.js +11 -11
  8. package/build/conductor/bridge-api-client.js +143 -1
  9. package/build/conductor/bring-up-facts.js +1 -1
  10. package/build/conductor/doctor.js +3 -3
  11. package/build/conductor/epic-implementer-cli.js +22 -6
  12. package/build/conductor/errors.js +4 -3
  13. package/build/conductor/guided-repair.js +6 -2
  14. package/build/conductor/hook-runtime-qualification.js +1 -1
  15. package/build/conductor/merge-execution-contract.js +27 -8
  16. package/build/conductor/merge-execution-eligibility.js +3 -2
  17. package/build/conductor/pr-ci-producer.js +1 -1
  18. package/build/conductor/readiness-cli.js +18 -2
  19. package/build/conductor/readiness-sections.js +41 -6
  20. package/build/conductor/readiness.js +181 -29
  21. package/build/conductor/recovery-cli.js +200 -8
  22. package/build/conductor/recovery-operations.js +69 -3
  23. package/build/conductor/repair-contract.js +54 -3
  24. package/build/conductor/repository-workflow-remedy.js +125 -0
  25. package/build/conductor/tools.js +2 -2
  26. package/build/conductor-bundle-artifacts.js +8 -0
  27. package/build/docs.generated.js +8 -3
  28. package/build/drive-epic-key.js +6 -5
  29. package/build/drive-epic.js +85 -54
  30. package/build/epic-coherence-input.js +2 -1
  31. package/build/epic-implementer-bin.js +22 -20
  32. package/build/epic-implementer-bundle-cli.js +2 -2
  33. package/build/executor/cli.js +3 -2
  34. package/build/executor/completion-contract.js +48 -5
  35. package/build/executor/completion-supervision.js +4 -0
  36. package/build/executor/env.js +19 -0
  37. package/build/executor/heartbeat.js +1 -1
  38. package/build/executor/http-client.js +20 -0
  39. package/build/executor/job-errors.js +25 -0
  40. package/build/executor/job-runner.js +225 -41
  41. package/build/executor/observation.js +6 -1
  42. package/build/executor/preflight.js +51 -14
  43. package/build/executor/required-work-ledger.js +6 -1
  44. package/build/executor/resume-pre-spawn.js +2 -0
  45. package/build/executor/runner.js +8 -2
  46. package/build/executor/service-unit.js +7 -1
  47. package/build/executor/worker-finalization.js +82 -29
  48. package/build/executor/worker-guard-hook.js +137 -33
  49. package/build/executor/worktree.js +82 -1
  50. package/build/index.js +403 -429
  51. package/build/install-bridge-conductor.js +8 -6
  52. package/build/install-bridge.js +3 -3
  53. package/build/phase-context-git-probe.js +68 -0
  54. package/build/phase-result-artifacts.js +173 -5
  55. package/build/pipelines.generated.js +18 -24
  56. package/build/plane/alembic-head.js +12 -3
  57. package/build/plane/base-worktree-guard.js +147 -0
  58. package/build/plane/cli.js +217 -17
  59. package/build/plane/code-snapshot.js +468 -0
  60. package/build/plane/defaults.js +48 -3
  61. package/build/plane/endpoint.js +41 -0
  62. package/build/plane/manifest.js +120 -1
  63. package/build/plane/member-roster.js +94 -19
  64. package/build/plane/preflight.js +30 -2
  65. package/build/plane/process-cwd.js +67 -0
  66. package/build/plane/restart.js +542 -0
  67. package/build/plane/shutdown.js +31 -0
  68. package/build/plane/status.js +143 -8
  69. package/build/plane/supervisor.js +586 -179
  70. package/build/plane/types.js +54 -2
  71. package/build/printed-remedy-commands.js +37 -0
  72. package/build/readiness-check.js +1 -1
  73. package/build/readme.generated.js +1 -1
  74. package/build/review-generation.js +22 -0
  75. package/build/setup-epic.js +44 -19
  76. package/build/sfcc/config.js +9 -0
  77. package/build/sfcc/credentials.js +100 -38
  78. package/build/sfcc/hostname.js +67 -0
  79. package/build/sfcc/permissions.js +17 -13
  80. package/build/sfcc/register.js +1 -1
  81. package/build/sfcc/tool-wrapper.js +39 -43
  82. package/build/sfcc/write-grants.js +17 -11
  83. package/build/sfcc/write-result.js +4 -3
  84. package/build/sfcc/writes-site-preference.js +20 -8
  85. package/build/start-tickets.js +179 -31
  86. package/build/tool-error-envelope.js +61 -0
  87. package/build/version.generated.js +3 -3
  88. package/build/visual-diff.js +22 -1
  89. package/build/worker-guard-hook-bin.js +3 -6
  90. package/build/worker-guard-hook-main.js +47 -0
  91. package/build/worktree-core.js +130 -4
  92. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  93. package/docs/EPIC-IMPLEMENTER.md +662 -0
  94. package/docs/install/mcp-tool-integrations.md +24 -15
  95. package/docs/install/sfcc-integration.md +12 -4
  96. package/package.json +1 -1
  97. package/pipelines/plan-epic.json +0 -5
  98. package/pipelines/review-ticket.json +1 -1
  99. package/docs/CONDUCTOR.md +0 -605
  100. package/pipelines/idea-to-pr.json +0 -49
@@ -136,7 +136,7 @@ export function getStartTicketsUsage() {
136
136
  " --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
137
137
  " Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
138
138
  " --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases.",
139
- " --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. Mutually exclusive with --auto-tier.",
139
+ " --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. With --workflow review-and-implement a valid value is also forwarded to the spawned command so the review tab and the fresh implementation session both run on it. Mutually exclusive with --auto-tier.",
140
140
  " --auto-tier cheap|basic|premium Internal/advanced machine-carried raw backend tier for chained automation handoffs (e.g. /review-and-implement): bypasses the per-ticket lookup like --tier, but remains subject to per-session optimization-posture adjustment (may be downshifted under maximize_subscription, unless the server reports configured thresholds). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --tier.",
141
141
  " --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
142
142
  " --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
@@ -619,7 +619,9 @@ resumeMode = false, workflow = "implement", reviewRounds,
619
619
  // conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
620
620
  // resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
621
621
  // command.
622
- baseBranch, bridgeApiBaseUrlOverride) {
622
+ baseBranch, bridgeApiBaseUrlOverride,
623
+ // BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
624
+ manualTier) {
623
625
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
624
626
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
625
627
  }
@@ -640,7 +642,7 @@ baseBranch, bridgeApiBaseUrlOverride) {
640
642
  // falls back to the basename-derived repo name (the 403 root cause), and
641
643
  // (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
642
644
  // against it. The base assignment stays outermost.
643
- 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),
645
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier), repoName, platform), prBaseBranch, platform),
644
646
  spawnTerminalTab: deps.spawnTerminalTab,
645
647
  },
646
648
  };
@@ -1072,6 +1074,19 @@ export async function refreshBaseBranch(deps, options) {
1072
1074
  // `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
1073
1075
  // force-move a checked-out branch ref, so we must go through merge.
1074
1076
  if (basePath) {
1077
+ // BAPI-1362: a live plane member may be running out of this very checkout.
1078
+ // Fail closed — an unreadable or ambiguous record protects it. When
1079
+ // protected, nothing local is moved: the caller cuts from the fetched
1080
+ // `origin/<base>` SHA through the same fetch-only resolution the
1081
+ // non-mutating path uses.
1082
+ const decision = await evaluateGuardSafely(deps, basePath);
1083
+ if (decision.kind === "protect") {
1084
+ (deps.baseWorktreeGuardWarningLog ?? defaultGuardWarningLog)(formatBaseWorktreeGuardWarning(decision, baseBranch));
1085
+ const resolved = await fetchAndResolveBaseSha(deps, baseBranch);
1086
+ if (!resolved.ok)
1087
+ return { ok: false, error: resolved.error };
1088
+ return { ok: true, protectedBaseSha: resolved.base_sha };
1089
+ }
1075
1090
  const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
1076
1091
  if (!commandSucceeded(merge)) {
1077
1092
  return {
@@ -1103,6 +1118,25 @@ export async function refreshBaseBranch(deps, options) {
1103
1118
  }
1104
1119
  return { ok: true };
1105
1120
  }
1121
+ function defaultGuardWarningLog(message) {
1122
+ // stderr, never stdout: stdout carries the workflow's normal output.
1123
+ process.stderr.write(`${message}\n`);
1124
+ }
1125
+ /** Run the guard; any failure of the guard itself is a `protect` (fail closed). */
1126
+ async function evaluateGuardSafely(deps, basePath) {
1127
+ try {
1128
+ if (deps.evaluateBaseWorktreeGuard)
1129
+ return await deps.evaluateBaseWorktreeGuard(basePath);
1130
+ return await evaluateBaseWorktreeGuard(basePath, await createBaseWorktreeGuardDeps());
1131
+ }
1132
+ catch {
1133
+ return {
1134
+ kind: "protect",
1135
+ reason: "manifest-unreadable",
1136
+ manifestLocation: ".bridge/plane/plane.json",
1137
+ };
1138
+ }
1139
+ }
1106
1140
  /**
1107
1141
  * Compatibility wrapper around {@link refreshBaseBranch} that preserves the
1108
1142
  * historical `refreshMainBranch(deps, { refreshMain })` signature for callers
@@ -1195,6 +1229,7 @@ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides
1195
1229
  // import the VALUES here (so internal uses keep working) and re-export the same
1196
1230
  // references so existing importers of `./start-tickets.js` (index.ts,
1197
1231
  // review-tickets.ts) and the pinned start-tickets tests are unaffected.
1232
+ import { evaluateBaseWorktreeGuard, createBaseWorktreeGuardDeps, formatBaseWorktreeGuardWarning, } from "./plane/base-worktree-guard.js";
1198
1233
  import { validateBranchName, fetchAndResolveBaseSha,
1199
1234
  // BAPI-941: both live in the same leaf module so the interactive refresh path
1200
1235
  // and the non-mutating conductor path share one drift implementation.
@@ -1339,8 +1374,9 @@ export async function resumeWorktrees(deps, options) {
1339
1374
  * E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
1340
1375
  * adds a helper; branch B adds a contract test over all helpers), which only the
1341
1376
  * FULL suite catches — so this instruction requires the worker to run the full
1342
- * project test suite (the same gate the advisory pre-push hook enforces) and only
1343
- * push/complete on green, escalating otherwise.
1377
+ * project test suite (every unit suite the project defines, the same suites its
1378
+ * pull request CI runs) and only push/complete on green, escalating otherwise.
1379
+ * The wording stays project-neutral because this CLI runs in other repositories.
1344
1380
  *
1345
1381
  * Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
1346
1382
  * single-quoted into a shell command and embedded in terminal-launcher args.
@@ -1351,8 +1387,8 @@ export function buildResumeModeRemediationFinalizeInstruction() {
1351
1387
  "the current base branch and resolve the merge conflicts. A clean textual merge can " +
1352
1388
  "still break behavior, so inspect for semantic conflicts even when there are no textual " +
1353
1389
  "conflict markers. Before you push or mark the ticket complete, run the full test suite " +
1354
- "for the project (the full unit suite, the same gate enforced by the advisory pre-push " +
1355
- "hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) " +
1390
+ "for the project (the full unit suite: every unit test suite the project defines, the " +
1391
+ "same suites its pull request CI runs) " +
1356
1392
  "and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
1357
1393
  "complete only after the full suite is green. If you cannot make the full suite pass, " +
1358
1394
  "report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
@@ -1385,6 +1421,8 @@ export function buildAgentPrompt(key, opts = {}) {
1385
1421
  // Review-only arguments (`--rounds`, non-default `--base-branch`) must never
1386
1422
  // leak into the legacy `/implement-ticket` prompt.
1387
1423
  if (workflow === "review-and-implement") {
1424
+ if (opts.manualTier !== undefined)
1425
+ command += ` --tier=${opts.manualTier}`;
1388
1426
  if (opts.agentName === "cursor-agent")
1389
1427
  command += " --agent=cursor-agent";
1390
1428
  if (opts.reviewRounds !== undefined) {
@@ -1457,13 +1495,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
1457
1495
  }
1458
1496
  }
1459
1497
  /** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
1460
- export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1461
- 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);
1498
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1499
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1462
1500
  return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
1463
1501
  }
1464
1502
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
1465
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1466
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1503
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1504
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1467
1505
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
1468
1506
  }
1469
1507
  /**
@@ -1474,10 +1512,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
1474
1512
  * injected as `--model` at the spawn boundary. `conductorEnabled` appends the
1475
1513
  * BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
1476
1514
  */
1477
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1515
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1478
1516
  if (platform === "win32")
1479
- return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1480
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1517
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1518
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1481
1519
  }
1482
1520
  /**
1483
1521
  * Build the shell command run inside a spawned tab/session for an ARBITRARY
@@ -2105,7 +2143,7 @@ export function buildDryRunResults(keys, overrides) {
2105
2143
  * PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
2106
2144
  * POSIX fallback for unsupported platforms.
2107
2145
  */
2108
- export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
2146
+ export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier) {
2109
2147
  return {
2110
2148
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
2111
2149
  // The builder accepts an optional resolved modelAlias; the dry-run caller
@@ -2118,7 +2156,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
2118
2156
  // buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
2119
2157
  // dry-run has no created worktree to bind, so its preview omits the
2120
2158
  // process-local -C/-c MCP scope that the real spawn resolves later.
2121
- 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),
2159
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env), manualTier), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
2122
2160
  };
2123
2161
  }
2124
2162
  /**
@@ -2157,8 +2195,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
2157
2195
  * the secret-free MCP provisioning preview. Pure platform formatting only — no
2158
2196
  * preflight, no routing failures.
2159
2197
  */
2160
- export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds) {
2161
- const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
2198
+ export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds, manualTier) {
2199
+ const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch, manualTier);
2162
2200
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
2163
2201
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
2164
2202
  // BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
@@ -2383,6 +2421,12 @@ export function formatModelRoutingDiagnosticLine(d) {
2383
2421
  case "posture-unavailable":
2384
2422
  return ("model routing: optimization posture was unavailable; Quality routing was retained " +
2385
2423
  "(reason=optimization-posture-unavailable).");
2424
+ case "review-consumer-unrecognized":
2425
+ return (`model routing: the Bridge API server did not recognize the review consumer${ticketSuffix} ` +
2426
+ "(reason=consumer-unrecognized); the review tab uses the premium fallback. Upgrade the server.");
2427
+ case "review-threshold-legacy":
2428
+ return (`model routing: review thresholds are not configured${ticketSuffix} ` +
2429
+ "(reason=legacy); the review tab uses the premium fallback. Configure review model-tier thresholds.");
2386
2430
  default:
2387
2431
  return `model routing degraded: ${d.message} ${failOpen}`;
2388
2432
  }
@@ -3023,7 +3067,43 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
3023
3067
  return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
3024
3068
  `posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
3025
3069
  `alias=${input.alias ?? "unresolved"}` +
3026
- (input.threshold ? ` threshold=${input.threshold}` : ""));
3070
+ (input.threshold ? ` threshold=${input.threshold}` : "") +
3071
+ (input.phase === "review" ? " phase=review consumer=review" : ""));
3072
+ }
3073
+ /**
3074
+ * BAPI-1365: classify a successfully fetched review-consumer `/model-tier`
3075
+ * response. `consumer-unrecognized` = the server did not echo `consumer:
3076
+ * "review"` (its tier and marker are not a review decision and are never
3077
+ * interpreted); `legacy` = it echoed review but no stored review thresholds
3078
+ * decided the tier. `undefined` = a usable review decision, OR a
3079
+ * `source: "fallback"` response (the difficulty itself failed; callers keep the
3080
+ * existing lookup-failure handling for that).
3081
+ */
3082
+ function reviewTierFallbackCause(value) {
3083
+ if (value.consumer !== "review")
3084
+ return "consumer-unrecognized";
3085
+ if (value.source === "fallback")
3086
+ return undefined;
3087
+ if (!isThresholdDecidedTierResponse(value))
3088
+ return "legacy";
3089
+ return undefined;
3090
+ }
3091
+ /** BAPI-1365: the secret-free operator warning and diagnostic for a review fallback cause. */
3092
+ function reviewTierFallbackNotice(cause, repoName, ticketKey) {
3093
+ const kind = cause === "consumer-unrecognized" ? "review-consumer-unrecognized" : "review-threshold-legacy";
3094
+ const warning = cause === "consumer-unrecognized"
3095
+ ? `model routing: server did not recognize the review consumer for ${ticketKey} (reason=consumer-unrecognized)`
3096
+ : `model routing: review thresholds are not configured for ${ticketKey} (reason=legacy)`;
3097
+ return {
3098
+ warning,
3099
+ diagnostic: makeModelRoutingDiagnostic({
3100
+ kind,
3101
+ repoName,
3102
+ storeTarget: `bapi:${repoName}`,
3103
+ ticketKey,
3104
+ message: warning,
3105
+ }),
3106
+ };
3027
3107
  }
3028
3108
  /**
3029
3109
  * BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
@@ -3032,11 +3112,16 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
3032
3112
  * (`configured` or `anomaly`). Every other case returns `undefined`, so the
3033
3113
  * caller keeps today's exact downshift and text.
3034
3114
  */
3035
- function thresholdSkipMarker(posture, tierResult) {
3115
+ function thresholdSkipMarker(posture, tierResult, reviewWorkflow = false) {
3036
3116
  if (posture !== "maximize_subscription")
3037
3117
  return undefined;
3038
3118
  if (!tierResult || !tierResult.ok)
3039
3119
  return undefined;
3120
+ // BAPI-1365: a review-consumer lookup counts only when the server echoed the
3121
+ // review consumer; an older server that ignored the parameter answered for
3122
+ // the implementation consumer, which says nothing about the review threshold.
3123
+ if (reviewWorkflow && tierResult.value.consumer !== "review")
3124
+ return undefined;
3040
3125
  if (!isThresholdDecidedTierResponse(tierResult.value))
3041
3126
  return undefined;
3042
3127
  return tierResult.value.thresholdMode;
@@ -3206,6 +3291,12 @@ errorLog = (message) => console.error(message)) {
3206
3291
  // produced it.
3207
3292
  const postureResolution = await resolveOptimizationPostureForStartTickets(access);
3208
3293
  const posture = postureResolution.posture;
3294
+ // BAPI-1365: the spawned tab of a `review-and-implement` launch is a REVIEW
3295
+ // tab, so its tier lookups ask for the review consumer. An ordinary
3296
+ // `implement` launch supplies no options and keeps the endpoint's default
3297
+ // implementation consumer. Derived once and shared by both fetch paths below.
3298
+ const isReviewWorkflow = options.workflow === "review-and-implement";
3299
+ const tierFetchOptions = isReviewWorkflow ? { consumer: "review" } : {};
3209
3300
  // BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
3210
3301
  // resolution, emitted exactly here — outside the per-ticket row loop below, so a
3211
3302
  // multi-ticket invocation still logs it once for this canonical repository. Uses
@@ -3232,14 +3323,14 @@ errorLog = (message) => console.error(message)) {
3232
3323
  // hoisting pattern above.
3233
3324
  //
3234
3325
  // BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
3235
- // `/model-tier` is fetched once (the shared bounded fetch, no `consumer`
3236
- // parameter) purely to read `threshold_mode`. A threshold-decided
3326
+ // `/model-tier` is fetched once (the shared bounded fetch; BAPI-1365: with
3327
+ // the workflow's consumer) purely to read `threshold_mode`. A threshold-decided
3237
3328
  // (`configured`/`anomaly`) response keeps the carried tier — stored
3238
3329
  // thresholds made it final. A fetch failure, `legacy`, or anything else
3239
3330
  // downshifts exactly as before. `quality`/`speed` never downshift, so they
3240
3331
  // make no extra request.
3241
3332
  const thresholdTierMap = posture === "maximize_subscription"
3242
- ? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible)
3333
+ ? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions)
3243
3334
  : null;
3244
3335
  const aliasByEffectiveTier = new Map();
3245
3336
  const resolveAutoTierAlias = async (effectiveTier) => {
@@ -3269,7 +3360,14 @@ errorLog = (message) => console.error(message)) {
3269
3360
  autoOut.push(row);
3270
3361
  continue;
3271
3362
  }
3272
- const threshold = thresholdSkipMarker(posture, thresholdTierMap?.get(row.key));
3363
+ const markerResult = thresholdTierMap?.get(row.key);
3364
+ const threshold = thresholdSkipMarker(posture, markerResult, isReviewWorkflow);
3365
+ // BAPI-1365: for a review launch, say WHY a fetched marker did not decide
3366
+ // the tier (never a review-threshold claim). A failed fetch keeps the
3367
+ // existing silent downshift.
3368
+ const markerCause = isReviewWorkflow && markerResult && markerResult.ok
3369
+ ? reviewTierFallbackCause(markerResult.value)
3370
+ : undefined;
3273
3371
  const effectiveTier = posture === "maximize_subscription" && !threshold
3274
3372
  ? downshiftModelTier(autoTier)
3275
3373
  : autoTier;
@@ -3277,7 +3375,7 @@ errorLog = (message) => console.error(message)) {
3277
3375
  const lineageSuffix = threshold
3278
3376
  ? `; threshold:${threshold}`
3279
3377
  : posture === "maximize_subscription"
3280
- ? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
3378
+ ? `; auto-tier:${autoTier}->maximize:${effectiveTier}${markerCause ? `; review-marker:${markerCause}` : ""}`
3281
3379
  : "";
3282
3380
  const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
3283
3381
  const reason = resolvedAlias
@@ -3294,7 +3392,14 @@ errorLog = (message) => console.error(message)) {
3294
3392
  ? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
3295
3393
  : {}),
3296
3394
  };
3297
- autoOut.push(aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow);
3395
+ let pushedRow = aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
3396
+ if (markerCause) {
3397
+ const notice = reviewTierFallbackNotice(markerCause, access.repoName, row.key);
3398
+ pushedRow = appendSummaryRowWarning(pushedRow, notice.warning);
3399
+ if (!pushedRow.modelRoutingDiagnostic)
3400
+ pushedRow = { ...pushedRow, modelRoutingDiagnostic: notice.diagnostic };
3401
+ }
3402
+ autoOut.push(pushedRow);
3298
3403
  // BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
3299
3404
  // carried baseline, the newly resolved posture, the exact adjustment token,
3300
3405
  // the calculated effective tier, and the validated alias (or `unresolved`).
@@ -3306,13 +3411,14 @@ errorLog = (message) => console.error(message)) {
3306
3411
  effective: effectiveTier,
3307
3412
  alias: resolvedAlias,
3308
3413
  ...(threshold ? { threshold } : {}),
3414
+ ...(isReviewWorkflow ? { phase: "review" } : {}),
3309
3415
  });
3310
3416
  }
3311
3417
  out = autoOut;
3312
3418
  }
3313
3419
  else {
3314
3420
  // Ordinary per-ticket difficulty-lookup path.
3315
- const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
3421
+ const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions);
3316
3422
  // Lazily resolve+validate the premium fallback alias at most once for this run
3317
3423
  // (the alias is identical across rows; cursor-agent validation spawns a probe).
3318
3424
  let premiumApplier = null;
@@ -3351,6 +3457,40 @@ errorLog = (message) => console.error(message)) {
3351
3457
  continue;
3352
3458
  }
3353
3459
  const { difficulty, tier, source } = tierResult.value;
3460
+ // BAPI-1365: a review-launch response is a review-threshold decision only
3461
+ // when the server echoed `consumer: "review"` AND stored thresholds decided
3462
+ // it. Anything else takes the existing premium fallback with a closed
3463
+ // reason — the returned tier and marker are never interpreted.
3464
+ if (isReviewWorkflow) {
3465
+ const cause = reviewTierFallbackCause(tierResult.value);
3466
+ if (cause) {
3467
+ const notice = reviewTierFallbackNotice(cause, access.repoName, row.key);
3468
+ if (agent.name === "codex") {
3469
+ loopOut.push(codexDefaultRows([row], () => true, notice.warning, notice.diagnostic)[0]);
3470
+ continue;
3471
+ }
3472
+ loopOut.push(await applyPremiumFallback(row, notice.warning, notice.diagnostic));
3473
+ continue;
3474
+ }
3475
+ if (source === "fallback") {
3476
+ // The server could not obtain a difficulty: its premium tier is a
3477
+ // guess, not a review decision — keep the lookup-failure handling.
3478
+ const warning = `model routing: tier lookup failed for ${row.key}; using agent default model`;
3479
+ const diagnostic = makeModelRoutingDiagnostic({
3480
+ kind: "tier-fetch-unavailable",
3481
+ repoName: access.repoName,
3482
+ storeTarget: `bapi:${access.repoName}`,
3483
+ ticketKey: row.key,
3484
+ message: `tier lookup failed for ${row.key}`,
3485
+ });
3486
+ if (agent.name === "codex") {
3487
+ loopOut.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
3488
+ continue;
3489
+ }
3490
+ loopOut.push(await applyPremiumFallback(row, warning, diagnostic));
3491
+ continue;
3492
+ }
3493
+ }
3354
3494
  if (!tier) {
3355
3495
  // Backend reported no usable difficulty — default to premium/Opus. (The
3356
3496
  // backend now returns a premium fallback itself, so this is defensive.)
@@ -3396,7 +3536,7 @@ errorLog = (message) => console.error(message)) {
3396
3536
  // diagnostic carry a distinct `threshold` marker instead. The harness-
3397
3537
  // local premium fallback above is untouched.
3398
3538
  const difficultyTier = tier;
3399
- const threshold = thresholdSkipMarker(posture, tierResult);
3539
+ const threshold = thresholdSkipMarker(posture, tierResult, isReviewWorkflow);
3400
3540
  const effectiveTier = posture === "maximize_subscription" && !threshold
3401
3541
  ? downshiftModelTier(difficultyTier)
3402
3542
  : difficultyTier;
@@ -3406,7 +3546,10 @@ errorLog = (message) => console.error(message)) {
3406
3546
  : posture === "maximize_subscription"
3407
3547
  ? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
3408
3548
  : "";
3409
- const thresholdField = threshold ? { threshold } : {};
3549
+ const thresholdField = {
3550
+ ...(threshold ? { threshold } : {}),
3551
+ ...(isReviewWorkflow ? { phase: "review" } : {}),
3552
+ };
3410
3553
  // BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
3411
3554
  // cache/computation source (`cached`/`computed`/`fallback`) into the
3412
3555
  // summary's own routing-provenance field — that backend source is
@@ -3758,7 +3901,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3758
3901
  // epic.base_branch override applied above) so the spawned worker gets
3759
3902
  // BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
3760
3903
  // is non-default.
3761
- options.baseBranch, bridgeApiBaseUrl);
3904
+ options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
3762
3905
  if (!platformConfig.ok)
3763
3906
  return { ok: false, error: platformConfig.error };
3764
3907
  // BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
@@ -3787,6 +3930,11 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3787
3930
  });
3788
3931
  if (!refresh.ok)
3789
3932
  return { ok: false, error: refresh.error };
3933
+ // BAPI-1362: a live plane member runs from the base worktree, so the local
3934
+ // branch was left where it was; cut from the fetched remote SHA instead.
3935
+ if (refresh.protectedBaseSha !== undefined) {
3936
+ effectiveBaseStartPoint = refresh.protectedBaseSha;
3937
+ }
3790
3938
  }
3791
3939
  // BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
3792
3940
  // worker spawn at the one choke point both dispatch paths pass through, after
@@ -4023,7 +4171,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4023
4171
  const branch = resolveBranchForTicket(key, options.branchOverrides);
4024
4172
  const routedRow = routedByKey.get(key);
4025
4173
  const modelAlias = routedRow?.modelAlias ?? null;
4026
- for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds)) {
4174
+ for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds, isModelTier(options.injectedTier) ? options.injectedTier : undefined)) {
4027
4175
  log(line);
4028
4176
  }
4029
4177
  log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
@@ -7,6 +7,10 @@
7
7
  * "this operation has no local-backend equivalent" refusal, which the ticket
8
8
  * routes return as HTTP 409 with `detail.error = "UNSUPPORTED_IN_LOCAL_MODE"`.
9
9
  *
10
+ * BAPI-1382 adds a second trusted pair: the plan-entitlement denial, which the
11
+ * server returns as HTTP 403 with `detail.error = "ENTITLEMENT_REQUIRED"`. It
12
+ * tells a plan limit apart from a bad credential, which is also a 403.
13
+ *
10
14
  * Recognition is by the exact `(error, status)` PAIR — never by message text.
11
15
  * Message text is prose that gets reworded; matching on it would make a copy
12
16
  * edit silently change orchestration control flow.
@@ -18,6 +22,54 @@
18
22
  export const UNSUPPORTED_IN_LOCAL_MODE_ERROR = "UNSUPPORTED_IN_LOCAL_MODE";
19
23
  /** The HTTP status that marker is only ever valid with. */
20
24
  export const UNSUPPORTED_IN_LOCAL_MODE_STATUS = 409;
25
+ /** The plan-entitlement marker (BAPI-1382): the account's plan lacks a feature. */
26
+ export const ENTITLEMENT_REQUIRED_ERROR = "ENTITLEMENT_REQUIRED";
27
+ /** The HTTP status that marker is only ever valid with. */
28
+ export const ENTITLEMENT_REQUIRED_STATUS = 403;
29
+ /**
30
+ * The request-time too-little-ticket-detail refusal (BAPI-1382): the generate
31
+ * routes answer HTTP 422 with `detail.error = "INSUFFICIENT_TICKET_DETAIL"`
32
+ * plus the measured length, threshold, and description source.
33
+ */
34
+ export const INSUFFICIENT_TICKET_DETAIL_ERROR = "INSUFFICIENT_TICKET_DETAIL";
35
+ /** The HTTP status that marker is only ever valid with. */
36
+ export const INSUFFICIENT_TICKET_DETAIL_STATUS = 422;
37
+ /**
38
+ * The server `(error, status)` pairs the MCP relay keeps as received. Every
39
+ * other server-provided `error` is replaced by the relay's own status-derived
40
+ * code, so an arbitrary detail object can never forge a code.
41
+ */
42
+ export const TRUSTED_UPSTREAM_ERROR_PAIRS = [
43
+ [UNSUPPORTED_IN_LOCAL_MODE_ERROR, UNSUPPORTED_IN_LOCAL_MODE_STATUS],
44
+ [ENTITLEMENT_REQUIRED_ERROR, ENTITLEMENT_REQUIRED_STATUS],
45
+ [INSUFFICIENT_TICKET_DETAIL_ERROR, INSUFFICIENT_TICKET_DETAIL_STATUS],
46
+ ];
47
+ /** True only for an exact trusted `(error, status)` pair. */
48
+ export function isTrustedUpstreamErrorPair(error, status) {
49
+ return TRUSTED_UPSTREAM_ERROR_PAIRS.some(([code, code_status]) => error === code && status === code_status);
50
+ }
51
+ /**
52
+ * Add the typed `operation` to a serialized error envelope (BAPI-1382). A text
53
+ * that is not an envelope is wrapped into one with the given status, so a
54
+ * refused request is always parseable and never a prose-prefixed string.
55
+ */
56
+ export function withRequestOperation(envelopeText, operation, status) {
57
+ let parsed = null;
58
+ try {
59
+ parsed = JSON.parse(envelopeText);
60
+ }
61
+ catch {
62
+ parsed = null;
63
+ }
64
+ if (parsed !== null &&
65
+ typeof parsed === "object" &&
66
+ !Array.isArray(parsed) &&
67
+ typeof parsed.error === "string" &&
68
+ typeof parsed.status === "number") {
69
+ return JSON.stringify({ ...parsed, operation });
70
+ }
71
+ return JSON.stringify({ error: "UNKNOWN_ERROR", status, message: envelopeText, operation });
72
+ }
21
73
  /**
22
74
  * Parse `text` as a tool-error envelope, or return `null`.
23
75
  *
@@ -52,8 +104,17 @@ export function parseToolErrorEnvelope(text) {
52
104
  if (Array.isArray(obj.unsupported_fields)) {
53
105
  envelope.unsupported_fields = obj.unsupported_fields.filter((v) => typeof v === "string");
54
106
  }
107
+ if (typeof obj.operation === "string") {
108
+ envelope.operation = obj.operation;
109
+ }
55
110
  return envelope;
56
111
  }
112
+ /** True for the exact plan-entitlement contract: the marker AND HTTP 403. */
113
+ export function isEntitlementRequiredEnvelope(envelope) {
114
+ return (envelope !== null &&
115
+ envelope.error === ENTITLEMENT_REQUIRED_ERROR &&
116
+ envelope.status === ENTITLEMENT_REQUIRED_STATUS);
117
+ }
57
118
  /** True for the exact unsupported-local contract: the marker AND HTTP 409. */
58
119
  export function isUnsupportedInLocalModeEnvelope(envelope) {
59
120
  return (envelope !== null &&
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.66";
3
- export const BUILD_COMMIT = "b90190f51abc";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"];
2
+ export const VERSION = "0.2.67";
3
+ export const BUILD_COMMIT = "8eb6ba563e5f";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.67", "serve"];
@@ -18,6 +18,7 @@
18
18
  * mismatch budget (never 0%, which AA/font rendering makes unreachable).
19
19
  */
20
20
  import path from "path";
21
+ import { createRequire } from "module";
21
22
  import { Worker } from "worker_threads";
22
23
  import { computePngVisualDiff, DEFAULT_PASS_MISMATCH_PCT, PIXELMATCH_COLOR_THRESHOLD, } from "./visual-diff-worker.js";
23
24
  // ---------------------------------------------------------------------------
@@ -247,6 +248,26 @@ export async function loadPlaywright() {
247
248
  return { ok: false };
248
249
  }
249
250
  }
251
+ /**
252
+ * The Chromium install command matching the bundled `playwright-core`.
253
+ *
254
+ * A bare `npx playwright install chromium` fetches whatever Playwright is
255
+ * newest, whose browser build may not match the one this server drives.
256
+ * Resolution is best-effort: any failure returns the historic unversioned
257
+ * command rather than throwing, since this only builds a hint.
258
+ */
259
+ export function chromiumInstallCommand(readMetadata = () => createRequire(import.meta.url)("playwright-core/package.json")) {
260
+ try {
261
+ const version = readMetadata()?.version;
262
+ if (typeof version === "string" && version.length > 0) {
263
+ return `npx playwright@${version} install chromium`;
264
+ }
265
+ }
266
+ catch {
267
+ // Not resolvable: fall through to the unversioned command.
268
+ }
269
+ return "npx playwright install chromium";
270
+ }
250
271
  export async function launchBrowser(playwright) {
251
272
  try {
252
273
  const browser = await playwright.chromium.launch({ headless: true });
@@ -258,7 +279,7 @@ export async function launchBrowser(playwright) {
258
279
  ok: false,
259
280
  error: "BROWSER_UNAVAILABLE",
260
281
  status: 503,
261
- message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "npx playwright install chromium".`,
282
+ message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "${chromiumInstallCommand()}".`,
262
283
  };
263
284
  }
264
285
  }