@bridge_gpt/mcp-server 0.2.19 → 0.2.21

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 (35) hide show
  1. package/README.md +6 -3
  2. package/build/agents.generated.js +1 -1
  3. package/build/commands.generated.js +4 -3
  4. package/build/conductor/local-merge.js +458 -95
  5. package/build/estimate-epic.js +84 -0
  6. package/build/executor/job-runner.js +151 -17
  7. package/build/executor/merge-job.js +84 -10
  8. package/build/executor/worker-finalization.js +98 -18
  9. package/build/index.js +1843 -401
  10. package/build/pipelines.generated.js +16 -20
  11. package/build/readme.generated.js +1 -1
  12. package/build/review-tickets.js +15 -5
  13. package/build/sfcc/client.js +192 -50
  14. package/build/sfcc/ocapi-write-faults.js +94 -0
  15. package/build/sfcc/permissions.js +7 -22
  16. package/build/sfcc/register.js +9 -0
  17. package/build/sfcc/write-grants.js +80 -0
  18. package/build/sfcc/write-guard.js +39 -0
  19. package/build/sfcc/write-result.js +47 -0
  20. package/build/sfcc/write-tool-common.js +85 -0
  21. package/build/sfcc/writes-custom-object-def.js +141 -0
  22. package/build/sfcc/writes-object-attribute-payloads.js +97 -0
  23. package/build/sfcc/writes-site-preference-payloads.js +59 -0
  24. package/build/sfcc/writes-site-preference.js +96 -0
  25. package/build/sfcc/writes-system-object-payloads.js +213 -0
  26. package/build/sfcc/writes-system-object.js +348 -0
  27. package/build/sfcc/writes.js +66 -0
  28. package/build/version.generated.js +1 -1
  29. package/package.json +3 -3
  30. package/pipelines/idea-to-ticket.json +7 -0
  31. package/pipelines/review-ticket.json +5 -18
  32. package/public/css/main.min.css +1583 -117
  33. package/public/css/main.min.css.map +1 -1
  34. package/public/js/main.min.js +2792 -449
  35. package/public/js/main.min.js.map +1 -1
package/build/index.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_version_generated = __esm({
20
20
  "src/version.generated.ts"() {
21
21
  "use strict";
22
- VERSION = "0.2.19";
22
+ VERSION = "0.2.21";
23
23
  }
24
24
  });
25
25
 
@@ -1736,10 +1736,10 @@ function parseDeclaredTouchedFilesFromEnv(env = process.env) {
1736
1736
  }
1737
1737
  function collectBranchChangedFiles(opts = {}) {
1738
1738
  const baseRef = opts.baseRef ?? FILE_SCOPE_GUARD_BASE_REF;
1739
- const spawn7 = opts.spawnSyncFn ?? defaultSpawnSync;
1739
+ const spawn8 = opts.spawnSyncFn ?? defaultSpawnSync;
1740
1740
  let result;
1741
1741
  try {
1742
- result = spawn7("git", ["diff", "--name-only", `${baseRef}...HEAD`], {
1742
+ result = spawn8("git", ["diff", "--name-only", `${baseRef}...HEAD`], {
1743
1743
  cwd: opts.cwd,
1744
1744
  encoding: "utf-8",
1745
1745
  shell: false
@@ -6609,7 +6609,7 @@ function getReviewTicketsUsage() {
6609
6609
  "",
6610
6610
  "Flags:",
6611
6611
  " --auto Auto-approve all review gates (global, applies to all tickets)",
6612
- " --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: 2)",
6612
+ " --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: adaptive \u2014 backend routes by difficulty)",
6613
6613
  " --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)",
6614
6614
  " --agent claude|cursor-agent Agent command to launch in each tab (default: claude)",
6615
6615
  " --model VALUE Model alias to pass as --model to the agent (optional passthrough)",
@@ -6921,15 +6921,16 @@ function parseReviewTicketsArgs(argv) {
6921
6921
  function resolveEffectiveReviewMode(key, options) {
6922
6922
  const override = options.reviewOverrides[key];
6923
6923
  const auto = override?.auto !== void 0 ? override.auto : options.auto;
6924
- const rounds = override?.rounds !== void 0 ? override.rounds : options.rounds !== void 0 ? options.rounds : 2;
6924
+ const rounds = override?.rounds !== void 0 ? override.rounds : options.rounds !== void 0 ? options.rounds : void 0;
6925
6925
  return { auto, rounds };
6926
6926
  }
6927
6927
  function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
6928
6928
  const autoFlag = mode.auto ? " --auto" : "";
6929
+ const roundsFlag = mode.rounds !== void 0 ? ` --rounds=${mode.rounds}` : "";
6929
6930
  const baseBranchFlag = base.baseBranch ? ` --base-branch=${base.baseBranch}` : "";
6930
6931
  const baseShaFlag = base.baseSha ? ` --base-sha=${base.baseSha}` : "";
6931
6932
  const noRefreshFlag = base.noRefreshBase ? " --no-refresh-base" : "";
6932
- return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
6933
+ return `/review-ticket ${key}${autoFlag}${roundsFlag}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
6933
6934
  }
6934
6935
  function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT) {
6935
6936
  const prompt = buildReviewTicketPrompt(key, mode, base);
@@ -7162,9 +7163,10 @@ function formatReviewTicketsSummaryReport(rows) {
7162
7163
  const lines = ["Summary:"];
7163
7164
  for (const row of rows) {
7164
7165
  const modelPart = row.modelAlias ? row.modelAlias : "default";
7166
+ const roundsPart = row.rounds !== void 0 ? String(row.rounds) : "adaptive";
7165
7167
  const runIdPart = row.runId ? ` run_id=${row.runId}` : "";
7166
7168
  lines.push(
7167
- `${row.key} auto=${row.auto} rounds=${row.rounds} agent=${row.agentName} model=${modelPart} status=${row.status}${runIdPart}`
7169
+ `${row.key} auto=${row.auto} rounds=${roundsPart} agent=${row.agentName} model=${modelPart} status=${row.status}${runIdPart}`
7168
7170
  );
7169
7171
  }
7170
7172
  const failedRows = rows.filter((r) => r.status === "spawn-failed" || r.status === "already-dispatched");
@@ -10394,8 +10396,8 @@ var init_probes = __esm({
10394
10396
  cwd: dir,
10395
10397
  outputFormat: "text"
10396
10398
  });
10397
- const textResult4 = assertMarkers(textRun, [marker], "text output-format did not emit the marker");
10398
- if (textResult4.status !== "pass") return textResult4;
10399
+ const textResult8 = assertMarkers(textRun, [marker], "text output-format did not emit the marker");
10400
+ if (textResult8.status !== "pass") return textResult8;
10399
10401
  const jsonRun = await ctx.runHeadless({
10400
10402
  prompt: `Do not use any tools. Output exactly the token ${marker}.`,
10401
10403
  cwd: dir,
@@ -10627,23 +10629,68 @@ var init_github_mergeability = __esm({
10627
10629
  });
10628
10630
 
10629
10631
  // src/conductor/local-merge.ts
10630
- import { spawnSync as spawnSync2 } from "child_process";
10632
+ import { spawn as spawn4 } from "child_process";
10633
+ function defaultSleep(ms) {
10634
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
10635
+ }
10636
+ function sanitizeWaitMs(value, fallback) {
10637
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
10638
+ }
10631
10639
  function resolveLocalMergeMethod(value) {
10632
10640
  return typeof value === "string" && MERGE_METHODS.has(value) ? value : "squash";
10633
10641
  }
10634
- function defaultRunCommand2(cmd, args, env) {
10635
- const result = spawnSync2(cmd, args, {
10636
- encoding: "utf8",
10637
- env: { ...process.env, ...env },
10638
- timeout: DEFAULT_COMMAND_TIMEOUT_MS
10642
+ async function defaultRunCommand2(cmd, args, env, signal) {
10643
+ return await new Promise((resolve2) => {
10644
+ let settled = false;
10645
+ let timedOut = false;
10646
+ let stdout = "";
10647
+ let stderr = "";
10648
+ const child = spawn4(cmd, args, { env: { ...process.env, ...env } });
10649
+ const killChild = () => {
10650
+ try {
10651
+ child.kill("SIGKILL");
10652
+ } catch {
10653
+ }
10654
+ };
10655
+ const timer = setTimeout(() => {
10656
+ timedOut = true;
10657
+ killChild();
10658
+ }, DEFAULT_COMMAND_TIMEOUT_MS);
10659
+ const onAbort = () => {
10660
+ timedOut = true;
10661
+ killChild();
10662
+ };
10663
+ const finish = (result) => {
10664
+ if (settled) return;
10665
+ settled = true;
10666
+ clearTimeout(timer);
10667
+ if (signal) signal.removeEventListener("abort", onAbort);
10668
+ resolve2(result);
10669
+ };
10670
+ if (signal) {
10671
+ if (signal.aborted) onAbort();
10672
+ else signal.addEventListener("abort", onAbort);
10673
+ }
10674
+ child.stdout?.setEncoding("utf8");
10675
+ child.stderr?.setEncoding("utf8");
10676
+ child.stdout?.on("data", (chunk) => {
10677
+ stdout += chunk;
10678
+ });
10679
+ child.stderr?.on("data", (chunk) => {
10680
+ stderr += chunk;
10681
+ });
10682
+ child.on("error", () => {
10683
+ finish({ status: null, stdout, stderr, timedOut });
10684
+ });
10685
+ child.on("close", (code, sig) => {
10686
+ finish({
10687
+ status: code,
10688
+ stdout,
10689
+ stderr,
10690
+ timedOut: timedOut || sig === "SIGKILL" || sig === "SIGTERM"
10691
+ });
10692
+ });
10639
10693
  });
10640
- const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGTERM";
10641
- return {
10642
- status: result.status,
10643
- stdout: result.stdout ?? "",
10644
- stderr: result.stderr ?? "",
10645
- timedOut
10646
- };
10647
10694
  }
10648
10695
  function buildResponse(request, status, reason, terminal, ledgerEvents) {
10649
10696
  return {
@@ -10682,10 +10729,103 @@ function allRequiredChecksGreen(pollResponse, requiredChecks) {
10682
10729
  };
10683
10730
  return requiredChecks.every((name) => isGreen(byName.get(name)));
10684
10731
  }
10732
+ function extractMergeCommitOid(parsed) {
10733
+ const mc = parsed?.mergeCommit;
10734
+ if (mc && typeof mc === "object" && typeof mc.oid === "string") {
10735
+ return mc.oid;
10736
+ }
10737
+ return void 0;
10738
+ }
10739
+ function isMergedAtExpectedHead(state, headOid, expectedSha) {
10740
+ return typeof state === "string" && state.toUpperCase() === "MERGED" && typeof headOid === "string" && headOid.toLowerCase() === expectedSha.toLowerCase();
10741
+ }
10742
+ async function readPrMergeState(run, ghEnv, pr, json = PR_STATE_JSON) {
10743
+ const view = await run("gh", ["pr", "view", String(pr), "--json", json], ghEnv);
10744
+ if (view.timedOut) return { ok: false, reason: "gh_pr_view_timeout" };
10745
+ if (view.status !== 0) return { ok: false, reason: "gh_pr_view_failed" };
10746
+ try {
10747
+ const parsed = JSON.parse(view.stdout);
10748
+ return {
10749
+ ok: true,
10750
+ headOid: parsed.headRefOid,
10751
+ state: parsed.state,
10752
+ mergeCommitOid: extractMergeCommitOid(parsed),
10753
+ raw: parsed
10754
+ };
10755
+ } catch {
10756
+ return { ok: false, reason: "gh_pr_view_unparseable" };
10757
+ }
10758
+ }
10759
+ function buildAlreadyMergedResponse(request, baseDetails, mergeCommitSha) {
10760
+ const details = {
10761
+ ...baseDetails,
10762
+ already_merged: true,
10763
+ ...mergeCommitSha ? { merge_commit_sha: mergeCommitSha } : {}
10764
+ };
10765
+ return buildResponse(request, "succeeded", null, true, [
10766
+ { type: "merge.succeeded", status: "succeeded", details }
10767
+ ]);
10768
+ }
10769
+ function ciPollFailureDetailFromError(err) {
10770
+ if (!(err instanceof ConductorBridgeApiError)) return void 0;
10771
+ const kind = err.kind;
10772
+ if (kind !== "timeout" && kind !== "network" && kind !== "unauthorized" && kind !== "server" && kind !== "http") {
10773
+ return void 0;
10774
+ }
10775
+ const detail = {
10776
+ kind,
10777
+ diagnostic: safeDiagnosticMessage(err, "ci_poll_failed")
10778
+ };
10779
+ if (typeof err.status === "number" && Number.isInteger(err.status)) {
10780
+ detail.status = err.status;
10781
+ }
10782
+ return detail;
10783
+ }
10784
+ function ciPollFailureReasonFromDetail(detail) {
10785
+ if (!detail) return "ci_poll_failed";
10786
+ switch (detail.kind) {
10787
+ case "timeout":
10788
+ return "ci_poll_timeout";
10789
+ case "network":
10790
+ return "ci_poll_network";
10791
+ case "unauthorized":
10792
+ return "ci_poll_unauthorized";
10793
+ case "server":
10794
+ return "ci_poll_server";
10795
+ case "http":
10796
+ return typeof detail.status === "number" && Number.isInteger(detail.status) ? `ci_poll_http_${detail.status}` : "ci_poll_http";
10797
+ }
10798
+ }
10799
+ function isAmbiguousCiPollFailureReason(reason) {
10800
+ return reason.startsWith("ci_poll_");
10801
+ }
10802
+ async function waitForRequiredChecksGreen(pollCi, access2, expectedSha, requiredChecks, timeoutMs, pollIntervalMs, sleep3, now, signal) {
10803
+ const start = now();
10804
+ for (; ; ) {
10805
+ if (signal?.aborted) return { ok: false, reason: "merge_aborted" };
10806
+ let pollResponse;
10807
+ try {
10808
+ pollResponse = await pollCi(access2, expectedSha);
10809
+ } catch (err) {
10810
+ const pollFailure = ciPollFailureDetailFromError(err);
10811
+ return { ok: false, reason: ciPollFailureReasonFromDetail(pollFailure), pollFailure };
10812
+ }
10813
+ if (allRequiredChecksGreen(pollResponse, requiredChecks)) return { ok: true };
10814
+ if (now() - start >= timeoutMs) return { ok: false, reason: "ci_not_green" };
10815
+ await sleep3(pollIntervalMs);
10816
+ if (signal?.aborted) return { ok: false, reason: "merge_aborted" };
10817
+ }
10818
+ }
10685
10819
  function makeLocalMergeExecutor(options = {}, deps = {}) {
10686
10820
  const method = resolveLocalMergeMethod(options.method);
10687
- const run = deps.runCommand ?? defaultRunCommand2;
10821
+ const rawRun = deps.runCommand ?? defaultRunCommand2;
10822
+ const run = (cmd, args, env) => Promise.resolve(rawRun(cmd, args, env, deps.signal));
10688
10823
  const pollCi = deps.pollCi ?? pollCiChecksForCommit;
10824
+ const sleep3 = deps.sleep ?? defaultSleep;
10825
+ const now = deps.now ?? (() => Date.now());
10826
+ const signal = deps.signal;
10827
+ const ciWaitTimeoutMs = sanitizeWaitMs(options.ciWaitTimeoutMs, DEFAULT_CI_WAIT_TIMEOUT_MS);
10828
+ const ciWaitPollIntervalMs = sanitizeWaitMs(options.ciWaitPollIntervalMs, DEFAULT_CI_WAIT_POLL_INTERVAL_MS);
10689
10829
  const ghEnv = {
10690
10830
  ...deps.env,
10691
10831
  GH_PROMPT_DISABLED: "1",
@@ -10703,9 +10843,28 @@ function makeLocalMergeExecutor(options = {}, deps = {}) {
10703
10843
  merge_method: method,
10704
10844
  executor: "local"
10705
10845
  };
10706
- const fail = (reason) => buildResponse(request, "failed", reason, false, [
10707
- { type: "merge.failed", status: "failed", reason, details: baseDetails }
10708
- ]);
10846
+ const fail = (reason, pollFailure) => {
10847
+ const details = pollFailure ? {
10848
+ ...baseDetails,
10849
+ poll_error: {
10850
+ kind: pollFailure.kind,
10851
+ ...typeof pollFailure.status === "number" ? { status: pollFailure.status } : {},
10852
+ diagnostic: pollFailure.diagnostic
10853
+ }
10854
+ } : baseDetails;
10855
+ return buildResponse(request, "failed", reason, false, [
10856
+ { type: "merge.failed", status: "failed", reason, details }
10857
+ ]);
10858
+ };
10859
+ const verifyMergedStateAfterAmbiguousFailure = async () => {
10860
+ const read = await readPrMergeState(run, ghEnv, pr);
10861
+ if (!read.ok) return null;
10862
+ if (isMergedAtExpectedHead(read.state, read.headOid, expectedSha)) {
10863
+ return buildAlreadyMergedResponse(request, baseDetails, read.mergeCommitOid);
10864
+ }
10865
+ return null;
10866
+ };
10867
+ if (signal?.aborted) return fail("merge_aborted");
10709
10868
  if (options.approvalRequired) {
10710
10869
  return buildResponse(request, "pending_approval", "local_merge_approval_required", false, [
10711
10870
  {
@@ -10716,66 +10875,89 @@ function makeLocalMergeExecutor(options = {}, deps = {}) {
10716
10875
  }
10717
10876
  ]);
10718
10877
  }
10719
- const view = run("gh", ["pr", "view", String(pr), "--json", "headRefOid,state"], ghEnv);
10720
- if (view.timedOut) return fail("gh_pr_view_timeout");
10721
- if (view.status !== 0) return fail("gh_pr_view_failed");
10722
- let headOid;
10723
- let state;
10724
- try {
10725
- const parsed = JSON.parse(view.stdout);
10726
- headOid = parsed.headRefOid;
10727
- state = parsed.state;
10728
- } catch {
10729
- return fail("gh_pr_view_unparseable");
10878
+ const firstRead = await readPrMergeState(run, ghEnv, pr);
10879
+ if (!firstRead.ok) {
10880
+ const verified = await verifyMergedStateAfterAmbiguousFailure();
10881
+ if (verified) return verified;
10882
+ return fail(firstRead.reason);
10883
+ }
10884
+ if (isMergedAtExpectedHead(firstRead.state, firstRead.headOid, expectedSha)) {
10885
+ return buildAlreadyMergedResponse(request, baseDetails, firstRead.mergeCommitOid);
10730
10886
  }
10731
- if (typeof state === "string" && state.toUpperCase() !== "OPEN") return fail("pr_not_open");
10732
- if (typeof headOid !== "string" || headOid.toLowerCase() !== expectedSha.toLowerCase()) {
10887
+ if (typeof firstRead.headOid !== "string" || firstRead.headOid.toLowerCase() !== expectedSha.toLowerCase()) {
10733
10888
  return fail("head_drift");
10734
10889
  }
10735
- let pollResponse;
10736
- try {
10737
- pollResponse = await pollCi(access2, expectedSha);
10738
- } catch {
10739
- return fail("ci_poll_failed");
10890
+ if (typeof firstRead.state === "string" && firstRead.state.toUpperCase() !== "OPEN") {
10891
+ return fail("pr_not_open");
10892
+ }
10893
+ const ciWait = await waitForRequiredChecksGreen(
10894
+ pollCi,
10895
+ access2,
10896
+ expectedSha,
10897
+ requiredChecks,
10898
+ ciWaitTimeoutMs,
10899
+ ciWaitPollIntervalMs,
10900
+ sleep3,
10901
+ now,
10902
+ signal
10903
+ );
10904
+ if (!ciWait.ok) {
10905
+ if (isAmbiguousCiPollFailureReason(ciWait.reason)) {
10906
+ const verified = await verifyMergedStateAfterAmbiguousFailure();
10907
+ if (verified) return verified;
10908
+ }
10909
+ return fail(ciWait.reason, ciWait.pollFailure);
10740
10910
  }
10741
- if (!allRequiredChecksGreen(pollResponse, requiredChecks)) return fail("ci_not_green");
10742
- const merge = run(
10911
+ const secondRead = await readPrMergeState(run, ghEnv, pr);
10912
+ if (!secondRead.ok) {
10913
+ const verified = await verifyMergedStateAfterAmbiguousFailure();
10914
+ if (verified) return verified;
10915
+ return fail(secondRead.reason);
10916
+ }
10917
+ if (isMergedAtExpectedHead(secondRead.state, secondRead.headOid, expectedSha)) {
10918
+ return buildAlreadyMergedResponse(request, baseDetails, secondRead.mergeCommitOid);
10919
+ }
10920
+ if (typeof secondRead.headOid !== "string" || secondRead.headOid.toLowerCase() !== expectedSha.toLowerCase()) {
10921
+ return fail("head_drift");
10922
+ }
10923
+ if (typeof secondRead.state === "string" && secondRead.state.toUpperCase() !== "OPEN") {
10924
+ return fail("pr_not_open");
10925
+ }
10926
+ if (signal?.aborted) return fail("merge_aborted");
10927
+ const merge = await run(
10743
10928
  "gh",
10744
10929
  ["pr", "merge", String(pr), `--${method}`, "--match-head-commit", expectedSha],
10745
10930
  ghEnv
10746
10931
  );
10747
10932
  if (merge.status !== 0) {
10748
10933
  if (merge.timedOut) {
10934
+ const verified = await verifyMergedStateAfterAmbiguousFailure();
10935
+ if (verified) return verified;
10749
10936
  const reason = "gh_merge_timeout";
10750
10937
  return buildResponse(request, "failed", reason, false, [
10751
10938
  { type: "merge.attempted", status: "attempted", details: baseDetails },
10752
10939
  { type: "merge.failed", status: "failed", reason, details: baseDetails }
10753
10940
  ]);
10754
10941
  }
10755
- let mergeability = { mergeable: null, mergeStateStatus: null };
10756
- let isConflict = isLikelyGhMergeConflictOutput(merge);
10757
- if (!isConflict) {
10758
- const recheck = run("gh", ["pr", "view", String(pr), "--json", "mergeable,mergeStateStatus"], ghEnv);
10759
- if (recheck.status === 0) {
10760
- try {
10761
- mergeability = parseGhPrMergeabilityFields(JSON.parse(recheck.stdout));
10762
- isConflict = isPrMergeConflict(mergeability);
10763
- } catch {
10764
- }
10765
- }
10942
+ if (isLikelyGhMergeConflictOutput(merge)) {
10943
+ return buildConflictResponse(request, baseDetails, expectedSha, {
10944
+ mergeable: null,
10945
+ mergeStateStatus: null
10946
+ });
10766
10947
  }
10767
- if (isConflict) {
10768
- const reason = "gh_merge_conflict";
10769
- const conflictDetails = {
10770
- ...baseDetails,
10771
- head_sha: expectedSha,
10772
- ...mergeability.mergeable ? { mergeable: mergeability.mergeable } : {},
10773
- ...mergeability.mergeStateStatus ? { mergeStateStatus: mergeability.mergeStateStatus } : {}
10774
- };
10775
- return buildResponse(request, "failed", reason, false, [
10776
- { type: "merge.attempted", status: "attempted", details: baseDetails },
10777
- { type: "merge.conflict", status: "failed", reason, details: conflictDetails }
10778
- ]);
10948
+ const recheck = await readPrMergeState(run, ghEnv, pr, PR_STATE_MERGEABILITY_JSON);
10949
+ if (recheck.ok) {
10950
+ if (isMergedAtExpectedHead(recheck.state, recheck.headOid, expectedSha)) {
10951
+ return buildAlreadyMergedResponse(request, baseDetails, recheck.mergeCommitOid);
10952
+ }
10953
+ let mergeability = { mergeable: null, mergeStateStatus: null };
10954
+ try {
10955
+ mergeability = parseGhPrMergeabilityFields(recheck.raw);
10956
+ } catch {
10957
+ }
10958
+ if (isPrMergeConflict(mergeability)) {
10959
+ return buildConflictResponse(request, baseDetails, expectedSha, mergeability);
10960
+ }
10779
10961
  }
10780
10962
  const mergeFailReason = "gh_merge_failed";
10781
10963
  return buildResponse(request, "failed", mergeFailReason, false, [
@@ -10784,13 +10966,10 @@ function makeLocalMergeExecutor(options = {}, deps = {}) {
10784
10966
  ]);
10785
10967
  }
10786
10968
  let mergeCommitSha;
10787
- const post = run("gh", ["pr", "view", String(pr), "--json", "mergeCommit"], ghEnv);
10969
+ const post = await run("gh", ["pr", "view", String(pr), "--json", "mergeCommit"], ghEnv);
10788
10970
  if (post.status === 0) {
10789
10971
  try {
10790
- const oid = JSON.parse(post.stdout)?.mergeCommit;
10791
- if (oid && typeof oid === "object" && typeof oid.oid === "string") {
10792
- mergeCommitSha = oid.oid;
10793
- }
10972
+ mergeCommitSha = extractMergeCommitOid(JSON.parse(post.stdout));
10794
10973
  } catch {
10795
10974
  }
10796
10975
  }
@@ -10804,14 +10983,31 @@ function makeLocalMergeExecutor(options = {}, deps = {}) {
10804
10983
  ]);
10805
10984
  };
10806
10985
  }
10807
- var MERGE_METHODS, DEFAULT_COMMAND_TIMEOUT_MS;
10986
+ function buildConflictResponse(request, baseDetails, expectedSha, mergeability) {
10987
+ const reason = "gh_merge_conflict";
10988
+ const conflictDetails = {
10989
+ ...baseDetails,
10990
+ head_sha: expectedSha,
10991
+ ...mergeability.mergeable ? { mergeable: mergeability.mergeable } : {},
10992
+ ...mergeability.mergeStateStatus ? { mergeStateStatus: mergeability.mergeStateStatus } : {}
10993
+ };
10994
+ return buildResponse(request, "failed", reason, false, [
10995
+ { type: "merge.attempted", status: "attempted", details: baseDetails },
10996
+ { type: "merge.conflict", status: "failed", reason, details: conflictDetails }
10997
+ ]);
10998
+ }
10999
+ var MERGE_METHODS, PR_STATE_JSON, PR_STATE_MERGEABILITY_JSON, DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CI_WAIT_TIMEOUT_MS, DEFAULT_CI_WAIT_POLL_INTERVAL_MS;
10808
11000
  var init_local_merge = __esm({
10809
11001
  "src/conductor/local-merge.ts"() {
10810
11002
  "use strict";
10811
11003
  init_bridge_api_client();
10812
11004
  init_github_mergeability();
10813
11005
  MERGE_METHODS = /* @__PURE__ */ new Set(["squash", "merge", "rebase"]);
11006
+ PR_STATE_JSON = "headRefOid,state,mergeCommit";
11007
+ PR_STATE_MERGEABILITY_JSON = "headRefOid,state,mergeCommit,mergeable,mergeStateStatus";
10814
11008
  DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
11009
+ DEFAULT_CI_WAIT_TIMEOUT_MS = 18e4;
11010
+ DEFAULT_CI_WAIT_POLL_INTERVAL_MS = 5e3;
10815
11011
  }
10816
11012
  });
10817
11013
 
@@ -11728,7 +11924,7 @@ function buildGateMetEventInput(binding, evaluation, runId = null, workerId = nu
11728
11924
  data: { ...evaluation.gateEventData }
11729
11925
  };
11730
11926
  }
11731
- function defaultSleep(ms) {
11927
+ function defaultSleep2(ms) {
11732
11928
  return new Promise((resolve2) => setTimeout(resolve2, ms));
11733
11929
  }
11734
11930
  async function observeWithResolved(binding, access2, gateConfig, deps) {
@@ -11831,7 +12027,7 @@ async function waitForDoneGate(params = {}, deps = {}) {
11831
12027
  const resolveBinding = deps.resolveBinding ?? resolvePrHeadBinding;
11832
12028
  const resolveAccess = deps.resolveAccess ?? (() => resolveConductorBridgeApiAccess({ env: deps.env, cwd: params.worktreePath ?? deps.cwd }));
11833
12029
  const fetchGateConfig = deps.fetchGateConfig ?? _fetchGateConfigDefault;
11834
- const sleep2 = deps.sleep ?? defaultSleep;
12030
+ const sleep3 = deps.sleep ?? defaultSleep2;
11835
12031
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11836
12032
  const timeoutMs = clampInt(params.timeoutMs, WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS, 0, WAIT_FOR_GATE_TIMEOUT_MAX_MS);
11837
12033
  const pollIntervalMs = clampInt(
@@ -11909,7 +12105,7 @@ async function waitForDoneGate(params = {}, deps = {}) {
11909
12105
  };
11910
12106
  }
11911
12107
  const remaining = deadline - Date.now();
11912
- await sleep2(Math.min(pollIntervalMs, Math.max(1, remaining)));
12108
+ await sleep3(Math.min(pollIntervalMs, Math.max(1, remaining)));
11913
12109
  }
11914
12110
  }
11915
12111
  async function observePrCiFromPollResponse(commitRef, pollResponse, deps = {}) {
@@ -13554,7 +13750,7 @@ var init_supervisor_runtime = __esm({
13554
13750
  // src/index.ts
13555
13751
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13556
13752
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13557
- import { z as z8 } from "zod";
13753
+ import { z as z15 } from "zod";
13558
13754
  import { writeFile as writeFile12, mkdir as mkdir12, readFile as readFile13, stat as stat9, rename as rename3, chmod as chmod3, unlink as unlink3, mkdtemp as mkdtemp3, rm as rm3, readdir as readdir3 } from "fs/promises";
13559
13755
  import path32 from "path";
13560
13756
  import os14 from "os";
@@ -13667,6 +13863,13 @@ var PIPELINES = {
13667
13863
  "instruction_file": "frame-goals-and-nfrs.md",
13668
13864
  "description": "Frame business goals, desired end-state, and non-functional requirements (documentary)"
13669
13865
  },
13866
+ {
13867
+ "id": "comp-analysis",
13868
+ "type": "agent_task",
13869
+ "instruction_file": "comp-analysis.md",
13870
+ "description": "Analyze any design comp and map it to existing codebase components/templates/tokens/routes (gated, backend-safe)",
13871
+ "on_error": "warn_and_continue"
13872
+ },
13670
13873
  {
13671
13874
  "type": "agent_task",
13672
13875
  "instruction_file": "draft-and-critique.md",
@@ -14119,13 +14322,14 @@ var PIPELINES = {
14119
14322
  },
14120
14323
  "review-ticket": {
14121
14324
  "name": "review-ticket",
14122
- "description": 'Review a ticket with two rounds of analysis (initial + automatic second opinion by default), evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. The automatic second-opinion step is skippable via --rounds=1 mode (skip_steps: ["second-opinion-review"]).',
14325
+ "description": "Review a ticket, evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. A single request_ticket_review step runs the backend's resolved difficulty-adaptive review policy, which internally runs any second-opinion rounds server-side. Pass --rounds=1|2 to forward an explicit round count that forces the review shape (1 = single pass, 2 = full second-opinion review); omit it to let the backend policy executor decide adaptively.",
14123
14326
  "variables": [
14124
14327
  "ticket_key",
14125
14328
  "docs_dir",
14126
14329
  "base_branch",
14127
14330
  "base_sha",
14128
- "no_refresh_base"
14331
+ "no_refresh_base",
14332
+ "rounds"
14129
14333
  ],
14130
14334
  "steps": [
14131
14335
  {
@@ -14141,23 +14345,10 @@ var PIPELINES = {
14141
14345
  "ticket_number": "{ticket_key}",
14142
14346
  "wait_for_result": true,
14143
14347
  "save_locally": true,
14144
- "provider": "{provider}"
14145
- },
14146
- "description": "Generate combined clarify+critique review (initial)",
14147
- "on_error": "warn_and_continue"
14148
- },
14149
- {
14150
- "type": "mcp_call",
14151
- "id": "second-opinion-review",
14152
- "tool": "request_ticket_review",
14153
- "params": {
14154
- "ticket_number": "{ticket_key}",
14155
- "second_opinion": "auto",
14156
- "wait_for_result": true,
14157
- "save_locally": true,
14158
- "provider": "{provider}"
14348
+ "provider": "{provider}",
14349
+ "rounds": "{rounds}"
14159
14350
  },
14160
- "description": "Generate combined clarify+critique review (second opinion)",
14351
+ "description": "Generate combined clarify+critique review. The backend runs the resolved difficulty-adaptive policy (including any second-opinion rounds) server-side; an explicit rounds value (1|2) forces the shape.",
14161
14352
  "on_error": "warn_and_continue"
14162
14353
  },
14163
14354
  {
@@ -14251,10 +14442,11 @@ var INSTRUCTIONS = {
14251
14442
  "capture-review-decisions.md": 'Capture user decisions on review findings for {ticket_key} using the HTML decision page, then interpretively rewrite the clarifying questions and critique docs and upload both to Jira.\n\n## Step 1: Read source documents\n\nRead the combined review-and-resolution file:\n- `{docs_dir}/review/{ticket_key}-review-and-resolution.md`\n\nIf the file does not exist or is unreadable, stop and report: "Combined review-and-resolution file not found or unreadable. Run the earlier pipeline steps first."\n\nThe combined file existing but containing no actionable items (empty `Needs Scrutiny` and `Open Questions` sections) is **not** a failure condition \u2014 Step 4 handles the no-decisions-needed flow gracefully when `generate_decision_page` is called with empty `actionable_items`.\n\n## Step 2: Map evaluation items to decision page input\n\nTransform the combined review-and-resolution document into `generate_decision_page` JSON input using these mapping rules:\n\n| Evaluation Section | JSON Field | Mapping Rule |\n|---|---|---|\n| Open Questions | `actionable_items` | E-item title \u2192 `question`, `**Source**` \u2192 `source`, `**Original question**` \u2192 `original_question`, `**Why it matters**` \u2192 `why_it_matters`, decision tree branch labels \u2192 `options` (string array, labels only), `**Option consequences**` (parallel to branches) \u2192 `option_consequences`, `**Recommendation explanation**` \u2192 `recommendation_explanation`, combined `**Assessment**` paragraph and `**Codebase Evidence**` bullet list \u2192 `codebase_evidence`, `**Recommendation Index**` \u2192 `recommendation_index` |\n| Needs Scrutiny | `actionable_items` | E-item title \u2192 `question`, `**Source**` \u2192 `source`, `**Original question**` \u2192 `original_question`, `**Why it matters**` \u2192 `why_it_matters`, decision tree branch labels \u2192 `options` (string array, labels only), `**Option consequences**` (parallel to branches) \u2192 `option_consequences`, `**Recommendation explanation**` \u2192 `recommendation_explanation`, combined `**Assessment**` paragraph and `**Codebase Evidence**` bullet list \u2192 `codebase_evidence`, `**Recommendation Index**` \u2192 `recommendation_index` |\n| Confirmed Improvements | `clear_improvements` | E-item title \u2192 `title`, confidence tag \u2192 `confidence`, recommended action \u2192 `action`, `**Source**` from the combined file \u2192 `source` |\n\n**Important**: The `original_question`, `why_it_matters`, `option_consequences`, `recommendation_explanation`, and the collapsed `codebase_evidence` block together replace the old single `context` blob. Each clarity field guides a different facet of the user\'s decision: `original_question` reminds the reviewer what was asked, `why_it_matters` frames the impact, `option_consequences` describe the behavioral outcome of each branch, `recommendation_explanation` motivates the recommended branch, and the closed-by-default `codebase_evidence` block surfaces the Assessment + file:line citations on demand without overwhelming the card.\n\nFor each actionable item, the `options` array is a list of plain label strings extracted from the combined file\'s decision tree branches. The tool auto-generates value keys (`opt-0`, `opt-1`, etc.) and auto-appends a "None of these" option. Do not generate value keys yourself.\n\n## Step 2.5: Auto-approve fast path\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\nIf `auto_approve` is `true` and Step 2 produced at least one actionable item, skip Steps 3\u20136 entirely and synthesize the commit JSON directly:\n\n- `ticket_key`: `{ticket_key}`\n- `general_comment`: `""`\n- `decisions`: an object keyed by each `actionable_items[*].id` from Step 2\'s mapped input. For each item:\n - If `recommendation_index` is a non-negative integer within range of `options`: `choice = "opt-" + recommendation_index`, `chosen_label = options[recommendation_index]`, `comment = ""`, `source` copied from the item.\n - Otherwise (missing, null, or out of range): `choice = "opt-0"`, `chosen_label = options[0]`, `comment = ""`, `source` copied. Never emit `"none"` and never emit `"ask"`.\n\nPost a single chat acknowledgement listing each auto-approved item ID and chosen label, then proceed directly to Step 7 with the synthesized JSON. Step 7\'s "Hard rule" about resolving `ask` items does not apply because no item carries `choice === "ask"`.\n\nIf Step 2 produced zero actionable items, fall through to Step 3 \u2014 Step 4\'s existing `no_decisions_needed` branch handles the empty case correctly.\n\nOtherwise (any value of `auto_approve` other than the literal `true` \u2014 including empty, `false`, or missing), proceed to Step 3.\n\n## Step 3: Call the MCP tool\n\nCall `generate_decision_page` with `ticket_key` at the root and the review arrays nested under `content`:\n\n```typescript\ninterface ReviewDecisionsContent {\n actionable_items?: Array<{\n id: string;\n question: string;\n why_it_matters: string; // required \u2014 concrete one-sentence impact\n recommendation_explanation: string; // required \u2014 why the recommended branch is best\n options: string[]; // 2-4 option labels\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based index into options\n original_question?: string; // optional display field\n codebase_evidence?: string; // optional display field \u2014 assessment + file:line\n source?: string; // optional source reference\n }>;\n clear_improvements?: Array<{\n id: string;\n title: string;\n action: string;\n confidence: string;\n source: string; // required for clear_improvements\n }>;\n}\n```\n\nExample call:\n```json\n{\n "ticket_key": "{ticket_key}",\n "content": {\n "actionable_items": [\n {\n "id": "E-1",\n "question": "Should we add a configurable timeout?",\n "why_it_matters": "Timeout behavior affects retry paths and user-visible latency.",\n "recommendation_explanation": "Configurable matches existing latency-branching code.",\n "options": ["Keep existing", "Add configurable timeout"],\n "option_consequences": ["No new work.", "Implementers add config + tests."],\n "recommendation_index": 1,\n "original_question": "Does the ticket specify timeout behavior?",\n "source": "Clarifying Q1"\n }\n ],\n "clear_improvements": [\n { "id": "ci-1", "title": "Tidy logging", "action": "Use the logger.", "confidence": "high", "source": "Eval 1" }\n ]\n }\n}\n```\n\n## Step 4: Check tool response\n\nThe tool returns a JSON response with a `status` field:\n- If `status` is `"no_decisions_needed"`: skip Steps 5, 6, 7, and 8 entirely. Output a success message: "No actionable review decisions needed \u2014 skipping doc rewrite and upload." This covers both the case where every item was confirmed as a Confirmed Improvement and the case where no items were emitted (e.g., both upstream source documents were absent).\n- If `status` is `"decision_page_generated"`: continue to Step 5. The response includes `file_path`.\n\n## Step 5: Direct user to the decision page\n\nTell the user to open the generated HTML file in their browser. Provide the `file_path` from the tool response. Then say to the user, verbatim: `Open the page. For any item you\'re unsure about, choose "Ask about this" \u2014 when you submit, I\'ll talk through those before we proceed. You can also ask me questions in chat before submitting if you prefer.`\n\nThis step only directs the user to the page and explains the two allowed next actions (submit selections, or ask questions first). Do not describe Step 7\'s rewrite semantics here; that belongs to the rewrite step.\n\n## Step 6: Q&A loop and commit signal\n\nEnter an open-ended Q&A loop. There is no turn cap \u2014 the user may ask any number of questions in any number of turns. Do not stop and wait silently; engage with each user message as either a commit signal or a discussion turn.\n\n### Proceed signal (commit)\n\nTrim the full user message and attempt to parse the entire trimmed message as JSON. The message is a commit only when the parsed value is an object with all three of these top-level fields:\n\n- `ticket_key` \u2014 must be a string\n- `decisions` \u2014 must be an object\n- `general_comment` \u2014 must be a string\n\nThe first valid commit-shaped JSON paste commits immediately. Proceed to Step 7 without prompting for additional confirmation. Any combination of `decisions` keys is accepted (the page may submit a partial set if the user only resolved some items conversationally). Do not over-validate the per-card fields beyond the top-level commit-shape check \u2014 the page guarantees the per-card schema, and over-validating risks rejecting valid pastes if the page schema evolves.\n\n### Discussion signal (Q&A turn)\n\nAnything that is not commit-shaped JSON is a discussion turn. This includes:\n\n- Freeform questions (with or without other text).\n- Questions pasted alongside other text or alongside JSON.\n- Malformed JSON (parse failure).\n- Well-formed JSON missing one or more of the required top-level keys (`ticket_key`, `decisions`, `general_comment`).\n\nFor JSON-shaped input that is missing required top-level fields, call this out in the reply \u2014 explain which fields are missing and ask whether the user intended to submit or share partial state \u2014 rather than silently treating it as a freeform question.\n\nAnswer discussion turns using these sources, in priority order:\n\n1. The combined `{ticket_key}-review-and-resolution.md` file already read in Step 1.\n2. The original `{ticket_key}-clarifying-questions.md` and `{ticket_key}-ticket-quality-critique.md` documents.\n3. Codebase lookups when the question requires verifying current code state.\n\nFallback: if running on a pre-PR1 branch where the combined review-and-resolution document does not exist, use the pre-PR1 `{ticket_key}-review-evaluation.md` and `{ticket_key}-resolution-guide.md` pair in its place.\n\nFor plain freeform questions, infer the item from chat context when possible.\n\n### In-flight decision state\n\nDuring the Q&A loop, maintain in-flight JSON state \u2014 agent-owned working memory representing the user\'s current intent for `decisions` and `general_comment`. This in-flight JSON state lives only in the agent\'s working memory for the duration of the loop; do not persist it server-side.\n\n- When the user clearly changes their mind about an item, chooses an option conversationally with reasonably explicit decision language ("choose option B for E-3", "go with the configurable timeout", "change E-7 to None of these"), or gives new overarching guidance, record that as an in-flight override.\n- Ambiguous preference language ("I\'m leaning toward...", "maybe option B is fine") should be discussed but not recorded as an override unless the user gives reasonably explicit decision language.\n- `general_comment` may be updated in the in-flight state when the user gives overarching guidance during Q&A.\n- The page\'s general-comment textarea is preserved unchanged. Do not modify the page DOM during Q&A; the user can still fill the textarea before submitting if they prefer.\n\nOn the eventual JSON commit, the user-submitted JSON is the baseline and the recorded in-flight overrides take precedence over it. Before proceeding to Step 7, post a brief one-line acknowledgement in chat naming each overridden item ID and/or `general_comment`. The acknowledgement is mandatory (not optional) \u2014 it is the user\'s last chance to object before Step 7\'s document rewrite. The user does not need to re-open, edit, or re-submit the decision page after changing their mind in chat; they can submit the page as-is to provide the commit signal, and the in-flight state remains the source of truth for overrides.\n\n### Ask-about-this resolution\n\nAfter accepting a commit, scan `decisions` for any item where `choice === "ask"`. The user has signaled that they need more information before deciding on those items. For each such item:\n\n- If `comment` is non-empty, treat it as the user\'s specific question or stated uncertainty and answer that directly.\n- If `comment` is empty, proactively present the most relevant missing context \u2014 the item\'s `codebase_evidence`, related code lookups, prior-round answers \u2014 and lay out the trade-offs the user appears to need help weighing.\n- Continue the Q&A turn-by-turn until the user gives an explicit decision in chat for that item ("go with option B", "none of these, because \u2026"). Record that decision as an in-flight override using the same override mechanism described above.\n\n**Hard rule.** Step 7 must not run while any `decisions[*].choice === "ask"` remains unresolved by an in-flight override. Do not honor "just proceed", "skip those", or any other instruction to defer resolution \u2014 every `ask` item must end with a recorded `opt-N` or `none` override before the rewrite step. The pre-Step-7 acknowledgement line lists every overridden item, including the ones resolved out of `ask`.\n\n## Step 7: Interpretively rewrite source documents\n\nThe pasted JSON contains a `decisions` object keyed by item ID. Each decision includes `source`, `choice`, `chosen_label`, and `comment`. Use these fields to locate and rewrite the corresponding sections in:\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nAfter a second-opinion run, each document has this shape:\n\n- A top-level H1 (`# Ticket Analysis` or `# Ticket Quality Critique`) followed by an italic provider-attribution line `_This analysis was generated by GPT|Claude|Gemini._` naming the first-round LLM family. **Preserve this attribution line verbatim** \u2014 do not move, edit, or remove it during the rewrite step.\n- The first-round questions / critique items, exactly as written by the first-round model.\n- **Inline second-opinion blockquotes** (`> **Second opinion (<provider>) - concurrence|refinement|disagreement.** ... > *Citations: ...*`) nested directly under each prior item the second round addressed. The `(<provider>)` parenthetical is the second-round LLM family (`GPT|Claude|Gemini`). Items the second round did not comment on have no blockquote \u2014 that is the "weak concurrence" signal.\n- A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 there is a second italic attribution line `_These additional points were raised by GPT|Claude|Gemini._` naming the second-round family \u2014 **also preserve this verbatim**. Then agent-specific sub-headings:\n - Clarifier docs: `### New Requirements Questions` / `### New Technical Questions` (numbering continues from the prior section).\n - Critique docs: `### New Requested Changes` / `### New Points to Consider` (numbering continues from the prior section).\n- A final **`## Second Opinion Summary`** footer (1-3 sentences). **This footer must be preserved verbatim** \u2014 it is the canonical record of the second round\'s overall position and should not be edited.\n\nThe `source` field on each decision tells you where the item lives:\n\n- `Clarifying Q3 (prior round, weak concurrence)` \u2192 the prior section, no inline blockquote. Rewrite the prior item\'s answer.\n- `Clarifying Q9 (prior round, concurrence inline)` \u2192 the prior section, prior item carries an explicit `concurrence` blockquote. Rewrite the prior answer; the blockquote can be removed once the answer absorbs the resolution.\n- `Clarifying Q3 (prior round, refinement inline)` / `(prior round, disagreement inline)` \u2192 the prior section, prior item carries an explicit `refinement` or `disagreement` blockquote. Rewrite the prior answer to reconcile the dispute, then handle the blockquote per the rule below.\n- `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` \u2192 the `## New in Second Opinion > ### New Requirements Questions` sub-section. Rewrite the item in place inside that sub-section, not at the top of the prior analysis.\n- Equivalent forms for critique items: `Critique: Requested Change 2 (prior round, refinement inline)`, `Critique: Points to Consider N+1 (new in second opinion \u2192 New Points to Consider)`, etc.\n\n**Legacy fallback shape**: if the document instead ends with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section (because the JSON pipeline fell back), apply decisions to the equivalent location: `### Response to Prior Items` for inline-style responses, `### Additional Points > New X` for tail-style new items. Preserve the `\\n\\n---\\n\\n` separator and the `## Second Opinion` heading verbatim.\n\nApply the decision to the item in its home location. Then apply the decision:\n\n### Actionable item decisions\n\n- **Selected option** (`choice` is `opt-N`): Add `**Review Decision**: Accepted. <chosen_label>.` to the corresponding section. Integrate the selected direction into the section text so it reads as a final recommendation or resolved answer.\n- **None of these** (`choice` is `none`): Add `**Review Decision**: Rejected \u2014 none of the proposed options accepted.` Include the user\'s `comment` explaining why. Rewrite the section to reflect this decision.\n\nFor actionable items sourced from clarifying questions, rewrite the question\'s best-guess answer so it reads as the final resolved direction chosen by the reviewer. Do not leave the item framed as an unresolved accept/reject/modify prompt.\n\nFor items sourced from `(prior round, refinement inline)` or `(prior round, disagreement inline)` \u2014 disputes of a prior-round item carried in an inline blockquote \u2014 the prior-round item is the canonical home: rewrite its answer to absorb the resolution. Then handle the blockquote in one of two ways: (a) remove the blockquote outright if the rewritten answer fully absorbs the second-opinion content, or (b) shorten the blockquote to a single sentence noting the resolution while preserving the `(<provider>)` attribution (e.g. `> **Second opinion (Claude) - refinement.** Resolved by reviewer decision E-N.`). Citations from the original blockquote may be promoted into the rewritten prior-item answer if useful \u2014 keep the strongest 1-2 grounding refs.\n\nFor items sourced from `(new in second opinion \u2192 ...)` \u2014 gap-captured items that received a decision \u2014 rewrite the item in place inside its tail-block sub-section (`## New in Second Opinion > ### New X`), not at the top of the prior analysis. Preserve the sub-section heading and continued numbering.\n\n### General comment handling\n\nTreat `general_comment` as overarching guidance that informs the tone and direction of both document rewrites. If it contains specific actionable feedback, weave it into the relevant sections. If it is broad or general, use it as context for how the rewrites should read. Do not create a separate "General Comment" or "Reviewer Notes" section \u2014 the goal is "final draft" form.\n\n### Rewrite principles\n\nThe goal is a **final draft** \u2014 the documents should read as if they were written with the decisions already made. Do not mechanically append decisions. Instead, lightly rewrite affected sections so they reflect the decisions naturally. Preserve all non-affected sections unchanged. The prior-round content should still read as coherent standalone analysis after integration. Preserve the `## New in Second Opinion` tail block intact for any items that weren\'t decided. **Always preserve the `## Second Opinion Summary` footer verbatim** \u2014 it is the canonical record of the second round\'s overall position and should not be edited even when individual items it references have been resolved.\n\n## Step 8: Upload to Jira\n\nUpload both updated documents to Jira using `attachment` (operation: `"upload"`):\n\n1. Upload clarifying questions:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md`\n - `link_type`: `clarifying-questions.md`\n\n2. Upload ticket quality critique:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n - `link_type`: `ticket-quality-critique.md`\n\n## Step 9: Complete\n\nConfirm: "Review decisions captured and uploaded to {ticket_key}."\n\n## Return\n\nConfirm "Review decisions captured and uploaded to {ticket_key}." and list the two attachments uploaded (`{ticket_key}-clarifying-questions.md` and `{ticket_key}-ticket-quality-critique.md`). Note any decisions that could not be applied.\n',
14252
14443
  "clarify-open-nfrs.md": 'Proactively clarify any open non-functional requirements with the user via an interactive decision page before decomposing the epic. Clear goals and a clear desired end-state make the functional decomposition far more accurate, so resolve the unclear NFRs first.\n\n## Inputs\n\n- The framing written by the previous step: `{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md`.\n\n## Instructions\n\n1. Read `{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md`. Collect the NFRs marked `open` and note the business goal, desired end-state, and system behavior.\n\n2. **If there are no `open` NFRs**, skip the decision page entirely. Note that no clarification was needed and proceed (return). Do not generate a page just to fill it.\n\n3. **If there is at least one `open` NFR**, build the inputs for an interactive planning decision page:\n - `system_goals` (read-only): `business_goal`, `desired_end_state`, `system_behavior`, and `nfrs` \u2014 the full classified NFR list, each with `category`, `requirement`, `implication`, and `status`.\n - `actionable_items`: one card per `open` NFR. Each card has:\n - `id`: a short stable id, e.g. `NFR-1`, `NFR-2`.\n - `question`: the decision the open NFR poses (e.g. "What latency budget must the harvester meet?").\n - `options`: 2\u20134 concrete option labels. Do **not** include "None of these" or "Ask about this" \u2014 the renderer auto-appends both. If there is one obvious answer, still provide the strongest alternative as a second option.\n - `option_consequences`: one consequence line per option, parallel to and the same length as `options`.\n - `why_it_matters`: the concrete impact of the decision.\n - `recommendation_explanation`: why the recommended option is best.\n - `recommendation_index`: the 0-based index of the recommended option.\n\n4. **Call `generate_decision_page`** with `ticket_key`, `artifact_type`, routing fields, and `labels` at the root, and `system_goals` + `actionable_items` nested under `content`:\n - `artifact_type`: `pre_ticket_planning`.\n - `ticket_key`: `{epic_slug}`.\n - `output_subdir`: `epic-plans/{epic_slug}`.\n - `output_filename`: `{epic_slug}-nfr-decisions.html`.\n - `labels`: planning-flavored overrides, e.g. `title` = "Epic Planning Decisions", `section_heading` = "Open Non-Functional Requirements", and an `intro` that frames the page as settling the goals and NFRs before decomposition.\n - `content`: an object containing `system_goals` and `actionable_items` from step 3. (Omit `implementation_order` \u2014 the order is produced after decomposition.)\n\n ```typescript\n interface NfrPlanningContent {\n system_goals?: {\n business_goal: string;\n desired_end_state: string;\n system_behavior: string;\n nfrs?: Array<{\n category: string; // e.g. "security/privacy", "performance/latency"\n requirement: string;\n implication: string; // required \u2014 what this changes about the implementation\n status: "confirmed" | "assumed" | "open";\n }>;\n };\n actionable_items?: Array<{\n id: string; // e.g. "NFR-1", "NFR-2"\n question: string;\n why_it_matters: string;\n recommendation_explanation: string;\n options: string[]; // 2-4 option labels\n option_consequences: string[]; // same length as options\n recommendation_index: number;\n }>;\n }\n ```\n\n Example call:\n ```json\n {\n "ticket_key": "{epic_slug}",\n "artifact_type": "pre_ticket_planning",\n "output_subdir": "epic-plans/{epic_slug}",\n "output_filename": "{epic_slug}-nfr-decisions.html",\n "labels": { "title": "Epic Planning Decisions", "section_heading": "Open Non-Functional Requirements" },\n "content": {\n "system_goals": {\n "business_goal": "Reduce MCP token tax to improve agent context efficiency.",\n "desired_end_state": "Core profile uses fewer than 15k tokens per session.",\n "system_behavior": "On-demand contract delivery with no schema round-trips.",\n "nfrs": [\n { "category": "performance/latency", "requirement": "No latency regression", "implication": "Validate in handler, not at boundary", "status": "confirmed" },\n { "category": "security/privacy", "requirement": "Errors never leak into HTML", "implication": "Use JSON envelope only", "status": "confirmed" }\n ]\n },\n "actionable_items": [\n {\n "id": "NFR-1",\n "question": "What latency budget must the harvester meet?",\n "why_it_matters": "Sets the retry window for downstream consumers.",\n "recommendation_explanation": "Under 30s matches existing SLA.",\n "options": ["Under 30s", "Under 60s"],\n "option_consequences": ["Tight but achievable.", "Relaxed, may delay alerts."],\n "recommendation_index": 0\n }\n ]\n }\n }\n ```\n\n5. **Capture the user\'s choices (stop and wait).** Direct the user to the returned `file_path`, tell them to open it and submit. Treat a paste as a commit only when it is a JSON object with all three top-level fields: `ticket_key` (string), `decisions` (object), and `general_comment` (string). For any item where `choice === "ask"`, discuss until the user gives an explicit decision before finalizing. You MUST stop and wait for the user to respond \u2014 do NOT assume answers and do NOT proceed until the open NFRs are resolved or the user explicitly declines.\n\n6. **Fold the answers back into the framing.** Rewrite `{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md` so each resolved NFR\'s `status` moves from `open` to `confirmed` (or `assumed` when the user chose a provisional default), recording the chosen resolution in the `requirement`/`implication`. Weave `general_comment` in as overarching guidance. Leave settled sections unchanged.\n\nThis step is non-blocking only insofar as the user may explicitly decline; if `generate_decision_page` fails, log a warning, direct the user to the markdown framing instead, and continue.\n\n## Return\n\nReport whether a planning decision page was generated (and its path) or skipped because there were no open NFRs, and how many open NFRs were resolved into `confirmed`/`assumed`.\n',
14253
14444
  "commit-and-push.md": 'Stage, commit, and push implementation changes for ticket {ticket_key}.\n\nBefore executing, assess the git state and present a clear plan for user approval.\n\n## Step 1 \u2014 Assess Git State\n\nRun these commands and note the results:\n- `git branch --show-current` \u2014 record the current branch name\n- `git status --porcelain` \u2014 identify all modified, added, and untracked files\n\n## Step 2 \u2014 Determine Branch\n\nDecide the branching strategy and be prepared to state it explicitly. Cover:\n\n- Whether you will commit on the current branch, or create a new branch.\n- If creating a new branch: the exact new branch name, and which branch it will be created from (current branch vs. `main`).\n- If branching from `main`: whether `main` needs to be pulled/updated first, and the command you will run.\n- Whether the target branch already exists remotely (and if so, whether you will push to the existing remote branch).\n\nDefault rules:\n\n- If the current branch already contains `{ticket_key}` (case-insensitive), plan to commit on the current branch.\n- Otherwise, plan to create a new branch named `feature/{ticket_key}` from the current branch.\n\n## Step 3 \u2014 Prepare Commit Details\n\n- Separate implementation files from unrelated changes. Only stage files related to the ticket.\n- Compose a commit message: `{ticket_key}: <brief description of what was implemented>`\n\n## Step 4 \u2014 Present Plan for Approval (commit, push, and PR)\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\n**Auto-approve mode.** If `auto_approve` is `true`, do NOT present the approval plan and do NOT wait for user input. Apply the default branching rule from Step 2 (commit on the current branch if it contains `{ticket_key}` case-insensitively; otherwise create `feature/{ticket_key}` from the current branch). Stage all files reported by `git status --porcelain` that you assess as related to the ticket per Step 3\'s "Only stage files related to the ticket" rule (when uncertain, prefer including over excluding \u2014 auto-approve trades caution for momentum, and the user has explicitly opted in). Use the commit-message format from Step 3. Skip directly to Step 5 and execute.\n\nOtherwise (any value of `auto_approve` other than the literal `true` \u2014 including empty, `false`, or missing), proceed with the existing approval flow below.\n\nPresent a single approval plan covering the commit, push, and pull request creation before proceeding:\n\n```\nCommit Plan for {ticket_key}\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCurrent branch: <current branch name>\nBranching: - <"Commit on current branch" | "Create new branch `<name>` from `<source branch>`">\n - <if branching from main: "Pull latest main first via `git checkout main && git pull`" | omit if N/A>\n - <"Remote branch already exists \u2014 will push to existing" | "New remote branch \u2014 will push with -u" | omit if N/A>\nFiles to stage: <count> files\n - path/to/file1.py\n - path/to/file2.py\nExcluded: <any unrelated changed files, or "None">\nCommit message: {ticket_key}: <description>\nPush to: origin/<target branch>\nPR title: <commit subject \u2014 derived automatically after commit>\nPR base: main\n```\n\nWait for the user to approve, request changes, or reject. The user may adjust the branch name, file inclusion, commit message, PR title, PR base, or give other instructions. The PR title defaults to the commit subject after the commit is made, and the PR base defaults to `main`.\n\nDo not proceed until the user explicitly approves.\n\n## Step 5 \u2014 Execute\n\n1. If creating a new branch, run `git checkout -b <branch name>`.\n2. Stage approved files with `git add <file1> <file2> ...` \u2014 do not use `git add -A` or `git add .`.\n3. Commit with the approved message.\n4. Push with `git push -u origin <branch>`.\n\n## Return\n\nConfirm the commit was made and pushed by reporting the branch name, the commit subject line, and the pushed remote (e.g. `origin/feature/{ticket_key}`). Note any files that were intentionally excluded from the commit.\n',
14445
+ "comp-analysis.md": 'Perceive any attached/referenced design comp with your OWN vision and map it to the existing codebase BEFORE the `jira-ticket-writer` drafts. This is the pre-writer perception step: the orchestrating recipe agent (already a frontier vision model) opens the comp, classifies it against the shared fidelity taxonomy, researches the code, and writes a structured comp\u2192codebase map the writer consumes. The writer stays text-only and never opens images \u2014 it only reads the map you produce here.\n\nThis step is gated and backend-safe. It runs the perception with the ORCHESTRATING agent\'s own vision \u2014 a local image via the Read tool, or an already-attached Jira comp fetched as raw bytes via the BAPI-562 binary-safe `attachment` download (operation `download`) into a worktree `file_path`. It does NOT call `describe_image()`, does NOT depend on `src/python/llms/vision.py`, and does NOT require the `CHEAP_MODEL` vision path or any vision-model upgrade or procurement. The heavy visual reasoning is done for free by the agent that already runs the recipe.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` (source of truth for `idea`, `scope`, `readiness`). Read whichever prior artifacts exist under `{docs_dir}/idea-to-ticket/{slug}-{run_id}/` (research pack, resolved uncertainties, goals-and-nfrs, standards checklist) and proceed without the ones that do not.\n- Output artifact (this step writes it): `{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json` \u2014 the structured comp\u2192codebase map the drafting step reads.\n\n## Instructions\n\n1. **Gate first \u2014 evaluate only text/material metadata, never open an image yet.** Proceed to any image work ONLY when BOTH conditions hold:\n - a design comp is **referenced or attached** (a local image path in the idea/materials, or an already-attached Jira comp with an `attachment_id`), AND\n - the requested work is **design/UI work** (a frontend/fullstack change with a visual surface).\n\n This gate is deliberately conservative. A **backend-only** request, a **no-comp** request, or a **non-design** request fails the gate.\n\n2. **Not-applicable branch (gate fails) \u2014 short-circuit immediately.** When either gate condition is not met, write `comp-analysis.json` with `applicable: false`, a short `reason`, and the `gate` evidence fields, then **return immediately** \u2014 do NOT open any image, do NOT download any attachment, do NOT inject any fidelity/comp/visual language, and add no measurable latency. A backend-only or no-comp authoring input must be byte-for-byte unaffected downstream. Concretely, the not-applicable artifact is:\n\n ```json\n { "applicable": false, "reason": "backend-only work; no design comp referenced", "gate": { "comp_referenced": false, "design_ui_work": false }, "warnings": [] }\n ```\n\n3. **Applicable branch \u2014 obtain and OPEN the comp with your own vision.** When the gate passes:\n - **Local image path** \u2192 open it directly with the Read tool.\n - **Already-attached Jira comp** \u2192 fetch it as raw bytes using the BAPI-562 binary-safe `attachment` capability with operation `download`, passing the comp\'s `attachment_id` and a worktree `file_path`; the download saves the PNG/JPEG bytes to that `file_path` inside the project root, then open the saved file with the Read tool.\n - You are the orchestrating vision model \u2014 reason over the actual pixels yourself. Do NOT call `describe_image()`, do NOT use `src/python/llms/vision.py` / `CHEAP_MODEL`, and do NOT delegate perception to the text-only `jira-ticket-writer` subagent.\n\n4. **Degraded paths are skip-clean and warn-not-halt.** If the comp is missing, unreadable, an unsupported format, external-only (an `http(s)` URL you cannot fetch into a worktree `file_path`), or turns out not to be a usable design comp, write a valid `comp-analysis.json` with `applicable: false`, a `reason`, and a populated `warnings` array describing what failed, then return. A missing, unreadable, or non-comp image NEVER blocks downstream drafting and NEVER requires further image work after the failure.\n\n5. **Classify the opened comp using the shared fidelity taxonomy (do not fork it).** Using your own vision, classify the comp as exactly one of these four classes \u2014 the same labels the downstream final plan reviewer uses (`src/python/llms/agents/planner_agent/final_plan_review_agent.py`, `_get_runtime_verification_instructions`), so authoring, planning, and implementation all agree:\n - `full comp`\n - `wireframe`\n - `annotated-screenshot-of-existing-UI`\n - `unknown`\n\n Record `fidelity_classification` with a `class` (one of the four), a `confidence` value, and a short `reasoning` string.\n\n6. **Apply class-appropriate mapping depth \u2014 no over-specification.** Match the per-class rules exactly:\n - **full comp** (confident) \u2192 map exact existing components, Jinja2 templates, SCSS/CSS tokens, and routes; strict/exact component + token depth is used ONLY here.\n - **wireframe** \u2192 map layout and structure only (regions, order, rough proportions, responsive behavior); defer color, type, spacing, and component polish to the repo design system, NOT to the wireframe.\n - **annotated-screenshot-of-existing-UI** \u2192 map ONLY the delta against the current UI; preserve everything outside the annotated region and do not reproduce the screenshot wholesale.\n - **unknown / low confidence** \u2192 fall back to the design-system floor rather than mapping pixels.\n\n **Hard rule:** strict/exact mapping depth is enabled ONLY for a confidently-classified full comp. Fail toward the design system, never toward reproducing an ambiguous image.\n\n7. **Research the codebase and map each region/element to concrete existing code.** Inspect the working tree (search, grep, file reads) for the existing UI implementation surfaces: reusable components, Jinja2 templates, plain CSS/SCSS tokens and design-system styles, routes, and reusable frontend patterns. Only cite files you actually inspected \u2014 do not invent file paths, component names, tokens, or routes. For each region/element of the comp, produce a `mappings[]` entry containing:\n - `region` \u2014 the comp region or element name.\n - `visual_description` \u2014 a short description of what it looks like.\n - `components` \u2014 mapped existing component file(s).\n - `templates` \u2014 mapped existing Jinja2 template(s).\n - `tokens` \u2014 style/token references (SCSS/CSS tokens or design-system styles).\n - `routes` \u2014 route reference(s) where the element lives or should wire.\n - `confidence` \u2014 confidence for this mapping.\n - `implementation_guidance` \u2014 concrete guidance (e.g. "reuse component X", "extend template Y", "use token Z", "wire route R").\n\n8. **Write the structured map to a stable schema.** Write `comp-analysis.json` at `{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json` with these fields:\n - `applicable` \u2014 boolean; `true` only after a comp was both detected AND successfully opened.\n - `reason` \u2014 short string explaining the applicability decision.\n - `gate` \u2014 the two-condition gate evidence (`comp_referenced`, `design_ui_work`).\n - `comp` \u2014 provenance of the opened comp (source kind, path or `attachment_id`, filename, MIME type when known).\n - `fidelity_classification` \u2014 `{ class, confidence, reasoning }`, present only when `applicable` is true.\n - `mappings` \u2014 array of the per-region entries defined in step 7 (present only when `applicable` is true).\n - `design_system_floor` \u2014 the design-system fallback guidance to use for wireframe/unknown/low-confidence regions.\n - `warnings` \u2014 array of degraded-path notes (may be empty).\n - `writer_guidance` \u2014 instructions for the downstream drafting step: when `applicable` is `true`, Requirements must cite the mapped components/templates/tokens/routes as concrete implementation guidance; when `applicable` is `false` or the map is missing, the writer must ignore this artifact and mention no comp analysis, design comp, or visual-fidelity language unless the original request independently requires it.\n\n9. **Never over-write.** Write the artifact exactly once at the path above. Downstream steps (`draft-and-critique.md`) read it; do not move it.\n\n## Return\n\nConfirm the path written (`{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json`), whether the analysis was `applicable`, the fidelity `class` when applicable, and any `warnings`.\n',
14254
14446
  "create-pr.md": '# Create a pull request for the just-pushed branch\n\nThe implementation has been committed and pushed. Open a PR against `main` for the current branch, with a descriptive title derived from the commit you just made.\n\n## Step 1 \u2014 Read the commit subject line\n\nRun `git log -1 --pretty=%s` to get the most recent commit subject. The implement-ticket pipeline asks the commit step to use the form `{ticket_key}: <description>`, so this line is normally already a good PR title.\n\n## Step 2 \u2014 Determine the head branch\n\nUse `git branch --show-current`. This is the head branch.\n\n## Step 2.5 \u2014 Run the file-scope guard (warn-only, before opening the PR)\n\nBefore opening or updating the PR, run the conductor file-scope guard so an\nout-of-scope diff is surfaced in the PR-creation context:\n\n```bash\nnode "$BAPI_CONDUCTOR_CLI_FILE" file-scope-guard\n```\n\n(or `conductor file-scope-guard` if the packaged binary is on PATH).\n\nThis guard is **warn-only and fail-open** \u2014 it **always exits 0 and never blocks\nPR creation** in v1. Behavior:\n\n- If `BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON` is **absent, empty, or invalid**,\n the guard is a **no-op** \u2014 it prints nothing and you **continue** to Step 3\n normally. Do not treat a missing declaration as an error.\n- If the guard prints a warning that your branch diff touched files **outside** the\n ticket\'s declared touched-file set, **surface that warning in the PR-creation\n context** (include it in your summary to the user / PR notes) but **do not block**\n \u2014 proceed to open the PR. The warning is advisory: it flags a possible\n sibling-scope over-reach for human review, it does not stop the workflow.\n\n## Step 3 \u2014 Call create_pull_request and report the PR URL\n\nCall the `create_pull_request` MCP tool directly with:\n\n- `head_branch`: value from `git branch --show-current`\n- `base_branch`: `"main"` \u2014 unless the user supplied a different PR base at the commit step, in which case use that value instead.\n- `title`: the commit subject from Step 1 (the derived PR title) \u2014 unless the user supplied a different PR title at the commit step, in which case use that value instead.\n\nHonor any PR title / PR base overrides the user gave at the commit step\'s plan; the commit step advertises those fields as adjustable, so any override the user gave there must carry forward into this tool call rather than being silently replaced by the defaults above.\n\nDo not pass a `body` parameter so the project\'s `.github/PULL_REQUEST_TEMPLATE.md` populates the description.\n\nReport the returned `pr_url` to the user.\n\n## Return\n\nReturn the URL of the created pull request.\n',
14255
14447
  "decompose-epic-candidate.md": 'Decompose an Epic parent draft into ordered child tickets with idempotency and per-child duplicate checks.\n\n## Inputs\n\n- Epic parent draft: `{docs_dir}/tickets/EPIC-{slug}.md`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n- Goals & NFR framing: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/goals-and-nfrs.md` (its System Goals/NFRs and any provisional Recommended Implementation Order should inform the child breakdown and ordering).\n- Hard cap variable `{max_children}` (string integer; default `"10"` when not set by the caller). The default `"10"` is a **hard ceiling / upper bound, not a target** child count \u2014 it caps how many children are allowed, and is **not a goal to fill**. The normal target child count is smaller (fewer, larger M/L slices); see the sizing heuristics in step 2.\n\n## Instructions\n\n1. Read the Epic parent draft, research pack, standards checklist, and resolved uncertainties. Use only this context plus optional narrow web search; do not call deep research from this step.\n\n2. Propose ordered child tickets that, together, fully implement the Epic.\n\n **Sizing heuristics (maintainer-owned defaults).** Size each proposed child by its expected **file-touch breadth and depth plus rough lines of code (LOC) changed**, using these exact thresholds:\n - `S = 1\u20132 files / <~80 LOC`\n - `M = ~3\u20138 files / ~80\u2013400 LOC (ideal target)`\n - `L = ~8\u201315 files / ~400\u2013900 LOC (acceptable)`\n - `XL = >15 files / >~900 LOC \u2192 split further; never emit an XL child`\n\n Target size priority: `M (ideal) \u2192 L (acceptable) \u2192 S (only if unavoidable); never XL`.\n\n Bias the decomposition toward **fewer, larger, independently implementable vertical slices** rather than many tiny one-feature children. The Bridge implementation tooling works better on M\u2013L vertical slices, and a swarm of tiny S children magnifies sibling merge risk under parallel execution. Each child should be an independently implementable vertical slice; if a proposed child would be XL, split it further until each piece is M or L.\n\n Each proposed child must include:\n - `summary` \u2014 Jira title.\n - `issue_type` \u2014 typically `Task`; use `Spike` only for primarily discovery children.\n - `rationale` \u2014 short explanation of why this child exists and what it produces. Include a brief size estimate inside this existing field (do **not** add a new `size` field), e.g. `Estimated size: M (~4 files / ~150 LOC)`.\n - `labels` \u2014 must include `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and the unique child idempotency label `bapi-idea-to-ticket-{run_id}-child-<N>` where `<N>` is the 1-based child index in the final ordered list.\n - `idempotency_label` \u2014 the same `bapi-idea-to-ticket-{run_id}-child-<N>` string.\n - `draft_path` \u2014 `{docs_dir}/tickets/TICKET-{slug}-child-<N>.md` (drafts written by `jira-ticket-writer` later).\n - `depends_on` \u2014 array of the 1-based child indexes that are **hard prerequisites** (must land first), or empty. Keep this list minimal and real.\n - `recommended_after` \u2014 array of child indexes that are **soft sequencing** preferences (nicer to do after, but not blockers), or empty.\n - `order_rationale` \u2014 one line explaining why this child sits at this point in the order.\n\n Keep hard prerequisites (`depends_on`) strictly separate from soft sequencing (`recommended_after`). These fields drive the recommended implementation order posted to the epic later; they do **not** create Jira dependency links.\n\n3. Hard cap enforcement. First attempt a normal, smaller M/L-biased decomposition per the step 2 sizing heuristics. Then count proposed children: `{max_children}` is a hard ceiling that **halts on exceed**, not a target to fill. If the count exceeds `{max_children}` (parsed as an integer), halt locally with a clear "split first" message: ask the user to split the idea into multiple smaller Epics or to raise `--max-children` deliberately. Do not silently truncate.\n\n4. Per-child duplicate lookup. For each proposed child (in order), call `get_tickets` once with a title/keyword search built from the child\'s summary. If a clear duplicate exists, drop that child from the plan and record the drop reason; never halt the whole run because a child has a duplicate. Re-number `<N>` only after all drops are finalized so child indexes are contiguous.\n\n5. Per-child research is restricted to the parent research pack plus optional narrow web search. Do not call deep research per child.\n\n6. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json` with at minimum:\n - `parent_summary` \u2014 copy from the parent draft.\n - `max_children` \u2014 the resolved integer value used for the cap.\n - `children` \u2014 ordered array of surviving children with all fields from step 2 (including `depends_on`, `recommended_after`, and `order_rationale`). After re-numbering in step 4, fix up the `depends_on`/`recommended_after` indexes so they still point at the correct surviving children.\n - `dropped_children` \u2014 array of `{proposed_summary, reason}` for children removed by duplicate lookup.\n\n## Return\n\nConfirm `decomposition-plan.json` was written, report the final child count and the number of children dropped for duplicate reasons.\n',
14256
14448
  "decompose-epic.md": 'Decompose the epic into manageable sub-tasks and get user approval.\n\n## Epic Description\n\n{epic_description}\n\n## Instructions\n\n1. Read the following artifacts to establish full context. If a file does not exist or is empty, proceed without it:\n - `{docs_dir}/epic-plans/{epic_slug}/research-findings.md`\n - `{docs_dir}/epic-plans/{epic_slug}/codebase-exploration.md`\n\n2. Reason about the epic and produce a decomposition. Consider:\n - Logical groupings of work that can be implemented and tested independently\n - Dependencies between sub-tasks (what must be built first)\n - A reasonable scope for each sub-task (each should be achievable in a single implementation session)\n\n3. Write the decomposition to `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` with this format:\n\n```markdown\n# Epic Decomposition\n\n## Sub-tasks\n\n### 1. {Sub-task title}\n- **Scope**: {What this sub-task covers}\n- **Key files/areas**: {Files and code areas involved}\n- **Dependencies**: {Other sub-task numbers this depends on, or "None"}\n\n### 2. {Sub-task title}\n...\n```\n\n In addition, you MUST also write a structured JSON sidecar at\n `{docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json`.\n This file is the machine-readable intermediate consumed by the `plan-epic`\n pipeline to store and approve the plan in the backend \u2014 it must be written\n from your structured decomposition data, NEVER by re-parsing the markdown.\n\n The sidecar format is:\n\n```json\n{\n "plan_version": 1,\n "nodes": [\n {\n "ticket_key": "BAPI-XXX",\n "status": "planned",\n "depends_on": [],\n "automations": [\n { "kind": "start-tickets" }\n ]\n }\n ],\n "edges": [\n { "from": "BAPI-XXX", "to": "BAPI-YYY" }\n ]\n}\n```\n\n Rules for the sidecar:\n - `plan_version` must be 1 for a new plan (an integer, never a float).\n - Each node `ticket_key` must match the Jira key of the created sub-task\n (populated after Jira ticket creation in a later pipeline step; use the\n planned Jira key if known, or a placeholder like "TBD-1" if not yet created).\n - `ticket_key` values must be unique and non-empty after trimming.\n - `depends_on` lists the `ticket_key` values this node depends on (mirrors\n the markdown Dependencies field).\n - `status` must be `"planned"` for newly-created sub-tasks.\n - `automations` lists automation kinds to run on the ticket \u2014 valid values\n are `"start-tickets"` and `"review-tickets"`. Use an empty array if none.\n - `edges` is an explicit list of directed dependency edges (from \u2192 to).\n It may be empty if all dependencies are captured in `depends_on`.\n - The DAG must be acyclic (no circular dependencies).\n - The deterministic Jira-dependency-link DAG builder is the documented\n fallback/recovery path if this sidecar is lost or corrupted (not built here).\n\n4. **Soft limit check**: If the decomposition results in more than 8 sub-tasks, you must verbally warn the user: "This decomposition has N sub-tasks, which exceeds the recommended limit of 8. Consider splitting this feature into multiple epics." Then proceed with the approval flow.\n\n5. Present the decomposition to the user and ask for their feedback. Explain the reasoning behind the breakdown and the dependency ordering.\n\n6. You MUST stop and wait for the user to respond. Do NOT assume approval. Do NOT proceed to the next step.\n\n7. If the user provides feedback or rejects the decomposition:\n - Incorporate their feedback\n - Rewrite `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` with the revised version\n - Present the revised decomposition and ask for approval again\n - Repeat until the user explicitly approves\n\n8. Only after explicit user approval, confirm: "Decomposition approved. Proceeding to sub-task exploration."\n\n## Return\n\nConfirm "Decomposition approved." and report the final sub-task count plus the path to `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md`. Flag if the count exceeded the recommended limit of 8.\n',
14257
- "draft-and-critique.md": "Draft the ticket(s) for this idea, run a BAPI-320 hygiene pass, and emit structured draft metadata.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Duplicate assessment: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n- Goals & NFR framing: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/goals-and-nfrs.md`.\n\n## Instructions\n\n1. Read all six input artifacts in full before drafting. The manifest's `scope` (`task`, `spike`, or `epic_candidate`) determines the drafting path.\n\n The goals-and-nfrs.md framing must shape every draft. Lead each draft with the **business goal** and **desired end-state**, and include an explicit **Non-Functional Requirements** section (and, where it clarifies behavior, the required **system behavior**). For the **epic_candidate** parent, these belong in the Epic description itself (the parent's `slim_description` should at least name the business goal + end-state, and the attached full draft must carry the Goals / Desired End-State / Non-Functional Requirements sections). Any NFR still marked `open` in the framing must be written into the draft as an explicit assumption plus an open-risk note \u2014 never silently dropped. This flow is documentary: do not generate a decision page and do not pause for clarification here.\n\n2. Drafting path by scope:\n - **task** or **spike**:\n - Call the `jira-ticket-writer` sub-agent with an explicit output path of `{docs_dir}/tickets/TICKET-{slug}.md`. The sub-agent must write the full markdown draft to that exact file.\n - **epic_candidate**:\n - Call `jira-ticket-writer` to draft only the Epic parent. Use the explicit output path `{docs_dir}/tickets/EPIC-{slug}.md`. Child tickets are produced later by `decompose-epic-candidate.md`; do not draft them here.\n\n3. Issue type policy:\n - Default ambiguous ideas to `Task`.\n - Choose `Spike` only when the work is primarily discovery/research/learning with no clear acceptance criteria yet.\n - The Epic parent uses Jira issue type `Epic`.\n\n4. Hygiene pass (BAPI-320 forbidden tokens). After the sub-agent writes the draft, read it back and ensure none of these tokens are present:\n - markdown tables (any `|`-separated header row).\n - escaped pipe-table patterns (e.g. `\\|`).\n - task-list checkboxes such as `- [ ]` or `- [x]`.\n - angle-bracket placeholder tokens (any `<placeholder>` form, even inside backticks).\n - raw HTML blocks (`<div>`, `<br>`, `<table>`, etc.).\n When a forbidden token is found, rewrite the surrounding paragraph in plain prose or bullet form and save the cleaned draft over the same path.\n\n5. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json` describing what Jira should later create.\n\n For **task** / **spike** scope, the metadata shape is:\n - `summary` \u2014 Jira ticket title.\n - `issue_type` \u2014 `Task` or `Spike`.\n - `labels` \u2014 array of Jira labels. Must include `ai-generated`, `idea-to-ticket`, the per-run label `bapi-idea-to-ticket-{run_id}`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}` (so a future run of the same idea is caught by label).\n - `idempotency_label` \u2014 `bapi-idea-to-ticket-{run_id}` (matches the label used by the duplicate-and-context-scan step).\n - `slim_description` \u2014 short Jira-safe description (no forbidden tokens). The full draft is uploaded as an attachment. It must include a CONCISE, high-level summary of the draft's `## Materials & Access` inventory (which materials are gatherable vs. record-only), noting that the exhaustive list lives in the attached full draft. This keeps the missing-materials record visible to human reviewers and to description-reading review/critique flows.\n - `attachment_path` \u2014 `{docs_dir}/tickets/TICKET-{slug}.md` (or the equivalent path used above).\n\n For **epic_candidate** scope, the metadata shape is:\n - `parent.summary` \u2014 Epic title.\n - `parent.issue_type` \u2014 `Epic`.\n - `parent.labels` \u2014 must include `ai-generated`, `idea-to-ticket`, `bapi-idea-to-ticket-{run_id}-parent`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}`.\n - `parent.idempotency_label` \u2014 `bapi-idea-to-ticket-{run_id}-parent`.\n - `parent.slim_description` \u2014 short Epic description. It must include a CONCISE, high-level summary of the Epic draft's `## Materials & Access` inventory (gatherable vs. record-only), noting that the exhaustive list lives in the attached full draft.\n - `parent.attachment_path` \u2014 `{docs_dir}/tickets/EPIC-{slug}.md`.\n - `children` \u2014 placeholder array. Populated later by `decompose-epic-candidate.md`; leave as an empty array here.\n\n6. Save the metadata exactly once. Downstream steps read this file; do not move it.\n\n## Return\n\nConfirm the draft path, the metadata path, and the chosen scope (`task`, `spike`, or `epic_candidate`).\n",
14449
+ "draft-and-critique.md": 'Draft the ticket(s) for this idea, run a BAPI-320 hygiene pass, and emit structured draft metadata.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Duplicate assessment: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n- Goals & NFR framing: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/goals-and-nfrs.md`.\n- Comp\u2192codebase map (optional): `{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json`. Produced upstream by the gated `comp-analysis.md` perception step. A missing, unreadable, or `applicable: false` map is treated exactly like `applicable: false` \u2014 a no-op: it never halts drafting and this step injects no visual-fidelity/comp language on its own.\n\n## Instructions\n\n1. Read the six required input artifacts in full before drafting, plus the comp-analysis map when it is present. The manifest\'s `scope` (`task`, `spike`, or `epic_candidate`) determines the drafting path.\n\n The goals-and-nfrs.md framing must shape every draft. Lead each draft with the **business goal** and **desired end-state**, and include an explicit **Non-Functional Requirements** section (and, where it clarifies behavior, the required **system behavior**). For the **epic_candidate** parent, these belong in the Epic description itself (the parent\'s `slim_description` should at least name the business goal + end-state, and the attached full draft must carry the Goals / Desired End-State / Non-Functional Requirements sections). Any NFR still marked `open` in the framing must be written into the draft as an explicit assumption plus an open-risk note \u2014 never silently dropped. This flow is documentary: do not generate a decision page and do not pause for clarification here.\n\n2. Drafting path by scope:\n - **task** or **spike**:\n - Call the `jira-ticket-writer` sub-agent with an explicit output path of `{docs_dir}/tickets/TICKET-{slug}.md`. The sub-agent must write the full markdown draft to that exact file. Pass the comp-analysis map path (`{docs_dir}/idea-to-ticket/{slug}-{run_id}/comp-analysis.json`) into the sub-agent prompt alongside the six existing input artifacts (run manifest, research pack, duplicate assessment, standards checklist, resolved uncertainties, goals-and-nfrs).\n - **epic_candidate**:\n - Call `jira-ticket-writer` to draft only the Epic parent. Use the explicit output path `{docs_dir}/tickets/EPIC-{slug}.md`. Pass the same comp-analysis map path into the sub-agent prompt alongside the six existing input artifacts. Child tickets are produced later by `decompose-epic-candidate.md`; do not draft them here.\n\n Comp-analysis map consumption (both paths): when `comp-analysis.json` has `applicable: true`, tell the writer its Requirements MUST use the mapped components/templates/tokens/routes as concrete implementation guidance (e.g. "reuse component X", "use token Z", "extend template Y", "wire route R"). When the map has `applicable: false` or is missing/unreadable, tell the writer to ignore the artifact entirely and avoid mentioning comp analysis, design comps, or visual fidelity unless the original request independently requires those materials. This step only feeds the text JSON map to the writer \u2014 it never opens images, calls `describe_image()`, or performs vision analysis itself.\n\n3. Issue type policy:\n - Default ambiguous ideas to `Task`.\n - Choose `Spike` only when the work is primarily discovery/research/learning with no clear acceptance criteria yet.\n - The Epic parent uses Jira issue type `Epic`.\n\n4. Hygiene pass (BAPI-320 forbidden tokens). After the sub-agent writes the draft, read it back and ensure none of these tokens are present:\n - markdown tables (any `|`-separated header row).\n - escaped pipe-table patterns (e.g. `\\|`).\n - task-list checkboxes such as `- [ ]` or `- [x]`.\n - angle-bracket placeholder tokens (any `<placeholder>` form, even inside backticks).\n - raw HTML blocks (`<div>`, `<br>`, `<table>`, etc.).\n When a forbidden token is found, rewrite the surrounding paragraph in plain prose or bullet form and save the cleaned draft over the same path.\n\n5. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json` describing what Jira should later create.\n\n For **task** / **spike** scope, the metadata shape is:\n - `summary` \u2014 Jira ticket title.\n - `issue_type` \u2014 `Task` or `Spike`.\n - `labels` \u2014 array of Jira labels. Must include `ai-generated`, `idea-to-ticket`, the per-run label `bapi-idea-to-ticket-{run_id}`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}` (so a future run of the same idea is caught by label).\n - `idempotency_label` \u2014 `bapi-idea-to-ticket-{run_id}` (matches the label used by the duplicate-and-context-scan step).\n - `slim_description` \u2014 short Jira-safe description (no forbidden tokens). The full draft is uploaded as an attachment. It must include a CONCISE, high-level summary of the draft\'s `## Materials & Access` inventory (which materials are gatherable vs. record-only), noting that the exhaustive list lives in the attached full draft. This keeps the missing-materials record visible to human reviewers and to description-reading review/critique flows.\n - `attachment_path` \u2014 `{docs_dir}/tickets/TICKET-{slug}.md` (or the equivalent path used above).\n\n For **epic_candidate** scope, the metadata shape is:\n - `parent.summary` \u2014 Epic title.\n - `parent.issue_type` \u2014 `Epic`.\n - `parent.labels` \u2014 must include `ai-generated`, `idea-to-ticket`, `bapi-idea-to-ticket-{run_id}-parent`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}`.\n - `parent.idempotency_label` \u2014 `bapi-idea-to-ticket-{run_id}-parent`.\n - `parent.slim_description` \u2014 short Epic description. It must include a CONCISE, high-level summary of the Epic draft\'s `## Materials & Access` inventory (gatherable vs. record-only), noting that the exhaustive list lives in the attached full draft.\n - `parent.attachment_path` \u2014 `{docs_dir}/tickets/EPIC-{slug}.md`.\n - `children` \u2014 placeholder array. Populated later by `decompose-epic-candidate.md`; leave as an empty array here.\n\n6. Save the metadata exactly once. Downstream steps read this file; do not move it.\n\n## Return\n\nConfirm the draft path, the metadata path, and the chosen scope (`task`, `spike`, or `epic_candidate`).\n',
14258
14450
  "duplicate-and-context-scan.md": 'Detect existing Jira tickets that duplicate or relate to this idea before any Jira mutation.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` (if produced).\n- Pipeline variable `allow_duplicate` controls override behavior (for this run, `allow_duplicate` = `{allow_duplicate}`). Treat the literal string `"true"` as override; any other value (including `"false"`, missing, or empty) is non-override.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is part of the full-automation chain and is authorized to call `get_tickets` as directed below \u2014 performing an orchestrator-directed tool call is not "re-orchestrating".\n\n1. Build at least two Jira search queries from the manifest:\n - **Title/keyword query**: use the most salient nouns from `idea` and `slug` as title/text keywords. Prefer 2-4 concrete terms over long natural-language sentences. Run via `get_tickets`.\n - **Stable idea-hash query** (the reliable cross-run dedup): run `get_tickets` with its `labels` parameter set to `bapi-idea-hash-{idea_hash}`. This label is identical for every run of the same idea, so it catches a PRIOR run that already created a ticket for this idea \u2014 even one created days ago. A hit here is a strong `duplicate` signal.\n - **Idempotency-label query**: run `get_tickets` with its `labels` parameter set to `bapi-idea-to-ticket-{run_id}` (the tool builds the `labels in (...)` JQL for you \u2014 do not pass a raw JQL string). This per-run label only matches a partial run of THIS same run, so it supports resume behavior.\n\n2. For each returned ticket, capture: ticket key, summary, status, and a short reason it matched (which query, which keyword).\n\n3. Classify the overall verdict as one of:\n - `duplicate` \u2014 at least one returned ticket clearly describes the same work as `idea`.\n - `related` \u2014 returned tickets are adjacent or partial overlaps but not the same work.\n - `none_found` \u2014 no meaningful matches.\n - `unable_to_check` \u2014 the Jira search itself failed (network error, auth error, JQL rejection). Record the failure and pick this verdict.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json` with at minimum:\n - `verdict` \u2014 one of the four values above.\n - `matches` \u2014 array of `{ticket_key, summary, status, reason}` objects (may be empty).\n - `queries_used` \u2014 array of the actual JQL/search strings sent.\n - `allow_duplicate` \u2014 the resolved value of `{allow_duplicate}` for this run.\n\n5. Halt behavior:\n - If `verdict` is `duplicate` and `allow_duplicate` is not `"true"`, halt locally. Do not continue the pipeline. Tell the user that the duplicate halt is strict and that re-running with `--allow-duplicate` overrides it.\n - If `verdict` is `duplicate` and `allow_duplicate` is `"true"`, continue the pipeline but keep the duplicate evidence in the assessment file so downstream steps can reference it (e.g., to add a "supersedes" note to the draft).\n - For `related`, `none_found`, and `unable_to_check`, continue without halting.\n\n## Return\n\nConfirm `duplicate-assessment.json` was written, report `verdict`, and report whether the run is halting or continuing.\n',
14259
14451
  "evaluate-and-recommend.md": 'Evaluate the clarifying questions and ticket critiques generated for {ticket_key} against the actual codebase, then decorate every actionable item with the resolution guidance the reviewer will need on the decision page. The result is a single combined review-and-resolution document.\n\n## Phase 0 \u2014 Grounding & Audit Setup\n\nBefore gathering any source documents, extract the codebase-grounding context produced by the preceding `materialize_fresh_base` pipeline step:\n\n- Read the `materialize_fresh_base` tool result from earlier in this session. It returns JSON `{ base_sha, base_branch, fresh_base_root }` \u2014 or, when `no_refresh_base` was set, `{ base_sha: "local-stale", fresh_base_root: <original repo root> }`.\n- Retain `fresh_base_root` and `base_sha` for the rest of this procedure. Every codebase read in Phase 1 / Phase 2 below is grounded against `fresh_base_root`. `fresh_base_root` is also the exact value you must pass to the pipeline\'s later `cleanup_fresh_base` step \u2014 it is a *runtime* value returned by the tool call, not a static recipe variable, so pass the real path string you captured here, not any placeholder text shown in the step\'s params.\n- If the `materialize_fresh_base` step\'s result contains an `error` field and `no_refresh_base` was NOT set, this is the fail-loud condition the recipe\'s `on_error: "halt"` exists for: stop here, do not fall back to grounding against your own working directory, and report the failure (name the attempted base branch and the remediation \u2014 retry, or rerun with `--no-refresh-base`).\n\n**Metadata Audit Header** \u2014 the very first content of the generated review-and-resolution document, before any other section, must be:\n\n```\n**Base SHA**: <base_sha>\n**Base Branch**: <base_branch, or "(local, in-place)" when base_sha is "local-stale">\n**Grounding Status**: <Freshly Materialized | Stale/In-Place Fallback>\n```\n\n- `Grounding Status` is **Freshly Materialized** whenever `base_sha` is a real commit SHA (the normal path).\n- `Grounding Status` is **Stale/In-Place Fallback** whenever `base_sha` is exactly `local-stale` (the `--no-refresh-base` opt-out path). In this case, immediately follow the header with a prominent, bold, high-contrast warning block, for example:\n\n > **\u26A0 STALE GROUNDING \u2014 `--no-refresh-base` was used.** This review evaluated the codebase as checked out locally, NOT a freshly-fetched `origin/<base>`. `file:line` citations may reflect uncommitted or unmerged local state.\n\n**Codebase grounding rule**: Ground ALL file reads and codebase searches exclusively against the `fresh_base_root` directory extracted above. Do NOT read codebase files from your default working directory or session cwd \u2014 `fresh_base_root` is the only trustworthy source of truth for `file:line` citations in this procedure.\n\n**Original-repo rule**: Ticket docs, `{docs_dir}` inputs, and ALL output paths stay in the ORIGINAL repository, never the `fresh_base_root` temp dir. This includes the ticket-fetch call below, the clarifying-questions / critique source documents, and the saved review-and-resolution output file (see the Save rule at the bottom). Do NOT redirect any of these into `fresh_base_root`.\n\n**Path hygiene rule**: Every `file:line` citation and Codebase Evidence entry in the output document MUST be repo-relative \u2014 strip the `fresh_base_root` absolute-path prefix before writing it down. A citation must never contain a temp-dir / `/tmp/...`-style absolute path (write `src/foo.ts:10`, never `/tmp/bridge-review-.../src/foo.ts:10`).\n\n1. Fetch the current ticket description using the `get_ticket` tool with ticket_number `{ticket_key}` exactly once at the top of this procedure.\n\n2. Gather the clarifying questions and critique documents from the preceding pipeline steps. The local files at `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md` and `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md` are the canonical source. After a second-opinion run, each document has this shape:\n\n - A top-level H1 (`# Ticket Analysis` for clarifier docs, `# Ticket Quality Critique` for critique docs) followed by an italic provider-attribution line of the form `_This analysis was generated by GPT|Claude|Gemini._`. The attribution names the LLM family that produced the **first round**.\n - The first-round questions / critique items, exactly as written by the first-round model.\n - **Inline second-opinion blockquotes** nested directly under each prior item the second round addressed. Each blockquote starts with `> **Second opinion (<provider>) - <stance>.**` where `<provider>` is `GPT|Claude|Gemini` and `<stance>` is `concurrence|refinement|disagreement`. The blockquote is followed by `> *Citations: <comma-separated grounding refs>*`. Items the second round did **not** comment on have no blockquote \u2014 that is the "weak concurrence" signal. Use the provider name in the blockquote header to attribute the comment to the second-round LLM family in your evaluation prose where helpful.\n - A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 you will find a second italic attribution line of the form `_These additional points were raised by GPT|Claude|Gemini._` \u2014 this names the second-round LLM family. Sub-headings are agent-specific:\n - Clarifier docs: `### New Requirements Questions` and `### New Technical Questions` \u2014 numbering continues from the prior section.\n - Critique docs: `### New Requested Changes` and `### New Points to Consider` \u2014 numbering continues from the prior section.\n Each new item has its own `*Citations: ...*` line.\n - A final **`## Second Opinion Summary`** footer (1-3 sentences) capturing the second round\'s overall position. This always renders, even when the second round had no inline comments and no new items.\n\n **Legacy fallback shape**: in rare cases (model lacks JSON-schema support, the JSON call failed, or the response could not be parsed), the document may instead end with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section containing `### Response to Prior Items` and `### Additional Points` subsections. If you detect this fallback shape, treat it equivalently: subsection responses tagged `concurrence` map to weak/strong concurrence (use the body length to disambiguate \u2014 bare one-line concurrences are weak), `refinement`/`disagreement` map to the disagree buckets, and items under `### Additional Points` map to the gap-captured bucket below.\n\n **Partial-source-doc tolerance**: if the clarifying-questions doc OR the ticket-critique doc is missing or unreadable, skip that document silently and produce items only for the surviving doc. Do not fail. If **both** documents are absent, still write the combined output file at `{docs_dir}/review/{ticket_key}-review-and-resolution.md` with the standard top-level sections (`Confirmed Improvements`, `Needs Scrutiny`, `Open Questions`, `Round Agreement Summary`) present but no emitted E-items in any section. This preserves downstream file-existence expectations for the capture-review-decisions step.\n\n3. Determine **Round Agreement** for every clarifying question and critique point using these rules:\n\n - **Both rounds agree (weak concurrence)** \u2014 the prior item has NO inline blockquote AND is not in `## New in Second Opinion`. The second round did not object to the point and did not consider it important enough to comment on. Briefly validate the answer\'s groundedness against the codebase. If validation surfaces concerns, demote this item to **rounds disagree** (single round only depth) and treat as Needs Scrutiny.\n - **Both rounds agree (strong concurrence)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - concurrence.** ...` blockquote. The second round explicitly reinforced the prior point. Reuse the blockquote\'s `*Citations:*` as starting evidence; verify briefly.\n - **Rounds disagree (refinement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - refinement.** ...` blockquote. The second round modified or added detail. Apply full disagreement-depth analysis; reuse blockquote citations.\n - **Rounds disagree (disagreement)** \u2014 the prior item carries an inline `> **Second opinion (<provider>) - disagreement.** ...` blockquote. The second round contradicts the prior. Apply full disagreement-depth analysis; categorize the outcome based on which position the codebase supports.\n - **Gap captured** \u2014 the item lives under `## New in Second Opinion > ### New <category>` (one of: New Requirements Questions, New Technical Questions, New Requested Changes, New Points to Consider). Apply the two-axis check below. Reuse the new item\'s `*Citations:*` as starting evidence.\n - **Single round only** \u2014 the document has none of the above markers (no inline blockquotes, no `## New in Second Opinion` block, no `## Second Opinion Summary` footer). The pipeline ran only one round. Treat every item as a disagreement: cite 2+ codebase locations and give full analytical depth.\n\n Apply these depth and categorization rules:\n\n - **Both rounds agree (weak concurrence)**: 1 codebase citation, 1-2 sentence assessment confirming grounding. Categorize as Confirmed Improvement if grounded; demote to Needs Scrutiny if validation finds problems.\n - **Both rounds agree (strong concurrence)**: 1 codebase citation (may reuse a blockquote citation), 1-2 sentence assessment. Categorize as Confirmed Improvement.\n - **Rounds disagree (refinement or disagreement)**: 2+ codebase citations, 3-4 sentence assessment that explicitly weighs the prior-round position against the second-opinion position. Categorize based on which position the evidence supports. Always include both positions in the Assessment.\n - **Gap captured \u2014 two-axis check** (for items in `## New in Second Opinion`):\n - If both the question is grounded in the codebase/standards AND the best-guess answer is sensible \u2192 **Confirmed Improvement** with a 1-2 sentence assessment and 1 citation.\n - If the question is genuine but the best-guess answer is flawed \u2192 **Needs Scrutiny**. Cite 2+ files. Use disagreement-depth.\n - If the question itself does not hold up \u2192 **Needs Scrutiny** with evidence of what the code actually does. Disagreement-depth.\n - If neither codebase nor standards can settle the question \u2192 **Open Questions**. Disagreement-depth.\n - **Single round only**: Treat as a disagreement \u2014 cite 2+ codebase locations and give full analytical depth.\n\n For critique points (Requested Changes and Points to Consider), apply the same Round Agreement rules. The signal locations are inline `> **Second opinion (<provider>) - ...**` blockquotes nested under items in `### Requested Changes` / `### Points to Consider`, and gap-captured items under `## New in Second Opinion > ### New Requested Changes` / `### New Points to Consider`.\n\n **Depth calibration**:\n - When Round Agreement is `both rounds agree (weak concurrence)`, `both rounds agree (strong concurrence)`, or `gap captured` (passes both axes), keep Assessment to 1-2 sentences and Codebase Evidence to 1 citation \u2014 the validation step or the consensus does the heavy lifting.\n - When Round Agreement is `rounds disagree (refinement)`, `rounds disagree (disagreement)`, or `single round only`, Assessment should be 3-4 sentences and Codebase Evidence should cite 2+ files explaining the discrepancy.\n - A `gap captured` item that FAILS the two-axis check uses the disagreement depth, not the gap-captured depth.\n - A `weak concurrence` item that FAILS your validation gets demoted: change Round Agreement to `rounds disagree (single round only)`, expand Assessment to 3-4 sentences, and add a 2nd citation.\n\n **Source field conventions** \u2014 the `**Source**` string disambiguates where in the source doc the item lives so the downstream `capture-review-decisions` step can route the rewrite correctly. Use these forms:\n\n - **Weak concurrence (silent prior item)**: `Clarifying Q3 (prior round, weak concurrence)` or `Critique: Requested Change 2 (prior round, weak concurrence)`.\n - **Strong concurrence (explicit blockquote)**: `Clarifying Q9 (prior round, concurrence inline)` or `Critique: Points to Consider 1 (prior round, concurrence inline)`.\n - **Refinement (inline blockquote)**: `Clarifying Q3 (prior round, refinement inline)`.\n - **Disagreement (inline blockquote)**: `Clarifying Q5 (prior round, disagreement inline)`.\n - **Gap captured (tail-block item)**: `Clarifying Q11 (new in second opinion \u2192 New Requirements Questions)` or `Critique: Requested Change N+1 (new in second opinion \u2192 New Requested Changes)`. Always spell out the sub-section name after the arrow \u2014 capture-review-decisions uses it to find the rewrite target.\n - **Single round only**: `Clarifying Q3 (single round)`.\n\n## Phase 1 \u2014 Evaluate and classify every item\n\nNumber every item sequentially across all sections (E-1, E-2, E-3, \u2026). When the same underlying issue is raised in BOTH the clarifying-questions doc and the critique doc, consolidate it into a SINGLE E-item rather than emitting one per source, and cite both origins in its `**Source**` field (e.g. `Clarifying Q3 + Critique: Requested Change 2`); keep the numbering sequential with no gaps. Classify every clarifying question and every critique point into exactly one of three buckets using the Round Agreement rules, codebase groundedness checks, and the `gap captured` two-axis check before producing any recommendation decoration:\n\n- **Confirmed Improvements**: Suggestions that are grounded and would genuinely improve the ticket by closing significant gaps or correcting design issues. Includes weak-concurrence items that passed validation, strong-concurrence items, and `gap captured` items that passed both axes.\n- **Needs Scrutiny**: Suggestions based on inaccurate codebase assumptions, with evidence of the actual code behavior. Includes `gap captured` items that failed either axis, weak-concurrence items demoted by validation, and the loser of any rounds-disagree pair.\n- **Open Questions**: Legitimate ambiguities that require human input to resolve.\n\nPhase 1 must complete before Phase 2 begins \u2014 do not start decorating an item with a decision tree, recommendation index, or clarity fields until classification is final.\n\n## Phase 2 \u2014 Decorate actionable items with resolution guidance\n\nPhase 2 applies **only** to items in the `Needs Scrutiny` and `Open Questions` buckets. Confirmed Improvements remain compact and undecorated (see "Confirmed Improvements output" below).\n\nFor every actionable (Needs Scrutiny / Open Questions) item, produce the following template using these stable labels:\n\n```\n### E-<sequential number>: <concise title>\n\n**Source**: <where this item lives in the source doc \u2014 see Source field conventions above>\n\n**Round Agreement**: <one of the six values> \u2014 <1 sentence on what the second round contributed>\n\n**Confidence**: <High|Medium|Low>\n\n**Resolution path**: <"resolve at your desk" or "needs a conversation">\n\n**Decision tree**:\n- If <condition 1>, then <action 1>. See `file:line`. <1-2 sentence rationale.>\n- If <condition 2>, then <action 2>. See `file:line`. <1-2 sentence rationale.>\n- If <condition 3>, then <action 3>. See `file:line`. <1-2 sentence rationale.>\n\n**Recommendation Index**: <0-based index of the recommended branch in the decision tree above>\n\n**Recommendation**: <which branch the evidence best supports and why, 1-2 sentences>\n\n**Original question**: <the clarifying-question or critique point as it was originally raised, sourced verbatim or near-verbatim from the original clarifying-questions / critique docs. Light rephrasing is allowed; do NOT introduce new technical content. Soft cap ~30 words.>\n\n**Option consequences**:\n- <consequence for branch 1 \u2014 describe the behavioral consequence of choosing this option, not its rationale. ~25 words.>\n- <consequence for branch 2 \u2014 same shape. ~25 words.>\n- <consequence for branch 3 \u2014 same shape. ~25 words.>\n\n**Why it matters**: <one concrete sentence on the impact this decision has on the ticket, the users, or the affected code paths. Soft cap ~40 words.>\n\n**Recommendation explanation**: <explain why the recommended branch is the best choice, tied to the codebase evidence and the consequences of each option. Soft cap ~60 words.>\n\n**Assessment**: <three-point structure>\n1. **State the original suggestion**: What did the clarifying question or critique point propose?\n2. **State the codebase evidence**: What does the actual code show about this suggestion?\n3. **State the implication**: Does the evidence confirm the suggestion, contradict it, or leave it unresolved?\n\n**Codebase Evidence**:\n- `path/to/file.ts:42` \u2014 <what this line/block demonstrates>\n- `path/to/other.ts:110-125` \u2014 <what this range demonstrates>\n\n<If no direct codebase evidence exists, state: "No direct codebase evidence found.">\n```\n\n**Writing quality**: Write each Assessment as if explaining to a colleague who has NOT read the original clarifying questions or critique documents. Each assessment should be self-contained and understandable without cross-referencing the source material. The three-point Assessment structure ensures every assessment tells a complete story rather than assuming the reader already knows what was suggested and why.\n\n**Decision tree rules**:\n- Each decision tree must have **2\u20134 branches**. Do not exceed 4 and do not produce only 1.\n- **Strict lower bound \u2014 reclassify on single-branch items**: If you can think of only one branch for a `Needs Scrutiny` or `Open Questions` item \u2014 that is, the resolution is effectively forced \u2014 you must reclassify the item as a **Confirmed Improvement** instead of emitting a single-branch decision tree. The 2-branch lower bound is a hard rule; do not work around it by stretching to a contrived second branch. If a single answer is genuinely the only path, the item belongs in Confirmed Improvements.\n- Each branch must end with a concrete, actionable step (not "investigate further").\n- Cite relevant code in `file:line` format where possible. If no code reference exists, omit the citation rather than fabricating one.\n- Cap each branch at 2-3 sentences total (including the action and rationale).\n- `**Recommendation Index**` must be the 0-based index of the recommended branch in the decision tree above. The first branch is index 0, the second is index 1, etc.\n- **Option consequences** must be a list parallel to the decision-tree branches: one entry per branch, in the same order. Describe the behavioral consequence of choosing that option, not its rationale.\n- **"resolve at your desk"**: The item can be resolved through technical investigation \u2014 reading code, running tests, or checking configuration. No stakeholder input needed.\n- **"needs a conversation"**: The item involves a product decision, scope question, or cross-team dependency that cannot be resolved from the codebase alone.\n\n**Confidence Tags** \u2014 assign confidence based on codebase evidence strength:\n- **High**: Cite specific `file:line` references that directly support the assessment.\n- **Medium**: Reference related code patterns or architectural conventions, but not the exact code in question.\n- **Low**: No direct codebase evidence. Assessment is based on general reasoning or domain knowledge.\n\n### Confirmed Improvements output\n\nRender each Confirmed Improvement as a single bullet in a compact list. No headings per item, no decision trees, no clarity-field decoration:\n\n- **E-<number>: <title>** \u2014 Source: <source string>; Round Agreement: <one of the six values>; Confidence: <High|Medium|Low>. <recommended action, 1 sentence.>\n\nThe compact bullet still includes `Source`, `Round Agreement`, `Confidence`, and the one-sentence recommended action so `capture-review-decisions.md` can map these items to its `clear_improvements` array.\n\n## Round Agreement Summary\n\nAfter all items are processed, produce a summary section that groups items by round agreement status:\n\n### Points of Disagreement\nFor items where the evaluation marked `rounds disagree (refinement)`, `rounds disagree (disagreement)`, or `single round only` \u2014 including `gap captured` items that failed the two-axis check and landed in Needs Scrutiny \u2014 list as bullets with the E-number, the nature of the disagreement, and a 1-sentence explanation of why this disagreement matters for the ticket (e.g., it indicates an architectural ambiguity, a scope question, or a standards gap).\n\nIf no items were marked as disagreements, write: "All reviewed points had round consensus. No disagreement-driven risks identified."\n\n### Points of Agreement\nSplit this section into two sub-bullets to surface the difference between the second round explicitly reinforcing a point versus tacitly accepting it:\n\n**Strong agreement** \u2014 items where the evaluation marked `both rounds agree (strong concurrence)`. The second round took the trouble to write an explicit `concurrence` blockquote; this is a soft signal that the point is important enough that the second round wanted to underline it. List as bullets with the E-number and a half-sentence noting the shared conclusion.\n\n**Weak agreement** \u2014 items where the evaluation marked `both rounds agree (weak concurrence)`. The second round did not object and did not consider the item important enough to comment on; the local agent\'s brief validation found no concerns. List as bullets with the E-number and a half-sentence noting the conclusion. Lower priority for human review than strong-agreement items.\n\nIf a sub-bullet has no items, omit it (rather than writing a "no items" note for each \u2014 keep the section tidy).\n\n### Gaps Captured by Second Round\nFor items where the evaluation marked `gap captured` (sound second-opinion Additional Points confirmed as Confirmed Improvements): list as bullets with the E-number and a half-sentence noting the gap the second round surfaced. These items did not require a decision \u2014 they are already in Confirmed Improvements \u2014 but are surfaced here so the reviewer sees what the second-round analysis added on top of the first round.\n\nIf no gaps were captured, write: "The second round did not surface any net-new confirmed improvements."\n\n## Edge Cases\n\n- If the evaluation contains zero items in Needs Scrutiny, write: "No items flagged for scrutiny. All reviewed suggestions were either confirmed or remain open questions."\n- If the evaluation contains zero items in Open Questions, write: "No open questions identified. All ambiguities were resolved through codebase analysis."\n- If both Needs Scrutiny and Open Questions are empty, include only the Confirmed Improvements section and add a summary: "All suggestions from the review were confirmed as grounded improvements. No decision trees are needed."\n- If both source documents are absent, still write the combined file with the standard top-level sections present but no emitted E-items rather than failing.\n\n## Example of a Well-Written E-Item (Weak Concurrence \u2014 Confirmed Improvement)\n\n### E-2: Caching of analysis-type lookups\n\n**Source**: Clarifying Q4 (prior round, weak concurrence)\n\n**Round Agreement**: both rounds agree (weak concurrence) \u2014 the second round did not comment on this item; brief validation confirms the answer is grounded.\n\n**Assessment**: The prior round suggested caching `ANALYSIS_TYPES` lookups in a module-level variable to avoid repeated DB round trips. The codebase already does this at `src/python/learn_repository/__init__.py:14`, so the suggestion is grounded and the second round\'s silence is consistent with tacit agreement.\n\n**Codebase Evidence**:\n- `src/python/learn_repository/__init__.py:14` \u2014 module-level constant pattern is the established convention\n\n(Confirmed Improvements compact bullet form: **E-2: Caching of analysis-type lookups** \u2014 Source: Clarifying Q4 (prior round, weak concurrence); Round Agreement: both rounds agree (weak concurrence); Confidence: High. Confirm the existing module-level cache and add a short comment naming the pattern.)\n\n## Example of a Well-Written E-Item (Strong Concurrence \u2014 Confirmed Improvement)\n\n### E-4: Sequential per-type review_repository fan-out\n\n**Source**: Clarifying Technical Q2 (prior round, concurrence inline)\n\n**Round Agreement**: both rounds agree (strong concurrence) \u2014 the second round explicitly reinforced the prior recommendation, citing per-type lock release simplicity as the deciding factor.\n\n**Assessment**: The prior round recommended sequential per-type execution; the second-opinion blockquote reinforced this, noting that the per-type lock release contract becomes trivial under sequential execution. `review_repository` already uses internal `asyncio.gather` for chunk-level concurrency, so wrapping it in another concurrency layer would not buy throughput and would complicate the abort/finally cleanup contract.\n\n**Codebase Evidence**:\n- `src/python/learn_repository/review_repository.py:369-387` \u2014 review_repository internally gathers chunks with return_exceptions=True\n\n## Example of a Well-Written E-Item (Rounds Disagree \u2014 Needs Scrutiny with full clarity fields)\n\n### E-5: Authentication middleware placement for new endpoint\n\n**Source**: Clarifying Q2 (prior round, disagreement inline)\n\n**Round Agreement**: rounds disagree (disagreement) \u2014 the prior round recommended adding auth at the router level; the second-opinion blockquote argued the existing middleware stack already covers it.\n\n**Confidence**: High\n\n**Resolution path**: resolve at your desk\n\n**Decision tree**:\n- If the global middleware stack already enforces auth on `/api/*` routes, then drop the explicit `Depends(require_api_key)` from the new endpoint. See `main.py:45-52`.\n- If routers each opt in to auth via dependencies, then add `Depends(require_api_key)` to the new endpoint. See `api/routes/__init__.py:18-30`.\n- If only certain `/api/*` sub-paths need auth, then carve out a sub-router with its own dependency. See `api/routes/__init__.py:18-30`.\n\n**Recommendation Index**: 1\n\n**Recommendation**: The existing routers each opt in to auth, so the new endpoint must do the same. Adding `Depends(require_api_key)` is the smallest correct change.\n\n**Original question**: Should the new `/api/exports` endpoint declare an explicit auth dependency, or is it covered by the global middleware?\n\n**Option consequences**:\n- Endpoint becomes publicly reachable; protected data leaks via the new path.\n- Endpoint requires a valid API key, matching every other `/api/*` route.\n- Adds a parallel router; doubles the auth surface that has to be kept consistent.\n\n**Why it matters**: Authentication on `/api/exports` directly determines whether protected data leaks; the wrong default is a security regression, not a stylistic choice.\n\n**Recommendation explanation**: The codebase pattern in `api/routes/__init__.py:18-30` shows each router declaring its own `Depends(require_api_key)`. Following that convention adds two lines, keeps auth uniform across endpoints, and avoids a parallel sub-router that future maintainers would have to keep in sync.\n\n**Assessment**: The prior round suggested that the new `/api/exports` endpoint needs an explicit `Depends(require_api_key)` guard because it is not covered by the global middleware. The second opinion disagreed, claiming the middleware stack in `main.py` handles authentication for all `/api/*` routes. Codebase analysis shows that `main.py:45-52` applies rate limiting globally but authentication is applied per-router in `api/routes/__init__.py:18-30` \u2014 each router must opt in via `Depends(require_api_key)`. This supports the prior round\'s position: the new endpoint needs an explicit auth dependency.\n\n**Codebase Evidence**:\n- `main.py:45-52` \u2014 global middleware applies rate limiting and CORS, but not authentication\n- `api/routes/__init__.py:18-30` \u2014 each router includes its own auth dependency; there is no catch-all auth middleware\n\n## Example of a Well-Written E-Item (Gap Captured \u2014 Confirmed Improvement)\n\n### E-7: Missing Alembic migration for new role-scope column\n\n**Source**: Critique: Requested Change N+1 (new in second opinion \u2192 New Requested Changes)\n\n**Round Agreement**: gap captured \u2014 the second opinion surfaced a missing migration that the prior round did not raise, and recommended adding an Alembic revision.\n\n**Assessment**: The ticket introduces a new `role_scope` column on the `users` table but does not mention a migration. The second opinion flagged this gap and recommended adding an Alembic revision; both the gap and the recommendation are grounded, since `db/alembic/versions/` is the established location for schema changes per the project\'s database guide.\n\n**Codebase Evidence**:\n- `db/alembic/versions/` \u2014 all schema changes land here as autogenerated revisions\n\n## Save rule\n\nSave the combined review-and-resolution document to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`. Output only the combined review-and-resolution document \u2014 no meta-commentary.\n\n## Return\n\nConfirm "Review-and-resolution document written to `{docs_dir}/review/{ticket_key}-review-and-resolution.md`." and report the total count of E-items captured.\n',
14260
14452
  "execute-epic-research.md": 'Execute the research plan and write findings.\n\n## Instructions\n\n1. Read the research plan from `{docs_dir}/epic-plans/{epic_slug}/research-plan.md`.\n\n2. Execute the plan based on the Research Mode:\n\n **If mode is `deep`**:\n - Call the `request_deep_research` MCP tool with:\n - `query`: the Deep Research Query from the plan\n - `context`: "Bridge API is a Python/FastAPI application with PostgreSQL, LiteLLM, and Pinecone. This research supports epic planning for: {epic_description}"\n - `wait_for_result`: true\n - `save_locally`: true\n - If deep research fails, log a warning and fall back to web searches using the Web Search Topics from the plan. Do NOT halt.\n\n **If mode is `web`**:\n - Perform web searches for each topic listed in the plan.\n - Capture relevant findings from each search.\n\n **If mode is `none`**:\n - Write a brief note: "No external research needed. Proceeding with codebase exploration."\n\n3. Write all findings to `{docs_dir}/epic-plans/{epic_slug}/research-findings.md` with this structure:\n\n```markdown\n# Research Findings\n\n## Mode\n{deep | web | none}\n\n## Findings\n{Synthesized research results organized by topic. Include source references where applicable.}\n\n## Key Takeaways\n{Bullet points summarizing the most important findings that will inform the codebase exploration and epic decomposition.}\n```\n\n## Return\n\nConfirm research findings were written to `{docs_dir}/epic-plans/{epic_slug}/research-findings.md` and report the mode used (`deep`, `web`, or `none`) plus a one-line summary of the key takeaways.\n',
@@ -14263,7 +14455,7 @@ var INSTRUCTIONS = {
14263
14455
  "explore-epic-codebase.md": 'Perform a holistic, epic-level codebase exploration.\n\n## Epic Description\n\n{epic_description}\n\n## Instructions\n\n1. Read the research findings from `{docs_dir}/epic-plans/{epic_slug}/research-findings.md` to establish context. If the file does not exist or is empty, proceed without it.\n\n2. Explore the codebase with a focus on breadth rather than depth. The goal is to build a "lay of the land" understanding for the entire epic, not to deeply analyze any single sub-task. Search by filename pattern, search file contents by text pattern, and read relevant files to find:\n - Files, modules, and directories relevant to the epic\n - Architectural patterns used in similar features\n - Integration points and dependencies between modules\n - Existing conventions for the type of work this epic involves\n - Database models, API routes, agent flows, and utilities that may be affected\n\n3. Build a mental model of:\n - What exists today that relates to the epic\n - What patterns and conventions are used in similar features\n - What dependencies, data flows, and integration points are involved\n - What areas of the codebase will likely need changes\n\n4. Write the exploration findings to `{docs_dir}/epic-plans/{epic_slug}/codebase-exploration.md` with this structure:\n\n```markdown\n# Codebase Exploration\n\n## Architecture Overview\n{High-level description of how the relevant parts of the codebase are structured.}\n\n## Relevant Code Areas\n{List of key files, modules, and directories with brief descriptions of their relevance to the epic.}\n\n## Existing Patterns\n{Patterns and conventions discovered that should be followed when implementing the epic.}\n\n## Integration Points\n{Dependencies, data flows, and integration points that the epic will need to account for.}\n\n## Potential Challenges\n{Any architectural constraints, technical debt, or complexity that could affect implementation.}\n```\n\n## Return\n\nConfirm the codebase exploration was written to `{docs_dir}/epic-plans/{epic_slug}/codebase-exploration.md` and return a concise summary of the discovered codebase areas, naming the key files and patterns relevant to the epic.\n',
14264
14456
  "explore-epic-subtasks.md": "Perform focused code explorations for each approved sub-task.\n\n## Instructions\n\n1. Read the approved decomposition from `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md`.\n\n2. Create the explorations directory:\n ```\n mkdir -p {docs_dir}/epic-plans/{epic_slug}/explorations/\n ```\n\n3. For each sub-task in the decomposition, perform a focused exploration:\n - Search for specific files and patterns relevant to the sub-task\n - Identify implementation options and tradeoffs\n - Reference the holistic codebase exploration (`{docs_dir}/epic-plans/{epic_slug}/codebase-exploration.md`) and research findings (`{docs_dir}/epic-plans/{epic_slug}/research-findings.md`) for context\n - Default to lightweight exploration \u2014 only go deeper when the holistic exploration left significant gaps for a specific sub-task\n\n4. Write an exploration document for each sub-task to `{docs_dir}/epic-plans/{epic_slug}/explorations/NN-{subtask-slug}.md` (using zero-padded numbering, e.g., `01-add-pipeline-json.md`, `02-create-instruction-files.md`).\n\n5. Each exploration document MUST include these exactly named sections:\n\n```markdown\n# {Sub-task title}\n\n## Context\n{Brief description of the sub-task scope and its role within the epic.}\n\n## Relevant Code\n{Specific files, functions, and patterns relevant to this sub-task. Reference with file_path:line_number format.}\n\n## Implementation Options\n{Viable approaches for implementing the sub-task. For each option: description, pros, cons.}\n\n## Recommendation\n{Which option to pursue and why. Include any caveats or risks.}\n```\n\n6. **Word count guidance**: Target 300-500 words per document. Keep the exploration lightweight. Only exceed this limit if the holistic codebase exploration left significant gaps for a specific sub-task.\n\n## Return\n\nConfirm one exploration document was written per sub-task under `{docs_dir}/epic-plans/{epic_slug}/explorations/` and return a concise summary of the discovered code areas and recommended approaches across the sub-tasks.\n",
14265
14457
  "frame-goals-and-nfrs.md": 'Frame the business goals, desired end-state, and non-functional requirements (NFRs) for this work before any functional decomposition or drafting. When the goals and the desired end-state of the system are clear, the functional requirements become much easier to design accurately. This step is documentary: it records the framing and classifies what is unclear. It does NOT pause and does NOT generate a decision page (interactive surfaces handle that separately).\n\n## Inputs\n\n- The idea or epic description for this run, plus any prior planning artifacts the earlier steps wrote into this run\'s working directory under `{docs_dir}` (for example: research findings, codebase exploration, resolved uncertainties, duplicate assessment). Read whichever of these exist; proceed without the ones that do not.\n\n## Instructions\n\n1. From the inputs, derive and state plainly:\n - **Business goal** \u2014 the business value this work delivers and why it matters.\n - **Desired end-state** \u2014 the concrete state the system should reach once this work is done.\n - **System behavior** \u2014 how the system must behave to complete its task (the quality attributes in prose, not a feature list).\n\n2. Identify the non-functional requirements. Consider every one of these canonical NFR categories and include the ones that genuinely apply (omit categories that do not):\n - security/privacy\n - performance/latency\n - reliability/failure-modes\n - observability/auditability\n - accessibility/UX\n - data-integrity/migration\n - compatibility\n - operability/config\n - compliance/SOC2\n - rollout/reversibility\n\n For each NFR you include, write three things: the `requirement`, its `implication` (what this requirement changes about the implementation), and a `status`. **An NFR with no concrete implication is boilerplate \u2014 drop it rather than record it.**\n\n3. Classify each NFR\'s `status` with this rubric:\n - `confirmed` \u2014 only if it is explicitly stated in the idea/description/standards or is directly observable in the codebase.\n - `assumed` \u2014 only if it is a low-risk, conventional, and reversible default.\n - `open` \u2014 if it touches architecture, the data model, security, user-visible behavior, migration, or irreversible Jira creation and is not settled. Be willing to mark things `open`: surfacing an unclear NFR is the point of this step.\n\n4. If this work is an epic (it will be decomposed into multiple sub-tasks or child tickets), draft a provisional **recommended implementation order**. For each slice, record a short title, its hard prerequisites (`depends_on` \u2014 what must land first), any soft sequencing preferences (`recommended_after` \u2014 not hard blockers), and a one-line rationale. Keep hard prerequisites separate from soft sequencing. Do not create Jira dependency links \u2014 the order is delivered into the epic downstream.\n\n5. Write the framing to a file named `goals-and-nfrs.md` in this run\'s working directory \u2014 the **same directory the earlier exploration/research steps in this pipeline wrote to** under `{docs_dir}`. Getting this path right matters: downstream steps read `goals-and-nfrs.md` from that exact directory and silently degrade (they see no framing) if it lands elsewhere. The directory differs by pipeline:\n - **plan-epic**: the epic plan directory, `docs/epic-plans/<epic-slug>/` (alongside `codebase-exploration.md` and `epic-plan.md`).\n - **idea-to-ticket**: the run directory, `docs/idea-to-ticket/<slug>-<run-id>/` (alongside `research-pack.md` and `resolved-uncertainties.md`).\n\n Use this structure (no markdown tables, no `- [ ]` checkboxes \u2014 BAPI-320 hygiene):\n\n```markdown\n# Goals & Non-Functional Requirements\n\n## Business Goal\n{business goal}\n\n## Desired End-State\n{desired end-state}\n\n## System Behavior\n{how the system must behave to complete its task}\n\n## Non-Functional Requirements\n- **{nfr category}** ({confirmed, assumed, or open}): {the requirement}. Implication: {what it changes about the implementation}.\n- ...\n\n## Recommended Implementation Order\n(Epics only; omit this section for a single task or spike.)\n1. {slice title} \u2014 depends on: {hard prerequisites or "none"}; recommended after: {soft preferences or "none"}. Rationale: {one line}.\n2. ...\n```\n\n## Return\n\nConfirm `goals-and-nfrs.md` was written, report the counts of `confirmed` / `assumed` / `open` NFRs, and state whether a recommended implementation order was produced (epics) or skipped (single task/spike).\n',
14266
- "gather-and-attach-materials.md": 'Post-create materials-completeness step. Gather the reachable local text materials a freshly-created ticket references and attach them via `attachment` (operation: `"upload"`), while recording everything that is record-only. This is the POST-CREATE half of the upload-time materials-completeness pass (BAPI-423); the PRE-CREATE half \u2014 inventorying and writing the `## Materials & Access` section into the draft \u2014 already ran in the `jira-ticket-writer` agent.\n\n## Inputs\n\n- `{ticket_number}` \u2014 the real Jira key of the already-created ticket (e.g. `BAPI-423`). Attachment is a POST-CREATE step; never attempt to attach before the key exists.\n- `{draft_file_path}` \u2014 path to the draft markdown that carries the trailing `## Materials & Access` section.\n- `{auto_approve_external}` \u2014 the unattended-vs-interactive signal (named for consistency with `upload-and-track.md`). **Polarity is counter-intuitive: `"true"` means UNATTENDED, which is the MORE restrictive mode here** \u2014 skip all prompts AND keep external/auth-gated materials record-only (never auto-attach them). It does NOT grant permission to attach external materials. Any other value (including `"false"`, missing, or empty) means an interactive invocation that MAY prompt for external/auth-gated materials. Invocations from `write-ticket` and `full-automation` are always unattended (`"true"`) for this step.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is authorized to call `attachment` (operations: `list`, `upload`) and `update_ticket_description` as directed below.\n\n1. **Read the record.** Read `{draft_file_path}` and parse its trailing `## Materials & Access` section. Collect the inventoried items grouped under *Reachable Local Files*, *External/Auth-Gated Links*, *Design/UI Comps (Fetchable)* (when present), and *Binary/Image Materials (Record-Only)*. If there is no `## Materials & Access` section, there is nothing to gather \u2014 return a no-op success.\n\n2. **Deduplicate first.** Call the `attachment` MCP tool with `operation` set to `"list"` and `ticket_number` set to `{ticket_number}` BEFORE uploading anything, so a resumed or re-run invocation does not re-attach a material that is already present. Compare against the deterministic filenames computed in step 4 and skip any that already exist.\n\n3. **Source classification (scheme-based, no network probe).** Honor the classification already recorded in the draft:\n - **Reachable Local Files** (local filesystem paths) are the only **low-risk** materials eligible for auto-attach \u2014 proceed to step 4.\n - **External/Auth-Gated Links** (every `http(s)` URI, even if explicitly linked) are **record-only** on unattended paths. If `{auto_approve_external}` is `"true"` (or the invocation is from `write-ticket` / `full-automation`), leave them record-only and never auto-attach. (Mind the polarity: `auto_approve_external = "true"` means we are in unattended mode, so external materials must stay record-only \u2014 `"true"` is NOT permission to attach them.) Only an explicitly interactive invocation (`auto_approve_external` is any non-`"true"` value) may prompt the user to confirm before attaching.\n - **Binary/Image Materials** are **record-only** in this step: the `attachment` tool\'s `upload` operation is UTF-8 text only, so never attempt a binary upload. (Binary-upload capability is the fast-follow sibling ticket BAPI-424.)\n - **Design/UI Comps (Fetchable)** are **not uploaded by this step**. A fetchable design/UI comp is a reference (an `attachment_id` or path) to a comp that already lives on the Jira ticket (or is fetched at implementation time); this gather step neither re-uploads it nor UTF-8-encodes its bytes. Leave each such comp reference recorded with its `attachment_id`/path in the `## Materials & Access` record so a later implementation agent can download it into its worktree via the Jira attachment download capability.\n\n4. **Gather and size-tier each reachable local text material.** For each low-risk local text material:\n - Read the local file from disk.\n - If the content exceeds **200,000 characters**, SKIP the upload and RECORD it (note the path and that it was skipped for size) \u2014 do not attach it.\n - If the content is **<= 200,000 characters**, upload it RAW via `attachment` (operation: `"upload"`). Do NOT summarize locally: the backend already summarizes attached text at plan time, so the size tiers are backend behavior this step defers to. The agent performs NO local summarization.\n - Use a deterministic, sanitized filename of the form `{ticket_number}-material-{hash}.md` (using the `{ticket_number}` input from the Inputs section), where `{hash}` is the first 8 hex characters of the SHA-256 digest of the sanitized absolute source path. Pin this algorithm exactly (SHA-256, first 8 hex chars, of the sanitized absolute path) \u2014 do NOT substitute another hash \u2014 so the same source always maps to the same filename and the dedup in step 2 works across separate sessions and re-runs. Keep the sanitized source provenance inside the attachment body, not only in the filename.\n\n5. **`attachment` upload parameter discipline (Zod).** When calling `attachment` with `operation: "upload"`, pass `ticket_number`, the attachment filename, and the text content. OMIT the optional parameters `link_type` and `replace_existing` entirely when they are unused \u2014 do NOT pass `null` or empty strings for them. The Zod schemas reject `null`/empty values, so an unused optional parameter must be omitted rather than nulled.\n\n6. **Redact secrets everywhere.** Before writing any URL or access note ANYWHERE \u2014 the Jira `## Materials & Access` record, any warning or final-report output, and any local intermediate file \u2014 sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. Mirror the backend `_redact_forge_fields()` / `_sanitize_jira_error_message()` patterns. A location/access note must never expose a plaintext secret.\n\n7. **Warn, never halt (error handling).** This step must NEVER halt, prompt-to-fail, or fail the overarching command because a material could not be gathered or attached. Follow the warn-not-halt convention:\n - If an `attachment` upload call fails (or a file disappeared between inventory and upload), warn gracefully and continue with the next material.\n - On such a post-create attach failure, call `update_ticket_description` to record the failure in the issue\'s `## Materials & Access` record (the material became unavailable only after the issue existed). `update_ticket_description` is an existing MCP tool, not a backend change.\n - Everything knowable PRE-CREATE was already written into the description at create time, so `update_ticket_description` is reserved for these rarer post-create attach failures. This complements the existing `partial_success` recording convention in `upload-and-track.md`.\n - Apply the step 6 redaction to every warning and recorded note.\n\n## Return\n\nConfirm the outcome: which local materials were attached (with their deterministic filenames), which were skipped/recorded (over-size, external/auth-gated, or binary/image), any attach failures recorded via `update_ticket_description`, and that no failure halted the run.\n',
14458
+ "gather-and-attach-materials.md": 'Post-create materials-completeness step. Gather the reachable local text materials and eligible local design/UI comp images a freshly-created ticket references and attach them via `attachment` (operation: `"upload"`), while recording everything that is record-only. This is the POST-CREATE half of the upload-time materials-completeness pass (BAPI-423); the PRE-CREATE half \u2014 inventorying and writing the `## Materials & Access` section into the draft \u2014 already ran in the `jira-ticket-writer` agent.\n\n## Inputs\n\n- `{ticket_number}` \u2014 the real Jira key of the already-created ticket (e.g. `BAPI-423`). Attachment is a POST-CREATE step; never attempt to attach before the key exists.\n- `{draft_file_path}` \u2014 path to the draft markdown that carries the trailing `## Materials & Access` section.\n- `{auto_approve_external}` \u2014 the unattended-vs-interactive signal (named for consistency with `upload-and-track.md`). **Polarity is counter-intuitive: `"true"` means UNATTENDED, which is the MORE restrictive mode here** \u2014 skip all prompts AND keep external/auth-gated materials record-only (never auto-attach them). It does NOT grant permission to attach external materials. Any other value (including `"false"`, missing, or empty) means an interactive invocation that MAY prompt for external/auth-gated materials. Invocations from `write-ticket` and `full-automation` are always unattended (`"true"`) for this step.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is authorized to call `attachment` (operations: `list`, `upload`) and `update_ticket_description` as directed below.\n\n1. **Read the record.** Read `{draft_file_path}` and parse its trailing `## Materials & Access` section. Collect the inventoried items grouped under *Reachable Local Files*, *External/Auth-Gated Links*, *Design/UI Comps (Fetchable)* (when present), and *Binary/Image Materials (Record-Only)*. If there is no `## Materials & Access` section, there is nothing to gather \u2014 return a no-op success.\n\n2. **Deduplicate first.** Call the `attachment` MCP tool with `operation` set to `"list"` and `ticket_number` set to `{ticket_number}` BEFORE uploading anything, so a resumed or re-run invocation does not re-attach a material that is already present. Compare against the deterministic filenames computed in step 4 (for both text materials and design comp image uploads) and skip any that already exist.\n\n3. **Source classification (scheme-based, no network probe).** Honor the classification already recorded in the draft:\n - **Reachable Local Files** (local filesystem paths that are **NOT tracked in version control**) are **low-risk** materials eligible for auto-attach \u2014 proceed to step 4. The pre-create inventory already excluded version-controlled files (source code and in-repo docs are already available in the repository and are never attached \u2014 they are cited inline as *Relevant code*). As a safety net, this step must **never attach a file that is available in version control**: if any item listed under *Reachable Local Files* is a code file or otherwise clearly version-controlled, skip it and treat it as record-only.\n - **External/Auth-Gated Links** (every `http(s)` URI, even if explicitly linked) are **record-only** on unattended paths. If `{auto_approve_external}` is `"true"` (or the invocation is from `write-ticket` / `full-automation`), leave them record-only and never auto-attach. (Mind the polarity: `auto_approve_external = "true"` means we are in unattended mode, so external materials must stay record-only \u2014 `"true"` is NOT permission to attach them.) Only an explicitly interactive invocation (`auto_approve_external` is any non-`"true"` value) may prompt the user to confirm before attaching.\n - **Binary/Image Materials (Record-Only)** \u2014 ordinary/unrelated binaries (arbitrary screenshots, PDFs, ZIPs, and other binaries not design-relevant) stay **record-only** in this step; never attempt to upload them.\n - **Design/UI Comps (Fetchable)** \u2014 split by whether the comp is a *local* file or a reference to something already remote:\n - A **local design/UI comp image** (a reachable local file whose extension maps to an allowlisted image MIME type \u2014 `image/png`, `image/jpeg`, `image/webp`, `image/gif`) is eligible for auto-attach via the (now allowlist-guarded) binary upload path \u2014 proceed to step 4.\n - A **non-local design reference** \u2014 a Jira `attachment_id` reference on another ticket, or an external/auth-gated design link \u2014 is **not** uploaded by this step; it is a reference to a comp that already lives on Jira (or is fetched at implementation time). This gather step neither re-uploads it nor re-encodes its bytes. Leave each such reference recorded with its `attachment_id`/path in the `## Materials & Access` record so a later implementation agent can download it into its worktree via the Jira attachment download capability.\n\n4. **Gather and size-tier each reachable local text material; compute deterministic filenames for local design comp images.**\n - For each low-risk local **text** material:\n - Read the local file from disk.\n - If the content exceeds **200,000 characters**, SKIP the upload and RECORD it (note the path and that it was skipped for size) \u2014 do not attach it.\n - If the content is **<= 200,000 characters**, upload it RAW via `attachment` (operation: `"upload"`). Do NOT summarize locally: the backend already summarizes attached text at plan time, so the size tiers are backend behavior this step defers to. The agent performs NO local summarization.\n - Use a deterministic, sanitized filename of the form `{ticket_number}-material-{hash}.md` (using the `{ticket_number}` input from the Inputs section), where `{hash}` is the first 8 hex characters of the SHA-256 digest of the sanitized absolute source path.\n - For each eligible local **design/UI comp image** identified in step 3:\n - Use a deterministic filename of the form `{ticket_number}-material-{hash}{ext}`, where `{hash}` is computed the same way (first 8 hex characters of the SHA-256 digest of the sanitized absolute source path) and `{ext}` is the lowercased allowlisted source extension (`.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`).\n - Pin the hash algorithm exactly (SHA-256, first 8 hex chars, of the sanitized absolute path) \u2014 do NOT substitute another hash \u2014 so the same source always maps to the same filename and the dedup in step 2 works across separate sessions and re-runs. Compute comp filenames before the step 2 dedup comparison is applied. Keep the sanitized source provenance inside the text attachment body (not applicable to binary comp uploads), not only in the filename.\n\n5. **`attachment` upload parameter discipline (Zod).**\n - For text materials, call `attachment` with `operation: "upload"`, `ticket_number`, the deterministic attachment filename, and the text `content`.\n - For design comp image uploads, call `attachment` with `operation: "upload"`, `ticket_number`, `file_path` (the local source path), and `file_name` set to the deterministic comp filename from step 4 \u2014 pass `file_path` rather than reading and UTF-8-encoding the bytes yourself, so `resolveUploadAttachment()` performs binary detection, the MIME allowlist check, and base64 encoding. Never UTF-8-encode image bytes locally.\n - In both cases, OMIT the optional parameters `link_type` and `replace_existing` entirely when they are unused \u2014 do NOT pass `null` or empty strings for them. The Zod schemas reject `null`/empty values, so an unused optional parameter must be omitted rather than nulled.\n\n6. **Redact secrets everywhere.** Before writing any URL or access note ANYWHERE \u2014 the Jira `## Materials & Access` record, any warning or final-report output, and any local intermediate file \u2014 sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. Mirror the backend `_redact_forge_fields()` / `_sanitize_jira_error_message()` patterns. A location/access note must never expose a plaintext secret.\n\n7. **Warn, never halt (error handling).** This step must NEVER halt, prompt-to-fail, or fail the overarching command because a material could not be gathered or attached. Follow the warn-not-halt convention:\n - If an `attachment` upload call fails (or a file disappeared between inventory and upload), warn gracefully and continue with the next material.\n - This includes design comp image uploads: an unsupported/disallowed MIME type, an oversize image (`> 10 MB`), a missing local file, a malformed upload payload, or a Jira upload failure must all be warned and skipped, never halting the run.\n - On such a post-create attach failure, call `update_ticket_description` to record the failure in the issue\'s `## Materials & Access` record (the material became unavailable only after the issue existed). `update_ticket_description` is an existing MCP tool, not a backend change.\n - Everything knowable PRE-CREATE was already written into the description at create time, so `update_ticket_description` is reserved for these rarer post-create attach failures. This complements the existing `partial_success` recording convention in `upload-and-track.md`.\n - Apply the step 6 redaction to every warning and recorded note.\n\n## Return\n\nConfirm the outcome, reporting each category separately: which local text materials were attached (with their deterministic filenames), which design/UI comp images were attached (with their deterministic filenames), which materials were skipped/recorded as record-only (over-size text, external/auth-gated links, ordinary/unrelated binaries, or non-local design references), any attach failures recorded via `update_ticket_description`, and that no failure halted the run.\n',
14267
14459
  "get-prd.md": "# get_prd\n\nRetrieve an already-generated **Product Requirements Document (PRD)** for a Jira\nticket.\n\nThis tool only **fetches** an existing PRD \u2014 it does **not** start or trigger\ngeneration. If no PRD exists yet (or you need a fresh one), call `request_prd`\nfirst; it starts the async generation and `get_prd` retrieves the result once\nprocessing completes.\n\nThe PRD is product/stakeholder-facing: problem framing, goals, non-goals, target\nusers, success metrics, product requirements, scope, and risks. Present the\nreturned markdown verbatim without summarizing.\n\n## Parameters\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `ticket_number` | string | \u2014 | Jira ticket key in `PROJECT-NUMBER` format (e.g. `BAPI-123`). |\n| `save_locally` | boolean | `true` | Save the retrieved PRD to a local file. Set to `false` to skip saving. |\n\nLocal saves go to `BAPI_DOCS_DIR/prd/{ticket}-prd-plan.md`.\n\n## Return\n\n- The full PRD as markdown text when one exists.\n- A `404` / not-found response when no PRD is ready yet \u2014 that means generation\n has not run, not that the tool failed. Call `request_prd` to generate one.\n",
14268
14460
  "learn-architecture.md": "## Objective\n\nExplore the codebase to identify architectural principles, directory conventions, design patterns, and data flow, then draft `architecture_instructions` for the project config.\n\n## Instructions\n\n### Phase 1 \u2014 Principles Research\n\nResearch the codebase to identify architectural principles and conventions. For each area below, examine at least 5 representative files. Cite file paths for every pattern. Include code examples (5-15 lines) showing correct usage. Where relevant, include a WRONG example showing the common mistake.\n\nFor each pattern, classify its evidence level:\n- `ENFORCED` \u2014 consistently followed across the codebase, violations would be bugs\n- `CONVENTION` \u2014 commonly observed, occasional deviations exist\n- `ASPIRATIONAL` \u2014 intended direction, not yet consistently applied\n\nResearch areas:\n1. **Architectural coding patterns**: Search `api/routes/` and `api/library/` for separation of concerns, layer boundaries, function-vs-class decisions. Read files matching `*_lib.py`, `*_utils.py`, `*_helpers.py` to document module naming suffix conventions.\n2. **Design patterns**: Search for factory functions, strategy patterns, middleware chains, registry patterns, and dependency injection in `api/` and `src/python/`. Cite concrete usage with file path and function name.\n3. **Dependency management**: Read `requirements.in`, `requirements-dev.in`, and `package.json` files to document how dependencies are declared and organized.\n4. **Error handling architecture**: Search for `log_exception_to_sentry` and `HTTPException` usage patterns across `api/routes/` to document the system-wide error propagation strategy.\n5. **Configuration management**: Search for `os.environ` and `get_config_field` usage to document the two-tier system (env vars vs. database config).\n6. **Tech stack detection**: Read `requirements.in`, `package.json`, and `main.py` to identify primary languages, frameworks, and key libraries.\n7. **Security architecture**: Read `api/routes/setup/auth.py` and search for `require_api_key`, `require_api_session`, and `verify_repo_access` to document authentication and authorization design.\n8. **Agent prompting conventions**: Read files in `src/python/llms/agents/` to document prompt construction, section headers, dynamic content delimiters, and role-based personas.\n\nScope exclusion: Do NOT document testing patterns. Skip the `tests/` directory entirely.\n\nWrite findings to `{docs_dir}/tmp/architecture-principles.md`.\n\n### Phase 2 \u2014 Structure & Data Flow Research\n\n1. Call the `regenerate_directory_map` MCP tool to get a fresh directory map.\n2. Read the principles document from Phase 1.\n3. Research and document:\n - **Directory conventions**: For each major directory, document purpose, file naming, internal structure, and an example file.\n - **Module boundaries and import patterns**: Which directories are distinct modules and how they interact. Document import restrictions.\n - **Data flow patterns**: Trace 2-3 complete request paths (synchronous, async background task, agent orchestration).\n - **Integration patterns**: How external services (Jira, GitHub/Bitbucket, LLMs, Pinecone, PostgreSQL) are integrated.\n - **Background task patterns**: The async task lifecycle with `asyncio.create_task`, semaphores, and error reporting.\n\nWrite findings to `{docs_dir}/tmp/architecture-structure.md`.\n\n### Phase 3 \u2014 Draft\n\n1. Read both research documents.\n2. Combine into a single `architecture_instructions` draft with these required sections:\n - **1. Core Principles** \u2014 Each principle with evidence level and explanation.\n - **2. Layered Architecture** \u2014 Layer separation, dependency rule, agent vs orchestration logic.\n - **3. Directory Conventions** \u2014 Purpose, naming, structure for each major directory.\n - **4. Data Flow Patterns** \u2014 Complete request path traces with file paths.\n - **5. Technical Standards** \u2014 Coding style, async patterns, database, schema, LLM integration, config, dependencies.\n - **6. Error Handling & Monitoring** \u2014 Error propagation strategy, Sentry integration, Langfuse tracing.\n - **7. Security & Authentication** \u2014 Auth architecture, session model, permission model.\n - **8. Agent Prompting Conventions** \u2014 Prompt construction, section headers, content delimiters.\n - **9. Integration Points** \u2014 External service clients and their calling patterns.\n - **10. AI Code Generation Guidelines** \u2014 Anti-patterns, duplication avoidance, pattern compliance checklist.\n\n3. Write the draft to `{docs_dir}/standards/architecture_instructions.md`.\n\n## Return\n\nReturn a brief summary of what was learned about the project's architecture (core principles, directory conventions, data flow), citing the key files inspected, and confirm the draft was written to `{docs_dir}/standards/architecture_instructions.md`.\n",
14269
14461
  "learn-backend-correctness.md": "## Objective\n\nExplore the codebase to identify correctness standards for backend code, then draft the corresponding correctness standards document.\n\n## Target Type\n\n- **Type**: `backend_correctness`\n- **Field name**: `backend_correctness_standards`\n- **Scope**: Server-side code: Python, Ruby, Go, Java, C#, Node.js server code, API routes, business logic.\n\n## Instructions\n\n### Phase 1 \u2014 Explore Correctness Patterns\n\nFocus on implementation correctness: how to write code that is correct, idiomatic, and robust within this project's conventions.\n\n1. **File Type Detection**: Search by filename pattern for files matching `**/*.py` in `api/` and `src/python/`. If very few or no files exist, note this and draft minimal instructions.\n\n2. **Convention Analysis**: Read 3-5 representative files in `api/routes/` and `api/library/` to identify:\n - Structure patterns (imports, exports, class structure, function ordering)\n - Naming conventions (variables, functions, classes, files)\n - Framework conventions and idioms\n - Best practices followed\n - Issues and inconsistencies\n\n Also read files to document:\n - Error handling implementation (try/except ordering, Sentry calls) with CORRECT/WRONG examples\n - Authentication implementation (auth check sequence) with code examples\n - Database call patterns (`postgres_helpers` (bool, result) tuple handling) with CORRECT/WRONG examples\n - Input validation patterns (Pydantic models, naming conventions)\n - HTTP client patterns (error handling, JiraError sanitization)\n - Async implementation patterns (`asyncio.to_thread()` for blocking code)\n\n### Phase 2 \u2014 Draft\n\nDraft correctness standards as clear, actionable instructions for an AI code generation agent. Cover:\n- Code structure and organization requirements\n- Naming conventions to follow\n- Framework-specific patterns and idioms\n- Security requirements relevant to this code type\n- Performance considerations\n- Common mistakes to avoid\n- Guards against common AI weaknesses: duplicative code, verbose implementations, security vulnerabilities\n\nAlso include:\n- Route handler boilerplate (auth -> validation -> business logic -> error handling)\n- Database interaction patterns with CORRECT/WRONG examples\n- Exception handling pattern (specific first, HTTPException re-raise, generic with Sentry)\n- Sentry reporting patterns and common mistakes\n- Input sanitization rules (JiraError headers, raw exception messages)\n\nWrite the draft to `{docs_dir}/standards/backend_correctness_standards.md`.\n\n## Return\n\nReturn a brief summary of what was learned about backend correctness conventions (structure, naming, error handling, auth, DB patterns), citing the key files inspected, and confirm the draft was written to `{docs_dir}/standards/backend_correctness_standards.md`.\n",
@@ -14291,7 +14483,7 @@ var INSTRUCTIONS = {
14291
14483
  init_version_generated();
14292
14484
 
14293
14485
  // src/readme.generated.ts
14294
- var README = '# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [Usage Documentation](#usage-documentation)\n - [Tier 1 \u2014 Regularly useful](#tier-1--regularly-useful)\n - [Tier 2 \u2014 Occasionally useful](#tier-2--occasionally-useful)\n - [Tier 3 \u2014 Now and then](#tier-3--now-and-then)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./CONDUCTOR.md).\n\n## Getting Started\n\n### Quick start\n\nFrom your **project root**, install and connect in one command:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\n`install-bridge` scaffolds the project, writes your editor\'s MCP config with real\nvalues, verifies connectivity, persists your API key to the user-scoped credential\nstore, and opens a fresh agent session to finish setup (`/install-bridge` then\n`/learn-repository`). The only inputs are an **API key** (generate one on the Bridge\nAPI web UI **Security** page) and a **repo name** \u2014 everything else is derived. Add\n`--dry-run` to preview every step without writing, pinging, or spawning anything.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` (to derive the\n remaining config fields from your codebase) and then `/learn-repository`.\n\nThe only inputs are an **API key** and a **repo name** (everything else is\nderived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); the command consumes a key, it\n never mints one. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` \u2192 `BAPI_REPO_NAME` env \u2192 an inferred default you\n confirm interactively. It MUST match the server-side repository registration.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config without\n prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n#### Manual Setup (Alternative)\n\n##### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n##### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n##### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n##### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n##### Upgrading (details)\n\nThe one-command `npx -y @bridge_gpt/mcp-server upgrade` (shown above) is the\nrecommended path. The legacy flag `npx -y @bridge_gpt/mcp-server --upgrade` still\nworks: it runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version\nsummary, then re-runs the full `--init` scaffolding flow to update your slash\ncommands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships read-only SFCC platform-introspection tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique (initial round), an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The second-opinion pass is included by default and can be skipped with `--rounds=1`.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` skip the automatic second-opinion review step while preserving downstream evaluation and decision-capture work (cheaper single-pass review). `--rounds=2` (default) runs the full two-round review.\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default.\n\n**3. Brainstorm**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of three modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), or **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`). Discovery needs no extra configuration. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists.\n- **How to use it:** ask your agent to brainstorm \u2014 *"Brainstorm approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design brainstorm for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery brainstorm \u2014 `request_brainstorm` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Commit staged changes and open a pull request |\n| `/check-ci [PROJ-123]` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, brainstorms, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The seven read tools must be enabled with a profile (step 3).\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. Prefer a single config \u2014 a multi-entry `configs[]` array forces you to pass an explicit `instance` on every call. `dw.json` is auto-added to git exclude and must never be committed. Alternatively, export `SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n**3. Enable the read tools.** Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\nWithout this, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** Ask your agent to run `sfcc_setup_status` (expect all checks \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\n</details>\n\n### Tools\n\nAll SFCC tools are read-only and target a developer sandbox. Oversized responses are automatically saved to `BAPI_DOCS_DIR/sfcc/` and previewed inline.\n\n**Diagnostics** (always available, no profile needed)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, and AM token acquisition.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).\n\n**System object model** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one type\'s definition, optionally with its full attribute definitions (`expand_attribute_definitions`).\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**Custom object definitions** (needs the `sfcc` profile)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object types, so `object_type` must already be known.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type. Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability.\n\n**Site preferences** (needs the `sfcc` profile; sandbox only)\n- `site_preference_get` \u2014 read a preference group\'s effective preferences.\n- `site_preference_search` \u2014 search/filter preferences within a group.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all omit `--model` (the agent uses its default) and surface a per-ticket warning rather than failing the spawn. `--dry-run` does **not** fetch tiers or inject `--model`.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 `conductor install-git-hooks`, the `conductor_done_gate` and `conductor_auto_merge_enabled` config fields, and the observability stream \u2014 lives in **[CONDUCTOR.md](./CONDUCTOR.md)**.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **59 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_brainstorm`/`get_brainstorm`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: initial clarifying questions + critique, automatic second-opinion pass (default), then evaluation and decision capture. Pass `--rounds=1` to skip the second-opinion step (`--rounds=2` is the default). | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';
14486
+ var README = '# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [Usage Documentation](#usage-documentation)\n - [Tier 1 \u2014 Regularly useful](#tier-1--regularly-useful)\n - [Tier 2 \u2014 Occasionally useful](#tier-2--occasionally-useful)\n - [Tier 3 \u2014 Now and then](#tier-3--now-and-then)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./CONDUCTOR.md).\n\n## Getting Started\n\n### Quick start\n\nFrom your **project root**, install and connect in one command:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\n`install-bridge` scaffolds the project, writes your editor\'s MCP config with real\nvalues, verifies connectivity, persists your API key to the user-scoped credential\nstore, and opens a fresh agent session to finish setup (`/install-bridge` then\n`/learn-repository`). The only inputs are an **API key** (generate one on the Bridge\nAPI web UI **Security** page) and a **repo name** \u2014 everything else is derived. Add\n`--dry-run` to preview every step without writing, pinging, or spawning anything.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` (to derive the\n remaining config fields from your codebase) and then `/learn-repository`.\n\nThe only inputs are an **API key** and a **repo name** (everything else is\nderived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); the command consumes a key, it\n never mints one. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` \u2192 `BAPI_REPO_NAME` env \u2192 an inferred default you\n confirm interactively. It MUST match the server-side repository registration.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config without\n prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n#### Manual Setup (Alternative)\n\n##### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n##### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n##### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n##### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n##### Upgrading (details)\n\nThe one-command `npx -y @bridge_gpt/mcp-server upgrade` (shown above) is the\nrecommended path. The legacy flag `npx -y @bridge_gpt/mcp-server --upgrade` still\nworks: it runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version\nsummary, then re-runs the full `--init` scaffolding flow to update your slash\ncommands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships read-only SFCC platform-introspection tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique plus an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The backend executor now owns all review round orchestration (including the second-opinion round) server-side; pass `--rounds=1` for a cheaper single-pass review, `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` request a cheaper single-pass review (no second-opinion round) while preserving downstream evaluation and decision-capture work \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide (it falls back to a full second-opinion review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved).\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default.\n\n**3. Brainstorm**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of three modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), or **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`). Discovery needs no extra configuration. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists.\n- **How to use it:** ask your agent to brainstorm \u2014 *"Brainstorm approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design brainstorm for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery brainstorm \u2014 `request_brainstorm` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Commit staged changes and open a pull request |\n| `/check-ci [PROJ-123]` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, brainstorms, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The seven read tools must be enabled with a profile (step 3).\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. Prefer a single config \u2014 a multi-entry `configs[]` array forces you to pass an explicit `instance` on every call. `dw.json` is auto-added to git exclude and must never be committed. Alternatively, export `SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n**3. Enable the read tools.** Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\nWithout this, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** Ask your agent to run `sfcc_setup_status` (expect all checks \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\n</details>\n\n### Tools\n\nAll SFCC tools are read-only and target a developer sandbox. Oversized responses are automatically saved to `BAPI_DOCS_DIR/sfcc/` and previewed inline.\n\n**Diagnostics** (always available, no profile needed)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, and AM token acquisition.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).\n\n**System object model** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one type\'s definition, optionally with its full attribute definitions (`expand_attribute_definitions`).\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**Custom object definitions** (needs the `sfcc` profile)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object types, so `object_type` must already be known.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type. Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability.\n- `custom_object_definition_attribute_create` \u2014 **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.\n- `custom_object_definition_attribute_update` \u2014 **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH \u2026/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.\n\n**Site preferences** (needs the `sfcc` profile; sandbox only)\n- `site_preference_get` \u2014 read a preference group\'s effective preferences.\n- `site_preference_search` \u2014 search/filter preferences within a group.\n- `site_preference_values_set` \u2014 **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all omit `--model` (the agent uses its default) and surface a per-ticket warning rather than failing the spawn. `--dry-run` does **not** fetch tiers or inject `--model`.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 `conductor install-git-hooks`, the `conductor_done_gate` and `conductor_auto_merge_enabled` config fields, and the observability stream \u2014 lives in **[CONDUCTOR.md](./CONDUCTOR.md)**.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **59 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_brainstorm`/`get_brainstorm`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';
14295
14487
 
14296
14488
  // src/update-check.ts
14297
14489
  init_version_generated();
@@ -14632,9 +14824,10 @@ var COMMANDS = {
14632
14824
  "create-doc.md": 'Generate a design document (TDD, FSD, or PRD) for a Jira ticket, wait for the result, and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, a required `--doc-type` flag, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--doc-type` appears followed by one of `tdd`, `fsd`, or `prd`, capture that as `doc_type`.\n - If `--doc-type` is absent, or is followed by anything other than `tdd`/`fsd`/`prd` (or is the last token), stop immediately and report: "Usage error: --doc-type requires a document type (tdd, fsd, or prd)."\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate ticket key format**: Confirm the ticket key matches the Jira key pattern `[A-Za-z][A-Za-z0-9]+-\\d+`. If it does not match (or `ticket_key` is empty or missing), stop immediately and display:\n\n ```\n Usage: /create-doc <ticket_key> --doc-type <tdd|fsd|prd> [--second-opinion [provider]] [--provider <name>] (e.g., /create-doc BAPI-150 --doc-type fsd)\n ```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Design Document\n\nCall the `create_doc` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `doc_type`: the parsed `doc_type` (`tdd`, `fsd`, or `prd`)\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nThis step may take 2-4 minutes while the backend processes the document.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nDesign document generation failed: <error message from the tool>\n```\n\nIf generation did not finish, the document can be retrieved later with the `get_doc` MCP tool using the same `ticket_number` and `doc_type`.\n\n## Step 4 \u2014 Confirm Success\n\nResolve the local file path from `doc_type`:\n- `tdd` \u2192 `{docs_dir}/architecture/<ticket_key>-architecture-plan.md`\n- `fsd` \u2192 `{docs_dir}/fsd/<ticket_key>-fsd-plan.md`\n- `prd` \u2192 `{docs_dir}/prd/<ticket_key>-prd-plan.md`\n\nDisplay a confirmation message:\n\n```\nDesign document generated successfully for <ticket_key>\nSaved to: <local file path>\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Design Document Report\n\n- **Ticket**: <ticket_key>\n- **Doc Type**: <doc_type>\n- **Status**: Generated successfully\n- **Local File**: <local file path>\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n',
14633
14825
  "create-pr.md": '# Create PR: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), fetches the ticket summary, determines the base branch, and creates a pull request on the configured VCS provider. It is designed to run after `/commit-ticket` completes.\n\nIf any critical stage fails (Stage 0), stop immediately and report which stage failed and why. Non-critical stages (Stage 1 and Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 3-stage pipeline to create a pull request for a Jira ticket. Execute all stages in sequence.\n\n## Stage 0 \u2014 Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a single required `ticket_key` argument. The expected format is a Jira ticket key such as `BAPI-150` or `PROJ-123` \u2014 one or more uppercase letters, a hyphen, and one or more digits (regex: `[A-Z]+-\\d+`). If `$ARGUMENTS` is empty or the value does not match the expected format, stop immediately and display:\n\n ```\n Invalid ticket key format: \'<value>\'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /create-pr <ticket_key> (e.g., /create-pr BAPI-150)\n ```\n\n2. **Connectivity check**: Call the `ping` MCP tool (no parameters). If the ping fails or does not return `"status": "ok"`, stop immediately and display:\n\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n3. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `head_branch`. Verify that `head_branch` contains the `ticket_key` (case-insensitive comparison). If the branch does not contain the ticket key, stop immediately and display:\n\n ```\n Current branch \'<head_branch>\' does not contain ticket key <ticket_key>.\n Please switch to the correct feature branch before running /create-pr.\n ```\n\n4. **Resolve base branch**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch`. If the tool returns null, or an HTTP 400 Validation Error / Invalid field name, treat it as not set and fallback to `main`. Store the resolved value as `base_branch`.\n\n5. **Fetch ticket summary**: Call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key`. Extract the ticket summary from the response. If the tool returns an error, log a warning and use a generic summary based on the ticket key.\n\n6. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Create Pull Request\n\n1. **Compose PR title**: Format the title as `<ticket_key>: <ticket_summary>`. Truncate to 72 characters if needed.\n\n2. **Compose PR body**: Build a PR body that includes:\n - A brief description derived from the ticket summary\n - A plain text reference to the local implementation plan: `Implementation Plan available locally at {docs_dir}/plans/{ticket_key}-plan.md` (do not use markdown hyperlink syntax \u2014 the local path is sufficient for team members pulling the branch)\n\n3. **Create the pull request**: Call the `create_pull_request` MCP tool with:\n - `head_branch`: the current branch from Stage 0\n - `base_branch`: the resolved base branch from Stage 0\n - `title`: the composed PR title\n - `body`: the composed PR body\n\n4. **Handle the response with graceful degradation**:\n - If the response contains `available: false`: Report the reason to the user and skip to Stage 2. Do not halt the pipeline.\n - If the response contains `created: false`: Log "PR already exists" and store the returned PR URL. Continue to Stage 2.\n - If the response contains `created: true`: Store the PR URL. Continue to Stage 2.\n - If an HTTP error occurs: Warn the user with the error details and continue to Stage 2. Do not halt the pipeline.\n\nThis stage is **non-critical** \u2014 warn on failure, continue to Stage 2 regardless.\n\n## Stage 2 \u2014 Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Pull Request Report\n\n**Ticket**: <ticket_key>\n**Branch**: <head_branch>\n**Base Branch**: <base_branch>\n**PR URL**: <pr_url or "N/A \u2014 see warnings">\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 1: PR creation failed or unavailable),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** \u2014 display the report regardless.\n\n## Final Report\n\nOn success, display the structured report from Stage 2 confirming that the pull request was created (or already existed), including the branch name, base branch, PR URL, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0), display which stage failed and the error details.\n',
14634
14826
  "critique-ticket.md": 'Generate a ticket quality critique and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command triggers an AI-powered critique of a Jira ticket and saves the result locally. **No human confirmation gates** \u2014 the command runs end-to-end without pausing. `$ARGUMENTS` should contain a single Jira ticket key in `PROJECT-NUMBER` format (e.g., `BAPI-123`).\n\nIf any step fails, stop immediately and report which step failed and why.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n\n2. **Validate the ticket key format**: Validate that `ticket_key` matches the regex pattern `^[A-Za-z][A-Za-z0-9]+-\\d+$`. If validation fails, stop immediately and report: "The argument does not match the expected `PROJECT-NUMBER` format. Example: `BAPI-123`."\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Critique\n\nCall the `request_ticket_critique` MCP tool with:\n- `ticket_number`: the validated `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nIf the tool returns an error, stop immediately and report: "Critique generation failed." Include the error details.\n\n## Final Report\n\n**On success**, display a summary including:\n\n- Path to the saved critique document: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nNote: The critique was NOT pushed to Jira. To incorporate the critique findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n\n**On failure at any step**, stop immediately and display the step that failed and the error details.\n',
14827
+ "estimate-epic.md": "Estimate an entire Jira Epic or an explicit ticket-key group via the shared epic estimation orchestrator.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is either a Jira Epic key (e.g. `BAPI-518`) or an explicit `--tickets` key list \u2014 never both. This command calls the `estimate_epic` MCP tool, which delegates to the Bridge API epic estimation orchestrator, and renders the structured result.\n\nIf any step fails, stop immediately and report which step failed and why, preserving the user's originally entered epic key or ticket list in the report.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract exactly one key-source input, plus an optional `--allow-partial` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--tickets` appears, every token after it (up to the next flag or end of input) is the explicit ticket-key list \u2014 this is the `ticket_keys` mode.\n - Otherwise, the first token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`) is the `epic_key` \u2014 this is the epic mode.\n - `--allow-partial` may appear anywhere; if present, set `allow_partial_value = true`. If absent, omit `allow_partial` entirely (do not pass `false`).\n - Never resolve both an `epic_key` and a `ticket_keys` list from the same invocation \u2014 usage is one mode or the other.\n\n2. **Validate input**:\n - Usage forms: `/estimate-epic EPIC-KEY` or `/estimate-epic --tickets KEY-1 KEY-2 ...`, plus optional `--allow-partial`.\n - If neither an `epic_key` nor a `--tickets` list can be resolved, stop immediately and report:\n ```\n Usage: /estimate-epic EPIC-KEY [--allow-partial]\n /estimate-epic --tickets KEY-1 KEY-2 ... [--allow-partial]\n ```\n - If `--tickets` is present but followed by zero keys, stop immediately and report: \"`--tickets` requires at least one ticket key.\"\n - Do not invent or pass a `mode` parameter \u2014 there isn't one; the tool infers the source from whichever of `epic_key`/`ticket_keys` is supplied.\n\n## Step 2 \u2014 Call the Tool\n\nCall the `estimate_epic` MCP tool with:\n- `epic_key`: the resolved epic key \u2014 **only** when in epic mode. Omit entirely in ticket-key mode.\n- `ticket_keys`: the resolved ticket-key list \u2014 **only** when in ticket-key mode. Omit entirely in epic mode.\n- `allow_partial`: `allow_partial_value` if `--allow-partial` was passed; omit entirely otherwise (never pass `null`, an empty string, or an empty array for any absent field).\n\nNever pass both `epic_key` and `ticket_keys` in the same call.\n\nIf the tool returns an error envelope (a JSON object with an `error` field), stop and report the error message, preserving the epic key or ticket list the user originally entered.\n\n## Step 3 \u2014 Render the Result\n\nRender the successful result as a structured report \u2014 do not dump raw JSON by default:\n\n1. **Top**: the final estimate and its scale label (`estimate_label`) as the primary heading \u2014 this is the strongest element of the report.\n2. **Immediately after the summary**: `math_source`.\n3. **Next**: resolved child ticket keys (`child_ticket_keys`) and the per-child breakdown, presented compactly.\n4. **Only if non-empty**: a compact warning section listing `failed_child_keys` and `skipped_child_keys`.\n\nKeep the happy-path report concise and scannable. Use backticks for Jira keys and technical identifiers (e.g. `BAPI-518`).\n\n> Note: this tool does not accept a `recreate` parameter \u2014 the underlying epic estimation orchestrator (BAPI-522) always reuses cached child estimates and has no recreate knob to forward to.\n\n## Final Report\n\nOn successful completion, display a structured summary per Step 3 above. On failure, display the error message returned by the tool (or the usage error from Step 1), preserving the user's originally entered epic key or ticket list.\n",
14635
14828
  "explore-ticket.md": 'Explore the codebase for a task and recommend implementation options or surface clarifying questions.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is a free-form prompt describing a task you want to accomplish and your goals for it. This is **not** a Jira ticket key \u2014 it is plain text describing the work.\n\nExecute all exploration and analysis directly in the main conversation. The user should see exploration progress as it happens.\n\nIf any critical stage fails, stop immediately and report which stage failed and why.\n\n## Stage 0 \u2014 Setup\n\n1. **Parse prompt**: Extract the prompt text from `$ARGUMENTS`. Trim any surrounding whitespace. If the prompt is empty or whitespace-only, stop immediately and display: `Usage: /explore-ticket <prompt describing your task and goals>`\n\n2. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n3. **Generate output slug**: Create a kebab-case slug from the prompt \u2014 take the first 6-8 meaningful words, strip non-alphanumeric characters, lowercase, and truncate to 60 characters. The slug **must start with a letter** so it is a valid decision-page `ticket_key` in Stage 5 (`/^[A-Za-z][A-Za-z0-9_-]*$/`); if it would start with a digit or hyphen, prefix it with `exploration-`. If `{docs_dir}/explorations/{slug}.md` already exists, append a short timestamp suffix (e.g., `-1710000000`) \u2014 and fold that suffix **into the `slug` variable itself**, not just the filename, so that Stage 5 (`ticket_key`, `output_filename`) and Stage 6 (the `{docs_dir}/explorations/{slug}.md` rewrite) all reference the same slug. The output file path is `{docs_dir}/explorations/{slug}.md`.\n\n4. **Initialize tracking**: Prepare to track `key_files_examined` (list of files read during exploration), `web_searches` (list of topics searched), and `research_queries` (list of deep research queries).\n\nIf this stage fails, stop immediately and report the error. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Codebase Exploration\n\nThis is the core discovery stage. Take your time \u2014 thorough exploration is more valuable than speed.\n\n1. **Analyze the prompt** to identify which areas of the codebase are relevant: route files, agent flows, database models, library utilities, LLM integration, MCP server, tests, etc.\n\n2. **Search for files** matching patterns related to the task (e.g., `api/routes/**/*.py`, `src/python/llms/agents/**/*.py`, `db/models/*.py`).\n\n3. **Search for content** \u2014 relevant function names, class names, patterns, and keywords across the codebase.\n\n4. **Read the most relevant files** in detail \u2014 understand existing implementations, conventions, and patterns that relate to the task.\n\n5. **Build a mental model** of:\n - What exists today that relates to the task\n - What patterns and conventions are used in similar features\n - What dependencies, data flows, and integration points are involved\n - What gaps or unknowns remain that need external research\n\n6. **Track all significant files** examined in `key_files_examined`.\n\nDo not rush this stage. When in doubt, read more code rather than less. Continue exploring until you have a solid understanding of the relevant code.\n\nThis stage is non-blocking \u2014 always proceed to Stage 2 regardless of what you find, since the exploration informs what research is needed.\n\n## Stage 2 \u2014 Research Unknowns\n\nBased on gaps identified in Stage 1, decide what research is needed. Apply these decision rules:\n\n- **No research needed**: The codebase exploration answered all questions. Skip directly to Stage 3.\n- **Web search**: For quick factual lookups \u2014 library API signatures, configuration syntax, small "how to" questions. Examples: "FastAPI dependency injection with custom headers", "Alembic batch migration syntax". Do web searches inline and capture relevant findings.\n- **Deep research** (via `request_deep_research` MCP tool): For large, multi-faceted unknowns that require synthesizing information from multiple sources. Examples: "Best practices for implementing WebSocket connection pooling in Python asyncio", "Tradeoffs between different approaches to real-time notification delivery in FastAPI applications". Only use deep research when the question genuinely needs a multi-source investigation.\n\n**If deep research is needed:**\n\n1. Call `request_deep_research` with `wait_for_result` set to `true`, `save_locally` set to `true`, a descriptive `query`, and `context` describing the Bridge API tech stack and the specific task.\n2. If deep research fails, note the failure and fall back to web searches for the same topic. Do NOT halt the pipeline.\n\nTrack all research performed in `research_queries` and `web_searches`.\n\nThis stage is non-blocking \u2014 failures degrade the quality of analysis but do not stop the command. Log a warning for any failed research and continue.\n\n## Stage 3 \u2014 Analysis and Recommendation\n\nSynthesize everything from Stages 1 and 2 into a structured analysis:\n\n1. **Frame the goals and non-functional requirements first (required).** Before weighing implementation options, state plainly:\n - **Business goal** \u2014 the value this work delivers and why it matters.\n - **Desired end-state** \u2014 the concrete state the system should reach once this work is done.\n - **System behavior** \u2014 how the system must behave to complete its task (the quality attributes in prose).\n\n Then identify the non-functional requirements. Consider every one of these canonical NFR categories and include the ones that genuinely apply (omit the rest): security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility. For each NFR you include, write its `requirement` and its `implication` (what it changes about the implementation) \u2014 an NFR with no concrete implication is boilerplate; drop it. Classify each NFR\'s status with this rubric: `confirmed` only if explicitly stated or observable in code; `assumed` only if a low-risk, reversible default; `open` if it touches architecture, the data model, security, user-visible behavior, migration, or irreversible creation and is not settled. When the goals or an NFR are genuinely unclear, prefer marking them `open` and asking \u2014 clear goals make the functional choices far more accurate.\n\n2. **Identify viable implementation options** \u2014 at least 2 when multiple approaches exist, or 1 if there is genuinely only one reasonable path.\n\n3. **For each option, evaluate:**\n - Implementation complexity and estimated effort\n - How well it follows existing codebase patterns and conventions\n - Risks, tradeoffs, and potential pitfalls\n - Files that would need to be created or modified\n\n4. **Decide whether to recommend or ask questions:**\n - **Recommend** if one option is clearly superior, or if the tradeoffs are well-understood and the choice is primarily technical.\n - **Ask clarifying questions** if there are significant unknowns about goals, business requirements, or constraints that would change the recommendation. For each question, explain why the answer matters and how it would affect the choice between options.\n - **When in doubt, ask rather than guess** \u2014 this command prioritizes thorough discovery over premature commitment.\n\n5. **Frame the open decisions so they are decision-page-ready.** Stage 5 renders these as cards on an interactive decision page, so each decision \u2014 the primary implementation-direction choice, any `open` NFR from step 1, plus any clarifying question that has discrete candidate answers \u2014 must be expressed with:\n - A short decision **question** (e.g. "Which storage approach for the cache?").\n - **2\u20134 concrete option labels.** Do **not** include a "None of these" or "Ask about this" option \u2014 the page auto-appends both. When there is genuinely a single reasonable path, still provide a second option: frame it as the recommended approach **plus the strongest alternative you considered** (a minimal/conservative variant, the rejected approach, or "defer until X is known").\n - A one-line **consequence per option**, parallel to the options (what choosing that branch actually means for the implementation).\n - A `why_it_matters` line (the concrete impact of the decision) and a `recommendation_explanation` (why the recommended branch is best).\n - The 0-based index of the recommended option.\n - Optional supporting evidence: an Assessment paragraph plus `file:line` citations from Stage 1.\n\n Genuinely open-ended clarifying questions with no discrete answers do not need to become cards \u2014 capture them in the doc\'s Recommendation section as written. Aim to surface the real choices as cards; do not invent decisions just to fill the page.\n\nThis stage is inline analysis \u2014 no tool calls required. This stage is non-blocking \u2014 always proceed to Stage 4.\n\n## Stage 4 \u2014 Write Output\n\n1. Create the `explorations/` directory under `docs_dir` if it does not exist.\n\n2. Write the exploration document to the slug-based path determined in Stage 0 (`{docs_dir}/explorations/{slug}.md`) with this structure:\n\n```markdown\n# Exploration: {concise summary of the prompt}\n\n**Date**: {current date}\n**Prompt**: {original prompt text}\n\n## Context\n\n{Brief description of the task and what areas of the codebase are relevant.}\n\n## Goals & NFRs\n\n{The business goal, desired end-state, and required system behavior from Stage 3. Then the non-functional requirements: each with its category, requirement, implication, and status (confirmed / assumed / open). Open NFRs should also appear as decision cards on the Stage 5 page.}\n\n## Codebase Findings\n\n{Key discoveries from Stage 1. What exists today, what patterns are used, what the relevant code paths look like. Reference specific files and functions with file_path:line_number format.}\n\n## Research Findings\n\n{Findings from web searches and deep research, if any. If no research was performed, state "No external research was needed."}\n\n## Implementation Options\n\n### Option A: {name}\n\n{Description, approach, affected files, pros, cons.}\n\n### Option B: {name}\n\n{Description, approach, affected files, pros, cons.}\n\n## Recommendation\n\n{If recommending: State which option and why. Mention any caveats or risks.}\n\n{If asking questions: State "The following questions need to be answered before a confident recommendation can be made:" followed by numbered questions. For each question, explain why it matters and how the answer would affect the recommendation.}\n\n## Key Files\n\n{Bulleted list of the most important files examined, with one-line descriptions of their relevance.}\n```\n\nIf the file cannot be written, stop immediately and report the failure.\n\n## Stage 5 \u2014 Generate Decision Page\n\nTurn the decisions framed in Stage 3 into an interactive HTML decision page so the user can record their choices by clicking, instead of hand-editing the markdown doc.\n\n1. **Map each Stage 3 decision to an actionable item.** Build an `actionable_items` array where each entry has:\n - `id`: a short stable id, e.g. `D-1`, `D-2`.\n - `question`: the decision question.\n - `options`: the 2\u20134 option labels (string array). Do **not** include "None of these" or "Ask about this" \u2014 the renderer auto-appends both.\n - `option_consequences`: the per-option consequence lines, **parallel to and the same length as** `options`.\n - `why_it_matters`: the concrete impact line.\n - `recommendation_explanation`: why the recommended branch is best.\n - `recommendation_index`: the 0-based index of the recommended option (must be within `options`).\n - `codebase_evidence` (optional): the Assessment paragraph plus `file:line` citations, shown collapsed.\n - `original_question` (optional): include only when the item maps to a verbatim clarifying question.\n\n Optionally include `clear_improvements` for low-risk findings you are confident about that do not need a choice (each with `id`, `title`, `action`, `confidence`, `source`) \u2014 these render as an informational list and are not submitted.\n\n2. **Call `generate_decision_page`** with routing fields at the root and all heavy arrays nested under `content`:\n - `artifact_type`: `pre_ticket_planning` (renders the read-only System Goals & NFRs panel above the decision cards).\n - `ticket_key`: the Stage 0 `slug` (a non-Jira slug is fine \u2014 it must start with a letter and contain only letters, digits, hyphens, or underscores).\n - `output_subdir`: `explorations` (so the page lands beside the markdown doc).\n - `output_filename`: `{slug}-decisions.html`.\n - `labels`: exploration-flavored overrides, e.g. `title` = "Exploration Decisions", `section_heading` = "Implementation Decisions", and an `intro` that frames the page as choosing the direction for the explored task.\n - `content`: an object containing `system_goals`, `actionable_items`, and optionally `clear_improvements` from step 1. **`system_goals` MUST ALWAYS be passed** inside `content` so the backend always writes a page. Never omit it, even if all NFRs are confirmed. Open NFRs must ALSO appear in `actionable_items`. (Do not pass `implementation_order` inside `content` \u2014 that is for epic surfaces, not a single explored task.)\n\n ```typescript\n interface ExploreTicketContent {\n system_goals?: {\n business_goal: string;\n desired_end_state: string;\n system_behavior: string;\n nfrs?: Array<{\n category: string;\n requirement: string;\n implication: string;\n status: "confirmed" | "assumed" | "open";\n }>;\n };\n actionable_items?: Array<{\n id: string; // e.g. "D-1", "D-2"\n question: string;\n why_it_matters: string;\n recommendation_explanation: string;\n options: string[]; // 2-4 option labels (no "None of these" or "Ask about this")\n option_consequences: string[]; // same length as options\n recommendation_index: number; // 0-based within options\n codebase_evidence?: string; // optional: assessment + file:line citations\n original_question?: string; // optional: only when item maps to a clarifying question\n }>;\n clear_improvements?: Array<{\n id: string;\n title: string;\n action: string;\n confidence: string;\n source: string;\n }>;\n // implementation_order: for epic surfaces only \u2014 do NOT include for single task explorations\n // depends_on: hard prerequisites (titles/keys that must land first)\n // recommended_after: soft sequencing preferences, not hard blockers\n }\n ```\n\n Example call:\n ```json\n {\n "ticket_key": "my-task-slug",\n "artifact_type": "pre_ticket_planning",\n "output_subdir": "explorations",\n "output_filename": "my-task-slug-decisions.html",\n "labels": { "title": "Exploration Decisions", "section_heading": "Implementation Decisions" },\n "content": {\n "system_goals": {\n "business_goal": "Improve token efficiency for MCP sessions.",\n "desired_end_state": "Core profile under 15k tokens.",\n "system_behavior": "Schema delivered on demand, not in every session.",\n "nfrs": [\n { "category": "security/privacy", "requirement": "Errors in JSON envelope only", "implication": "Never render validation errors into HTML", "status": "confirmed" }\n ]\n },\n "actionable_items": [\n {\n "id": "D-1",\n "question": "Which approach?",\n "why_it_matters": "Determines whether schema stays lean in production.",\n "recommendation_explanation": "Option A saves ~1.8k tokens per session.",\n "options": ["Lean schema + in-handler validation", "Keep full schema"],\n "option_consequences": ["~1.8k token saving per session.", "No change from today."],\n "recommendation_index": 0\n }\n ]\n }\n }\n ```\n\n3. **Handle the response `status`:**\n - `no_decisions_needed`: no page was written (no open decisions, no `system_goals`, and no `implementation_order`). This should not occur when `system_goals` is always passed. Skip Stage 6\'s capture loop entirely, tell the user there were no open decisions, and go straight to Stage 6\'s "Suggest next steps" guidance.\n - `decision_page_generated`: surface the returned `file_path` and proceed to Stage 6\'s capture loop. **Always proceed to Stage 6\'s capture loop when `decision_page_generated` is returned**, regardless of `actionable_items_count`. A goals-only page (zero actionable items) still has NFR stance controls that must be submitted.\n\nThis stage is non-blocking: if `generate_decision_page` fails, do not halt. **You MUST output a highly visible warning** (e.g. **\u26A0 WARNING: The decision page could not be generated** in bold) explaining that generation failed and that the user should work from the markdown doc written in Stage 4 instead. Do not silently continue \u2014 the failure must be diagnosable from your output. Then skip to Stage 6\'s "Suggest next steps" guidance.\n\n## Stage 6 \u2014 Capture Decisions and Finalize\n\nCapture the user\'s choices, fold them into the exploration doc as resolved decisions, and recommend what to do next.\n\n1. **Direct the user to the page.** Provide the `file_path` from Stage 5 and tell them to open it in their browser. Explain that for any item they are unsure about they can choose "Ask about this" and you will talk it through, and that they can also ask questions in chat before submitting.\n\n2. **Q&A loop and commit signal.** Engage with each user message as either a commit or a discussion turn:\n - **Commit:** trim the full message and attempt to parse the entire trimmed message as JSON. Treat it as a commit only when the parsed value is an object with all three top-level fields: `ticket_key` (string), `decisions` (object), and `general_comment` (string). The first valid commit-shaped paste commits \u2014 do not over-validate the per-card fields.\n - **Discussion:** anything that is not commit-shaped JSON. Answer from the exploration doc written in Stage 4 and from codebase lookups. If a JSON-shaped paste is missing one of the three required fields, say which field is missing rather than treating it as a freeform question.\n - **In-flight overrides:** when the user clearly changes a choice in chat ("go with option B for D-2", "change D-3 to None of these") or gives new overarching guidance, record it as a working-memory override. On commit, the submitted JSON is the baseline and recorded overrides take precedence; post a one-line acknowledgement naming each overridden item before you rewrite the doc.\n\n3. **Resolve "Ask about this" items (hard rule).** After accepting a commit, scan `decisions` for any item where `choice === "ask"`. For each, present the relevant evidence and trade-offs and continue the discussion until the user gives an explicit decision, which you record as an override. Do not rewrite the doc while any `choice === "ask"` remains unresolved \u2014 do not honor "just skip those".\n\n4. **Finalize the exploration doc.** Rewrite `{docs_dir}/explorations/{slug}.md` so it reads as a final draft with the decisions already made \u2014 not a mechanical append:\n - Mark the chosen Implementation Option as the selected direction in the Recommendation section and integrate it so the doc reads as a resolved plan.\n - Fold answered clarifying questions into the Context / Recommendation sections.\n - For a "None of these" choice, record that the proposed options were rejected, including the user\'s comment.\n - Weave `general_comment` in as overarching guidance; do not add a separate "Reviewer Notes" section.\n - Preserve all unaffected sections unchanged.\n\n5. **Suggest next steps (conditional).** Assess what the explored work still needs to be fully groomed, and recommend only the follow-ups that genuinely apply \u2014 as pointers for the user to run, not actions you take automatically:\n - A **brainstorm** (`/brainstorm` or `request_brainstorm`) when the direction would benefit from a thorough, wide review before committing.\n - A **second opinion** (`second_opinion`) when a few specific contested points need an independent check.\n - **Web or deep research** (`request_deep_research`) when the chosen direction still rests on technical unknowns that need grounding.\n - **Uploading a ticket** (`/write-ticket` or `create_ticket`) when the requirements are clear and certain. If the explored work is well-grounded and the decisions leave it in a good state, advise uploading directly.\n\nThis stage is non-blocking: if the user never commits, leave the doc as written in Stage 4 and stop without forcing a decision.\n\n## Final Report\n\nOn successful completion of all stages, display:\n\n> **Exploration Complete**\n>\n> **Prompt**: {first 80 characters of prompt}...\n> **Output**: {full path to the finalized exploration doc}\n> **Decision Page**: {full path to the generated decisions.html, or "not generated" when no decisions were needed or generation failed}\n> **Files Examined**: {count of key_files_examined}\n> **Research**: {count of web_searches} web searches, {count of research_queries} deep research queries\n>\n> **Result**: {"Recommendation provided" | "Clarifying questions raised \u2014 N questions need answers"}\n> **Decisions Captured**: {count of decisions the user committed, or "none \u2014 page not submitted / no decisions needed"}\n> **Suggested Next Step**: {the conditional next step advised in Stage 6, e.g. "upload a ticket", "run a brainstorm", or "none"}\n\nOn failure at any stage, stop immediately and report:\n- Which stage failed (by number and name)\n- The error details\n- Any partial results that were produced before the failure\n',
14636
14829
  "full-automation.md": '---\nschedulable: true\narguments: {"positionals":[],"flags":[{"name":"ideaFile","flag":"--idea-file","type":"string","required":true},{"name":"auto","flag":"--auto","type":"boolean"}]}\n---\n\nRun the end-to-end full-automation chain (idea-to-ticket \u2192 review-ticket \u2192 start-tickets) via the server-side chain orchestrator.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command drives Phase A\'s server-side full-automation chain. The only orchestration tools you may drive are `run_full_automation` and `resume_full_automation`; any other Bridge API MCP call you make must be one a server `agent_task` instruction explicitly directs. The server owns all orchestration \u2014 ticket creation, review fan-out, and the start-tickets handoff. Do NOT enrich, re-implement, or second-guess any of that work on the client side.\n\n## Stage 0 \u2014 Parse arguments\n\n1. Tokenize `$ARGUMENTS` on whitespace. Recognize the following position-independent flags. Each flag supports both the space form (`--flag value`) and the equals form (`--flag=value`) where a value is taken:\n - `--idea <text>` / `--idea=<text>`\n - `--idea-file <path>` / `--idea-file=<path>`\n - `--auto`\n - `--require-approval`\n - `--scheduled-at <ISO-8601>` / `--scheduled-at=<ISO-8601>`\n - `--chain-run-id <UUID>` / `--chain-run-id=<UUID>`\n - `--max-children N` / `--max-children=N`\n - `--allow-duplicate`\n\n2. Value-consumption rules:\n - `--idea` (space form) consumes every subsequent token until the next recognized flag \u2014 the idea may contain spaces.\n - `--idea-file`, `--scheduled-at`, `--chain-run-id`, and `--max-children` each consume exactly one value token (the immediately following token, or the text after `=`).\n - `--auto`, `--require-approval`, and `--allow-duplicate` are boolean toggles and consume no value.\n\n3. Free-form idea: all non-flag tokens become the free-form `idea` text **only when both `--idea` and `--idea-file` are absent**. Join those tokens back together preserving order and trim surrounding whitespace. When `--idea` or `--idea-file` is present, there must be no leftover non-flag tokens: reject any stray non-flag token (for example, text following `--idea=<text>` or following the `--idea-file <path>` value) before any MCP tool call rather than silently dropping it.\n\n4. Reject **unknown flags** (any token beginning with `--` that is not one of the recognized flags above) before making any MCP tool call. Stop and report the offending flag.\n\n5. Reject **combined `--idea` and `--idea-file`** before making any MCP tool call:\n ```text\n Provide exactly one of --idea or --idea-file; do not pass both.\n ```\n\n6. Missing-input rule: unless `--chain-run-id` is present, an idea is required. If `--chain-run-id` is absent **and** no idea was supplied (no `--idea`, no `--idea-file`, and no free-form idea tokens), stop immediately and display exactly:\n ```text\n Usage: /full-automation (--idea "<text>" | --idea-file <path> | <free-form idea>) [--require-approval] [--scheduled-at <ISO-8601>] [--chain-run-id <UUID>] [--max-children N] [--allow-duplicate]\n ```\n\n7. `--chain-run-id` is the resume path and does **not** require any idea content \u2014 when it is present, skip the missing-input check above and proceed to resume.\n\n8. `--idea-file` is forwarded as a path. The skill must **not** read the file contents locally; the server resolves the file.\n\n9. Resolve the derived values:\n - `auto_approve` defaults to `true` (full automation is hands-off by default). It is `false` **only** when `--require-approval` is present. `--auto` is accepted but redundant (a no-op that restates the default), and `--scheduled-at` likewise runs hands-off. When `--require-approval` is present, the chain pauses at external-mutation and review-decision gates for confirmation.\n - `max_children` is the parsed positive integer when `--max-children` is present; otherwise omit it entirely so the server default applies.\n - `allow_duplicate` is `true` only when `--allow-duplicate` is present; otherwise omit it.\n\n## Stage 1 \u2014 Drift-check gate\n\nThis gate runs immediately after parsing and **before any MCP tool call**.\n\n1. If `--scheduled-at` is absent, skip this entire stage.\n2. Compute `delta_seconds = now_utc - scheduled_at` (both in UTC).\n3. If `delta_seconds <= 60`, proceed silently to Stage 2.\n4. If `delta_seconds > 60`, present this prompt verbatim (substituting the bracketed values):\n ```text\n Scheduled at <T-iso> UTC; running now at <now-iso> UTC (<\u0394 human-readable> late). The laptop was likely asleep or unavailable at the scheduled time. Confirm to proceed with the chain, or cancel.\n ```\n Offer the user the choices: `[Confirm] / [Cancel]`.\n5. On `Confirm`, proceed to Stage 2.\n6. On `Cancel`, print this message verbatim and stop:\n ```text\n Chain cancelled by user (drift confirmation declined). No Jira tickets created.\n ```\n When the user cancels, `run_full_automation` must **not** be called.\n7. The 60-second threshold is fixed and must not be made configurable.\n\n## Stage 2 \u2014 Run or resume the chain\n\nThe chain is driven entirely by the server-side orchestrator. Announce progress using each envelope\'s `preamble`, preserving its `Stage N of M \u2014 <title>` shape.\n\n### Stage 2a \u2014 Start (when `--chain-run-id` is absent)\n\nCall **only** `run_full_automation`. Build the payload, **omitting** any optional value that was not provided (never send `null` or empty strings):\n```json\n{\n "idea": "<resolved inline/free-form idea, when provided>",\n "idea_file": "<idea-file path, when provided>",\n "auto_approve": "<resolved boolean>",\n "scheduled_at": "<scheduled-at value, when provided>",\n "max_children": "<parsed integer, when provided>",\n "allow_duplicate": "<true, when provided>"\n}\n```\n\n### Stage 2b \u2014 Resume (when `--chain-run-id` is present)\n\nCall **only** `resume_full_automation` first, with:\n```json\n{\n "chain_run_id": "<UUID>",\n "agent_result": "Manual resume requested from /full-automation --chain-run-id."\n}\n```\n\n### Stage 2c \u2014 Envelope loop\n\nFor each envelope returned by `run_full_automation` / `resume_full_automation`, dispatch on `status` / `next_action.kind`:\n\n- `status: "failed"` \u2192 stop chain progression and render the final report (Stage 3) with the failure status. Do **not** advance to any later stage.\n- `status: "completed"` or `next_action.kind: "complete"` \u2192 render the final report (Stage 3).\n- `status: "needs_agent_task"` with `next_action.kind: "agent_task"` \u2192 display the envelope `preamble`, perform the agent task exactly as the `next_action.instruction` directs, then call `resume_full_automation` with `chain_run_id` set to the envelope\'s `chain_run_id` and `agent_result` set to the resulting text. Loop back and process the next envelope.\n\nSpecial case \u2014 the stage-3 handoff: when the agent-task instruction names a `/start-tickets ...` command, invoke that slash command in **this same session**, summarize the outcome in one line, and pass that one-line summary as `agent_result` to `resume_full_automation`.\n\nConstraints:\n- On your own initiative, the skill must **not** call any Bridge API MCP tool other than `run_full_automation` / `resume_full_automation` \u2014 in particular, never independently drive orchestration (`run_pipeline`, `resume_pipeline`, `get_pipeline_recipe`) or enrich tickets (`get_ticket`, `update_ticket_description`, etc.). **However, when a `needs_agent_task` instruction returned by the server explicitly directs you to call a specific Bridge API MCP tool** (for example an orchestrator-directed `get_tickets`, `create_ticket`, `attachment`, or `track_ticket`), **you must invoke that tool exactly as instructed** \u2014 performing an orchestrator-directed agent task is not re-orchestrating.\n- If a v1 envelope unexpectedly returns `next_action.kind: "mcp_call"`, stop with a clear protocol error instead of bypassing the server-side orchestrator:\n ```text\n Protocol error: chain returned next_action.kind "mcp_call", which is out of scope for /full-automation v1. Stopping.\n ```\n\n## Stage 3 \u2014 Final report\n\nWhen the chain completes or fails, render this skeleton verbatim:\n\n```markdown\n## Full Automation Complete\n\nChain run: <chain_run_id>\nIdea: <first 80 chars of idea>...\nStages:\n 1. idea-to-ticket: <stages[0].summary>\n 2. review-ticket: <stages[1].summary>\n 3. start-tickets: <stages[2].summary>\n\nTotal Jira tickets created: N\nTotal worktrees spawned: M\nStatus: Success / Failed at stage N \u2014 <reason>\n```\n\n- Stage summaries come from the chain envelope or manifest when present.\n- When the completed envelope does not include full stage objects, use the summaries already surfaced in the prior `preamble` text rather than calling additional tools.\n- A stage-1 `too_vague_to_ticket` failure must render the upstream halt reason and set `Status: Failed at stage 1 \u2014 <reason>`.\n- Failed chains must not advance to later stages after a failed envelope is received.\n',
14637
- "idea-to-ticket.md": 'Convert a short human idea into a Jira ticket (or Epic plus child tickets) via the server-side idea-to-ticket pipeline.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly other than `get_pipeline_recipe` \u2014 the recipe determines which tools to call and with what parameters.\n\n## Stage 0 \u2014 Parse arguments\n\n1. Tokenize `$ARGUMENTS` on whitespace. Recognize the following position-independent flags (any other tokens are part of the idea):\n - `--allow-duplicate`\n - `--max-children=N` where `N` is a positive integer\n - `--auto`\n\n Flags are optional. Treat absence of a flag as the default; never pass empty-string or null as a placeholder.\n\n2. Everything that is not a recognized flag is the free-form `idea` text. Join those tokens back together preserving order. Trim surrounding whitespace.\n\n3. If the resulting `idea` is empty, stop immediately and display:\n ```\n Usage: /idea-to-ticket <idea> [--allow-duplicate] [--max-children=N] [--auto]\n ```\n\n## Stage 1 \u2014 Derive pipeline variables\n\n4. Derive `slug` from the first 6-8 meaningful words of the idea: lowercase, kebab-case, strip non-alphanumeric characters except hyphens, and truncate to roughly 60 characters. Skip stop-words such as "the", "a", "an" when picking the 6-8 meaningful words.\n\n5. Derive `run_id` as `<YYYYMMDD-HHMMSS>-<short-uuid>` using the current UTC time and a short UUID suffix (8 hex chars is enough). The combination of `slug` and `run_id` uniquely identifies this run\'s artifact directory.\n\n6. Derive the boolean-as-string variables:\n - `allow_duplicate` is `"true"` if `--allow-duplicate` was present, otherwise `"false"`.\n - `auto_approve_external` is `"true"` if `--auto` was present, otherwise `"false"`.\n - `max_children` is the integer following `--max-children=` as a string, or `"10"` when the flag is absent.\n\n## Stage 2 \u2014 Call the recipe\n\n7. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"idea-to-ticket"`\n - `variables`: `{ "idea": "<idea>", "slug": "<slug>", "run_id": "<run_id>", "allow_duplicate": "<allow_duplicate>", "auto_approve_external": "<auto_approve_external>", "max_children": "<max_children>" }`\n\n Do NOT pass `docs_dir` or `idea_hash` in variables \u2014 both are auto-injected by the pipeline system (`docs_dir` from `BAPI_DOCS_DIR`; `idea_hash` is a stable hash derived from the `idea`).\n\n If the tool returns an error, stop and report the failure.\n\n8. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n## Stage 3 \u2014 Final summary\n\n9. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Idea**: <first 80 characters of idea>...\n **Slug**: <slug>\n **Run directory**: <docs_dir>/idea-to-ticket/<slug>-<run_id>/\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',
14830
+ "idea-to-ticket.md": 'Convert a short human idea into a Jira ticket (or Epic plus child tickets) via the server-side idea-to-ticket pipeline.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly other than `get_pipeline_recipe` \u2014 the recipe determines which tools to call and with what parameters.\n\n## Stage 0 \u2014 Parse arguments\n\n1. Tokenize `$ARGUMENTS` on whitespace. Recognize the following position-independent flags (any other tokens are part of the idea):\n - `--allow-duplicate`\n - `--max-children=N` where `N` is a positive integer\n - `--auto`\n\n Flags are optional. Treat absence of a flag as the default; never pass empty-string or null as a placeholder.\n\n2. Everything that is not a recognized flag is the free-form `idea` text. Join those tokens back together preserving order. Trim surrounding whitespace.\n\n3. If the resulting `idea` is empty, stop immediately and display:\n ```\n Usage: /idea-to-ticket <idea> [--allow-duplicate] [--max-children=N] [--auto]\n ```\n\n## Stage 1 \u2014 Derive pipeline variables\n\n4. Derive `slug` from the first 6-8 meaningful words of the idea: lowercase, kebab-case, strip non-alphanumeric characters except hyphens, and truncate to roughly 60 characters. Skip stop-words such as "the", "a", "an" when picking the 6-8 meaningful words.\n\n5. Derive `run_id` as `<YYYYMMDD-HHMMSS>-<short-uuid>` using the current UTC time and a short UUID suffix (8 hex chars is enough). The combination of `slug` and `run_id` uniquely identifies this run\'s artifact directory.\n\n6. Derive the boolean-as-string variables:\n - `allow_duplicate` is `"true"` if `--allow-duplicate` was present, otherwise `"false"`.\n - `auto_approve_external` is `"true"` if `--auto` was present, otherwise `"false"`.\n - `max_children` is the integer following `--max-children=` as a string, or `"10"` when the flag is absent.\n\n## Stage 2 \u2014 Call the recipe\n\n7. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"idea-to-ticket"`\n - `variables`: `{ "idea": "<idea>", "slug": "<slug>", "run_id": "<run_id>", "allow_duplicate": "<allow_duplicate>", "auto_approve_external": "<auto_approve_external>", "max_children": "<max_children>" }`\n\n Do NOT pass `docs_dir` or `idea_hash` in variables \u2014 both are auto-injected by the pipeline system (`docs_dir` from `BAPI_DOCS_DIR`; `idea_hash` is a stable hash derived from the `idea`).\n\n If the tool returns an error, stop and report the failure.\n\n8. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n The recipe drives the ordered stages for you \u2014 do not invoke them directly. In order they are: preflight-and-readiness \u2192 research-decision \u2192 execute-research \u2192 duplicate-and-context-scan \u2192 screen-and-resolve \u2192 frame-goals-and-nfrs \u2192 **comp-analysis** (a gated, backend-safe perception step that maps any attached/referenced design comp to existing components, templates, SCSS/CSS tokens, and routes before drafting; it short-circuits for backend-only or no-comp work) \u2192 draft-and-critique \u2192 upload-and-track.\n\n## Stage 3 \u2014 Final summary\n\n9. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Idea**: <first 80 characters of idea>...\n **Slug**: <slug>\n **Run directory**: <docs_dir>/idea-to-ticket/<slug>-<run_id>/\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',
14638
14831
  "implement-ticket.md": '# Implement Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n## CONDUCTOR_MESSAGE_RELAY ##\n\nIf you were launched under the Conductor (your environment carries a conductor run/worker identity), cooperatively poll for supervisor guidance while you work: at natural checkpoints \u2014 after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response \u2014 call the `check_messages` MCP tool. The tool reads and acknowledges any messages addressed to you, and acknowledged messages are not redelivered, so a later call returns only new guidance. This is cooperative polling only \u2014 it is not prompt injection and never mutates a live session. If the tool or conductor identity is unavailable, continue the task without derailing.\n\n1. Parse `$ARGUMENTS` to extract:\n - A required `ticket_key` matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`).\n - An optional position-independent `--auto` flag.\n\n Tokenize `$ARGUMENTS` on whitespace. The first token matching the Jira key pattern is the `ticket_key`; ignore any additional ticket-key tokens. The presence of a `--auto` token (anywhere in `$ARGUMENTS`) sets `auto_approve` to `true`.\n\n If `$ARGUMENTS` is empty or contains no token matching the Jira key pattern, stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123 [--auto]\n Usage: /implement-ticket <ticket_key> [--auto]\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"implement-ticket"`\n - `variables`: `{ "ticket_key": "<ticket_key>" }`\n - `auto_approve`: `true` \u2014 only when `--auto` was passed; otherwise omit this field entirely.\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n\n---\n\n# Worker scope discipline and clean exit (Conductor auto mode)\n\nThese rules apply only when you were launched under the Conductor in auto mode (`--auto`); a normal interactive `/implement-ticket` run is unaffected.\n\n## Declared file-scope discipline (N-2)\n\nIf your environment carries `BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON` (a JSON array of the ticket\'s declared touched files), stay within that declared file boundary: do **not** create, modify, or delete files outside the declared set. Those out-of-scope files typically belong to a sibling ticket, and editing them risks a merge conflict or a silent clobber of the sibling\'s merged work. The pre-PR file-scope guard (run at the PR-creation step) will warn about any out-of-scope diff \u2014 treat that warning as a signal to re-check your scope, not as a blocker. When the variable is absent, empty, or invalid there is no declared boundary and this rule does not apply.\n\n## Clean session exit (D2)\n\nAfter the final pipeline step completes, cleanly end your worker session (for example by issuing the `/exit` command) so the worktree is released and no idle process lingers \u2014 but **only when no follow-up remains that you still own**. Do **not** exit while any of the following is true:\n\n- there are unresolved CI failures you are still correcting (the post-PR CI-correction loop in the CI-monitoring step still owns work),\n- review changes were requested and you have not yet addressed them,\n- there is a merge conflict on your PR that you still own,\n- you have unpushed local commits.\n\nExit only after your final branch state is pushed, the done-gate / CI-monitoring workflow required by the recipe has completed, and no CI/review follow-up remains. A clean `SessionEnd` is both the correct terminal lifecycle signal and the point at which the worker should exit.\n',
14639
14832
  "install-bridge.md": 'Bootstrap Bridge API project configuration from the local codebase via the easy-install manifest.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes no arguments; any `$ARGUMENTS` text is ignored.\n\nCommand contract version: **2**. The install manifest returns a `command_contract_version`; Stage 2\ncompares it to this number to detect a stale scaffolded copy of this command.\n\nThis command performs a one-time "easy install" of Bridge API project configuration. It reads the\nserver-owned install manifest once, derives values for the **unset** bootstrap fields from the local\ncodebase, asks for human approval of the project description, and applies everything in a single\natomic call. The server owns all skip-if-set, conflict, and confirmation semantics \u2014 this command\nnever makes its own skip-if-set decisions.\n\nThis command is portable across Claude Code, Cursor, and Copilot: it only drives the Bridge API MCP\ntools (`get_my_role`, `get_install_manifest`, `apply_install_manifest`).\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n## Stage 1 \u2014 Admin preflight\n\n1. Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `source` is `"legacy"`: proceed (legacy keys are permitted).\n - Else if `role` is `"admin"`: proceed.\n - Otherwise (a non-admin `user_access` key): stop immediately and display:\n ```\n Admin role required to apply install configuration. Ask a project admin to run /install-bridge,\n or use an admin API key.\n ```\n\n## Stage 2 \u2014 Read the manifest (once)\n\n1. Call the `get_install_manifest` MCP tool exactly once.\n2. Keep the returned `snapshot_token` verbatim \u2014 you must pass the SAME token to the apply call in\n Stage 5. Do not call `get_install_manifest` again before applying; a fresh read would invalidate\n the token you are about to use.\n3. The manifest contains ordered `groups` of bootstrap fields (each with `field_name`, `is_set`,\n `current_value`, `guidance`, `examples`, `validation_summary`, `requires_confirmation`), a list of\n `deferred_fields`, a `next_step`, `done_criteria`, a `command_contract_version`, and the\n `snapshot_token`.\n4. Compare the manifest\'s `command_contract_version` to this command\'s contract version (2, stated at\n the top of this file). If the manifest\'s version is HIGHER, this scaffolded command copy is stale:\n warn the user to refresh it by running `npx -y @bridge_gpt/mcp-server --init` in the project root,\n then proceed conservatively \u2014 wherever the manifest\'s `guidance`, `next_step`, or `done_criteria`\n differ from these instructions, follow the manifest.\n\n## Stage 3 \u2014 Derive values for UNSET bootstrap fields only\n\n1. Consider ONLY fields whose `is_set` is `false`. Never re-derive or overwrite a field that is\n already set \u2014 the server will also skip already-set fields, but you should not even propose them.\n2. For each unset field, derive a value from the local codebase following that field\'s `guidance`\n (e.g. infer `working_in` from frameworks/manifests, `version_control_system` from the git remote,\n testing stacks from test config and dependencies, directory fields from the repo layout).\n3. Omit any field you are uncertain about or that does not apply \u2014 leaving it unset is always\n acceptable. Do NOT guess.\n4. Specifically, do NOT invent a `version` when the platform has no meaningful version concept; leave\n it unset.\n5. Before deriving `version_control_system` or `base_branch`, confirm the git repository root is THIS\n project\'s root: `git rev-parse --show-toplevel` must resolve to the project directory itself. If\n the git root is an ancestor directory (the project is nested inside another repository), OMIT both\n fields and note why in the final report \u2014 deriving them would describe the parent repository, not\n this project.\n6. If you find real evidence for TWO OR MORE plausible values for a field (e.g. configs for multiple\n test frameworks, multiple frameworks in a monorepo), treat the value as ambiguous and omit it\n rather than picking one.\n7. For the "Automation policy" group (`selected_mcp_slugs`): propose MCP validation manuals only from\n clear platform markers, following the field\'s manifest guidance (e.g. SFCC cartridges \u2192\n `b2c-commerce-developer`; a Playwright config \u2192 `playwright-mcp`; PWA Kit markers \u2192\n `pwa-kit-mcp`). This field requires human confirmation (Stage 4). Omit it entirely when no manual\n clearly applies \u2014 never propose a slug on weak evidence.\n\n## Stage 4 \u2014 Human approval for confirmation-requiring fields\n\nSome manifest fields carry `requires_confirmation: true` (currently `project_description` and\n`selected_mcp_slugs`). These are never applied on derivation alone \u2014 each needs explicit human\napproval.\n\n1. If `project_description` is unset and you can draft a concise description from the codebase,\n present the proposed `project_description` text to the human and ask for explicit approval before\n including it.\n2. If you derived a `selected_mcp_slugs` list in Stage 3, present the proposed slugs and the platform\n evidence for each, and ask for approval in the SAME batched question round as the description.\n3. Include a confirmation-requiring field in the apply payload ONLY as\n `{ "value": <approved value>, "confirmed": true }`, and only after the human approves it. If the\n human does not approve a field, omit that field entirely.\n4. If no human response can be obtained (e.g. a non-interactive / headless session), do NOT stall and\n do NOT abandon the install: proceed to Stage 5 with every confirmation-requiring field omitted,\n and report them as "pending human input" in the final summary. The other derived fields must\n still be applied \u2014 unapproved fields never block them.\n\n## Stage 5 \u2014 Apply (one call)\n\n1. Make exactly ONE call to the `apply_install_manifest` MCP tool, passing:\n - `snapshot_token`: the exact token from Stage 2.\n - `fields`: a map of the derived fields. Scalar fields may be passed directly\n (e.g. `"base_branch": "main"`); confirmation-requiring fields (e.g. `project_description`,\n `selected_mcp_slugs`) must use the `{ "value": ..., "confirmed": true }` object form from\n Stage 4.\n2. Do not implement any client-side skip-if-set, conflict, or confirmation logic \u2014 the server owns\n all of that and returns the outcome buckets.\n3. The apply is partial-tolerant: fields that fail server-side validation are returned in the\n `rejected` bucket while the valid fields still commit. A rejected field is therefore reported,\n not fatal \u2014 do NOT retry the whole apply just because one field was rejected.\n\n## Stage 6 \u2014 Persist the routing credential\n\nThe Bash-spawned `start-tickets` CLI runs in a DIFFERENT runtime surface than this MCP server, so a\nkey that lives only in `.mcp.json` / `.cursor/mcp.json` is invisible to it and difficulty\u2192model\nrouting silently degrades. This final stage persists the already-validated key into the user-scoped\ncredential store (`~/.config/bridge/credentials.json`) so shell-spawned CLI commands can resolve it.\n\n1. Call the `persist_routing_credential` MCP tool with `repo_name` set to the repository name for\n this install (the configured `BAPI_REPO_NAME`). Pass `repo_name` ONLY \u2014 this tool resolves the\n API key inside the MCP server process. NEVER pass or display `BAPI_API_KEY` (nor `api_key`,\n `apiKey`, `secret`, or `token`); the key value must not appear in the tool call or in any output.\n2. On success, tell the user the credential was stored, echoing the returned `target` and `path`,\n e.g. `Stored routing credential for bapi:<repo_name> at <path>`.\n3. On failure, do NOT block the install \u2014 show the rest of the install summary anyway, then tell the\n user the routing credential could not be stored. They can rerun `/install-bridge` after setting\n `BAPI_API_KEY` in the environment, or run\n `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate an\n existing key. Until then, `start-tickets` model routing fails open (assumes a hard ticket and\n defaults to premium/Opus when available) \u2014 this is the MOST EXPENSIVE model tier, so unexplained\n cost is the symptom of leaving this unfixed. Recommend verifying credential resolution afterwards\n with `npx -y @bridge_gpt/mcp-server doctor`.\n\n## Stage 7 \u2014 Summarize the outcome\n\nBegin the summary with an explicit applied count: "Applied N of M derivable fields" (M = the unset\nbootstrap fields you derived values for). An install that applied 0 fields must say so loudly \u2014 the\ninstall is NOT complete until the apply call reports applied fields.\n\nThen summarize the six buckets from the apply response for the user:\n- `applied` \u2014 fields written this run.\n- `skipped` \u2014 fields already set (left untouched).\n- `conflict` \u2014 fields that changed since the manifest was read (re-run /install-bridge to retry).\n- `needs_confirmation` \u2014 fields awaiting explicit confirmation (e.g. project_description).\n- `rejected` \u2014 fields that failed validation or were not bootstrap-eligible (the other fields still\n applied; fix or drop the rejected values before any retry).\n- `deferred` \u2014 fields intentionally not auto-written by install (owned by /learn-repository or set\n deliberately).\n\nAfter the buckets, report the **integrations checklist** from the manifest\'s `integrations` list\n(read in Stage 2): one line per integration showing `label` and configured / NOT configured, and for\neach unconfigured one, its `required_for` items and the `configure_in` pointer. STRICT INVARIANT:\nyou DIRECT the human to configure integrations \u2014 you never ask for, accept, echo, or transport an\nintegration credential (API token, access token, webhook secret) in any form; a human enters them in\nthe setup UI. If the manifest had no `integrations` key, say the checklist was unavailable this run.\n\n## Stage 8 \u2014 Offer the next steps\n\n1. Offer to continue with `/learn-repository` to populate the deeper instruction-tier configuration\n (architecture, review, testing, and correctness standards), matching the manifest\'s `next_step`.\n2. After `/learn-repository` completes (or if the human declines it), offer to start **repository\n indexing**: ask ONE consent question ("Start repository indexing now? It runs as a background job\n and makes the codebase searchable for Bridge API\'s agents."). On yes, call the `parse_repository`\n MCP tool once. Report that the job was QUEUED and that progress can be checked with\n `get_parse_status` (or `/check-parse-status`) \u2014 do NOT poll it to completion in this session.\n Indexing is deliberately last so the applied `exclude_directories` / `custom_directories` values\n scope it correctly. If no human response can be obtained (non-interactive session), skip indexing\n and list it as a pending next step in the final report \u2014 never start it without consent.\n\n## Stage 9 \u2014 Offer CI follow-up configuration (only when CI is detected)\n\nThis stage is separate from the single apply call: it writes at most ONE field via the\n`config_field` MCP tool. `ci_followup_config` is REPLACE-NOT-AUGMENT \u2014 leaving it NULL already means\nsafe poll-only defaults, so "skip" is always a correct answer.\n\n1. Gate: only offer this when Stage 3 found real CI evidence (e.g. `.github/workflows/*.yml`,\n `bitbucket-pipelines.yml`). With no CI detected, skip this stage and note "no CI detected \u2014 CI\n follow-up not offered" in the final report.\n2. Ask ONE question, referencing the discovered CI by name, with exactly these preset profiles\n (default: skip):\n - **observe** \u2014 poll CI results only, never attempt fixes:\n `{"strategy": "poll_only", "max_iterations": 1, "max_minutes": 10, "instructions": ""}`\n - **self-heal** \u2014 bounded fix-and-iterate loop on the automation\'s own PRs:\n `{"strategy": "fix_and_iterate", "max_iterations": 3, "max_minutes": 45, "instructions": ""}`\n - **skip** (default) \u2014 leave `ci_followup_config` unset (NULL = baseline poll-only behavior).\n3. Do NOT offer or compose a `custom` strategy during install \u2014 free-form CI follow-up instructions\n are a deliberate later act (setup UI or a direct `config_field` update).\n4. When the human picks a profile, call the `config_field` MCP tool once (operation `"update"`,\n `field_name: "ci_followup_config"`, `value`: the profile\'s JSON object). If the server rejects the\n value, retry ONCE with a corrected payload; if it is rejected again, stop, show the proposed JSON\n to the human, and leave the field unset.\n5. Never select a profile in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never enable fix-and-iterate behavior.\n\n## Return\n\nReport the admin check result, the "Applied N of M" count, the\napplied/skipped/conflict/needs_confirmation/rejected field names, any fields omitted for\nnested-repository or ambiguity reasons, the approval outcome for each confirmation-requiring field\n(approved / declined / pending human input), whether a stale-command warning was raised (manifest\n`command_contract_version` higher than this command\'s), whether the routing credential was persisted\n(the returned `target` and `path`, or the non-blocking failure remediation), the CI follow-up outcome\n(profile written / skipped / no CI detected / pending), and the recommended next\nstep (`/learn-repository`).\n',
14640
14833
  "learn-repository.md": 'Learn and document all configuration fields for the repository by running parallel research agents.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. This command takes no arguments.\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"learn-repository"`\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',
@@ -14643,8 +14836,8 @@ var COMMANDS = {
14643
14836
  "plan-ticket.md": 'Generate an implementation plan for a Jira ticket, wait for the result, and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 \u2014 Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = "auto"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: "Usage error: --provider requires a provider name (openai, anthropic, or gemini)."\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n - If `ticket_key` is empty or missing, stop immediately and display:\n\n ```\n Usage: /plan-ticket <ticket_key> [--second-opinion [provider]] [--provider <name>] (e.g., /plan-ticket BAPI-150)\n ```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Generate Plan\n\nCall the `request_plan_generation` MCP tool with:\n- `ticket_number`: the parsed `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nThis step may take 1-5 minutes while the backend processes the plan.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nPlan generation failed: <error message from the tool>\n```\n\n## Step 4 \u2014 Confirm Success\n\nDisplay a confirmation message:\n\n```\nPlan generated successfully for <ticket_key>\nSaved to: {docs_dir}/plans/<ticket_key>-plan.md\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Plan Generation Report\n\n- **Ticket**: <ticket_key>\n- **Plan Status**: Generated successfully\n- **Local File**: {docs_dir}/plans/<ticket_key>-plan.md\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n',
14644
14837
  "regression-check.md": 'Run the deterministic regression-reviewer (lightweight mode) against a proposed code change and report its blast radius \u2014 which real call-sites, tests, mocks, or config the change does and doesn\'t account for.\n\n$ARGUMENTS\n\n---\n\n<!-- Platform coverage: this command reaches Cursor + Claude Code (the commands\n bundle is scaffolded to .claude/commands/ and .cursor/commands/ by --init).\n The companion `regression-reviewer` agent (agents/src/regression-reviewer.md)\n reaches Claude Code + GitHub Copilot. Union: Cursor, Copilot, and Claude\n Code all get this review, either via the command or the agent. -->\n\n# Instructions\n\nThis is the standalone diff/PR (or ticket-description) review entry point for the regression-reviewer (BAPI-460). It mirrors the `regression-reviewer` agent\'s orchestration exactly \u2014 same subcommand, same parsing, same report \u2014 so the same logical review behaves identically whether invoked as a Cursor command or a Claude Code agent.\n\n## Step 1 \u2014 Determine the Input Shape\n\nParse `$ARGUMENTS`:\n\n- If it looks like a git diff range, a ref, a PR number, or is empty (defaulting to the working tree vs. `HEAD`), treat this as a **diff/PR invocation**. Resolve a `--diff <range>` value when one is given (e.g. `main...HEAD`, a commit SHA range); omit `--diff` to use the default working-tree-vs-HEAD diff.\n- If it names specific function/class/symbol names (e.g. `--symbols resolve_db_params,SomeClass`, or prose describing a not-yet-diffed planned change), treat this as a **ticket-description invocation**. Extract the symbol names and pass them via `--symbols a,b,c`.\n\nIf neither a diff nor any extractable symbol names are available, stop and ask the user to provide one.\n\n## Step 2 \u2014 Run the Deterministic Core\n\nExecute exactly:\n\n```bash\nnpx -y @bridge_gpt/mcp-server regression-check --mode lightweight --json [--diff <range> | --symbols a,b,c]\n```\n\nDo NOT hand-roll your own `ast-grep`/`ripgrep` invocations or re-discover call-sites yourself \u2014 the subcommand owns that structural analysis.\n\n## Step 3 \u2014 Parse the Findings (Fail-Open)\n\nParse the JSON: `summary.symbols_analyzed`, `summary.truncated`, `summary.tools_used`, `summary.degraded_flags`, and the `findings` array (`symbol`, `file`, `definition_location`, `call_sites` (`count`, `by_file`), `broad_mentions`).\n\nIf `summary.degraded_flags` is non-empty (e.g. `ast-grep` or `ripgrep` was unavailable), do NOT treat the run as a failure. Proceed with whatever data IS present and call out each degraded section explicitly \u2014 never silently drop a gap. If `summary.truncated` is `true`, state that the symbol set was capped.\n\n**Ripgrep degradation formatting**: if any entry in `summary.degraded_flags` mentions ripgrep, render it with an action-first visual hierarchy of three scannable segments rather than as a plain sentence:\n1. **Status/Problem** \u2014 a warning header with a semantic warning indicator (e.g. \u26A0\uFE0F).\n2. **Root Cause** \u2014 a brief note that `rg` must be a real binary on `PATH`; a shell function/alias is invisible to the spawned subcommand.\n3. **Remediation** \u2014 a standalone, copy-pasteable install command (e.g. `brew install ripgrep`) on its own line.\n\nApply monospace typography (backticks) to every reference to a system command, CLI tool (`rg`), environment term (`PATH`), or installation package, in this warning and throughout the report.\n\n## Step 4 \u2014 Synthesize Risk\n\nFor each symbol, compare `call_sites.count` against `broad_mentions.length`:\n- **Accounted for**: every real call-site and broad mention is either already touched by the change or clearly unaffected (e.g. a doc/comment mention).\n- **Not accounted for**: a real call-site, or an uninspected broad mention, sits in a file the proposed change does not touch. Name the specific file.\n\nRank "not accounted for" items by how directly they call the changed symbol (a real call-site outranks a textual mention).\n\n## Step 5 \u2014 Propose De-Risking (Diagnostic Synthesis, Not a Patch)\n\nFor each "not accounted for" item, name ONE of:\n- **Update the affected caller**: point to the exact `file:line` and describe what needs to change there.\n- **Add a compatibility/guard seam**: when updating every caller isn\'t right (e.g. a public API, a config key read elsewhere), describe the seam needed \u2014 not its full implementation.\n\nDo NOT implement the fix. State what needs to happen and where.\n\n## Final Output\n\nPrint this report to chat (no file write required):\n\n```markdown\n# Regression Review: [Symbol(s) / Change Description]\n\n**Mode**: lightweight\n**Input**: [--diff <range> | --symbols a,b,c]\n**Tools used**: [summary.tools_used, joined]\n**Degraded**: [list summary.degraded_flags, or "none"]\n**Symbols analyzed**: [summary.symbols_analyzed.length][ \u2014 TRUNCATED, capped at N if summary.truncated]\n\n## Summary\n\n[2-3 sentences: overall risk level, how many symbols are fully accounted for vs. not, and any degraded-tool caveats.]\n\n## Systems Accounted For / Not Accounted For\n\nRender as a `.data-table`-style markdown table (bold header row; left-aligned `Symbol` / `File` columns; tight \u2705/\u26A0\uFE0F status indicators):\n\n| Symbol | File | Real Call-Sites | Broad Mentions | Status |\n|---|---|---|---|---|\n| `helper` | `src/foo.py` | 3 | 4 | \u26A0\uFE0F Not accounted for |\n| `caller` | `src/foo.py` | 1 | 1 | \u2705 Accounted for |\n\nIf `definition_location` is `null` for a symbol, degrade gracefully \u2014 do not leave the `File` column blank. Render a muted `\u2014` placeholder there instead.\n\n## De-Risking Guidance\n\n### 1. [Symbol / File]\n- **Issue**: [what\'s not accounted for, with file:line]\n- **Recommendation**: Update the affected caller at `file:line` | Add a compatibility/guard seam \u2014 [describe]\n\n[Continue for each not-accounted-for item]\n\n---\n\n*Generated by regression-check (lightweight mode). Structural findings via ast-grep + ripgrep; Pinecone semantic search not available.*\n```\n',
14645
14838
  "reimplement-ticket.md": "# Reimplement Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command retrieves the reimplement context for a previously-implemented Jira ticket via MCP, then implements follow-up changes inline. Use this for small follow-up requests on tickets that have already been through the plan+implement cycle.\n\nIf any critical stage fails (Stage 0 or Stage 1), stop immediately and report which stage failed and why.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to implement follow-up changes on a Jira ticket using assembled reimplement context. Execute all stages in sequence.\n\n## Stage 0 \u2014 Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = \"auto\"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: \"Usage error: --provider requires a provider name (openai, anthropic, or gemini).\"\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n - If `ticket_key` is empty or does not match the expected format (one or more uppercase letters, a hyphen, and one or more digits), stop immediately and display:\n\n ```\n Invalid ticket key format: '<value>'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /reimplement-ticket <ticket_key> [--second-opinion [provider]] [--provider <name>] (e.g., /reimplement-ticket BAPI-150)\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Request and Retrieve Reimplement Context\n\nCall the `request_reimplement_context` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if it is non-null; omit the parameter entirely if `second_opinion_value` is null\n- `provider`: set to `provider_value` if it is non-null; omit the parameter entirely if `provider_value` is null\n\nIf the tool returns an error or 404 persists after polling, stop immediately and display:\n\n```\nFailed to retrieve reimplement context for <ticket_key>.\nThis may mean:\n- The ticket has not been previously processed by Bridge API\n- Background processing failed \u2014 check server logs\n- The ticket does not exist in Jira\n\nTry running /plan-ticket <ticket_key> first if this is a new ticket.\n```\n\nOn success, read and internalize the returned context markdown. This document contains:\n- A summary of changes (if applicable)\n- New/changed information since last processing (comments, description changes, attachments)\n- The original ticket description\n- The existing implementation plan (at the bottom, for reference only)\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Implement Follow-Up Changes\n\nExecute changes inline in this conversation. Work directly so the user can see all progress and approve tool calls.\n\nFollow these rules:\n\n1. **Focus on the new information.** The context document identifies what has changed since the last implementation. Focus your changes on addressing the new/changed requirements.\n2. **Reference the existing plan as supplementary guidance only.** The plan at the bottom of the context describes the original implementation, not the follow-up. Use it to understand the existing code structure, not as a step-by-step guide.\n3. **Make code changes** as directed by the new information.\n4. **Run tests and checks** to verify your changes don't break existing functionality.\n5. **Do NOT run `git commit` or `git push`.** Leave all changes uncommitted for developer review.\n6. **Scope guard**: If the follow-up changes are too large in scope (e.g., fundamentally restructuring the original implementation, touching more than 5-6 files, or requiring new infrastructure), stop and ask the user for guidance rather than attempting everything. Follow-up reimplementations should be small and targeted.\n7. **If a change is ambiguous or blocked**, note the issue clearly and continue with the next change rather than halting entirely.\n\nThis stage is **critical** \u2014 if a blocking error prevents further progress, stop and report the failure.\n\n## Stage 3 \u2014 Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Reimplement Complete\n\n**Ticket**: <ticket_key>\n\n**Changes Made**:\n- <brief summary of each change>\n\n**Developer Action Items**:\n- All changes are uncommitted. Review the changes with `git diff` before committing.\n- Run the project's test suite to verify nothing is broken before committing.\n\n**Warnings**:\n<If any issues arose during implementation (scope concerns, ambiguous requirements,\nfiles that couldn't be modified), list them here. If no warnings, omit this section.>\n```\n\n## Final Report\n\nOn success, display the structured report from Stage 3 confirming that the follow-up changes are complete.\n\nOn failure at any critical stage (Stage 0 or Stage 1), display which stage failed and the error details.\n",
14646
- "review-ticket.md": '---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"baseSha","flag":"--base-sha","type":"string"}]}\n---\n\n# Review Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. Parse `$ARGUMENTS` to extract:\n - A required `ticket_key` matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`).\n - An optional position-independent `--auto` flag.\n - An optional position-independent `--rounds=<n>` argument, where `<n>` is `1` or `2`.\n - An optional position-independent `--no-refresh-base` flag.\n - An optional position-independent `--base-branch=<branch>` argument.\n - An optional position-independent `--base-sha=<sha>` argument.\n\n Tokenize `$ARGUMENTS` on whitespace. The first token matching the Jira key pattern is the `ticket_key`; ignore any additional ticket-key tokens. The presence of a `--auto` token (anywhere in `$ARGUMENTS`) sets `auto_approve` to `true`. A token matching `--rounds=1` sets `rounds` to `1`; a token matching `--rounds=2` sets `rounds` to `2`. If no `--rounds` token is present, `rounds` defaults to `2` (full review). The presence of a `--no-refresh-base` token sets `no_refresh_base` to `true`. A token matching `--base-branch=<branch>` sets `base_branch` to `<branch>`. A token matching `--base-sha=<sha>` sets `base_sha` to `<sha>`.\n\n `--auto`, `--rounds`, `--no-refresh-base`, `--base-branch`, and `--base-sha` are all independent and may be supplied in any combination.\n\n If `$ARGUMENTS` is empty or contains no token matching the Jira key pattern, stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123 [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n If a `--rounds` token is present but its value is not `1` or `2`, stop and display:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2 (default: 2)\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"review-ticket"`\n - `variables`: `{ "ticket_key": "<ticket_key>", "base_branch": "<base_branch or "">", "base_sha": "<base_sha or "">", "no_refresh_base": "<"true" if --no-refresh-base was passed, else "">" }`\n - `auto_approve`: `true` \u2014 only when `--auto` was passed; otherwise omit this field entirely.\n - `skip_steps`: `["second-opinion-review"]` \u2014 only when `rounds` is `1`; otherwise omit `skip_steps` entirely (do not pass `skip_steps: []` or `skip_steps: null`).\n\n Example combined-mode payload (`--rounds=1 --auto --base-branch=develop`):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "develop", "base_sha": "", "no_refresh_base": "" },\n "auto_approve": true,\n "skip_steps": ["second-opinion-review"]\n }\n ```\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',
14647
- "review-tickets.md": '---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"review","flag":"--review","type":"string","repeatable":true},{"name":"agent","flag":"--agent","type":"string"},{"name":"model","flag":"--model","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"}]}\n---\n\n# Review Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `review-tickets`, which opens one terminal tab per ticket running the selected agent with `/review-ticket <KEY> [--auto] --rounds=<1|2>`. Unlike `/start-tickets`, it creates no Worktrunk worktrees \u2014 but it now requires `git` on PATH: the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch before spawning tabs (BAPI-474), so every spawned review grounds its codebase evaluation against the same freshly-fetched base tree. Pass `--no-refresh-base` to skip this and restore the prior git-free, in-place-grounded behavior.\n\n---\n\n# Instructions\n\n## Stage 0 \u2014 Parse Arguments and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** to extract ticket keys, review modes, and pass-through flags:\n\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-1`). If zero keys are found, stop immediately and display:\n ```\n No ticket keys found. Expected one or more keys like BAPI-1.\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n - **Review mode interpretation** (per ticket or global):\n - `auto` or `--auto` \u2192 per-ticket or global auto-approve flag.\n - `single-pass`, `one-pass`, `rounds=1`, or `--rounds=1` \u2192 `rounds=1`.\n - `full`, `two-pass`, `rounds=2`, `--rounds=2`, or omitted rounds \u2192 `rounds=2`.\n - `--auto` and `--rounds` are independent: both may apply to the same ticket.\n\n - **Homogeneous modes**: when all tickets share the same auto and rounds values, translate into global `--auto` (if all auto) and `--rounds=1|2` (if all rounds are the same).\n\n - **Heterogeneous modes**: when different tickets have different auto or rounds values, translate into repeatable `--review KEY=auto,rounds=N` overrides. Do NOT set global `--auto` when only some tickets are auto-approved.\n\n - **Pass-through flags**: collect `--dry-run`, `--max-parallel N`, `--agent claude|cursor-agent`, `--model VALUE`, `--no-refresh-base`, and `--base-branch VALUE` if supplied, and forward verbatim to the CLI.\n\n2. **Connectivity check**: Call the `ping` MCP tool. If it fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n## Stage 1 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke exactly one CLI invocation:\n\n```\nnpx -y @bridge_gpt/mcp-server review-tickets [--auto] [--rounds=1|2] [--review KEY=auto,rounds=N ...] [--agent <name>] [--model <alias>] [--max-parallel N] [--dry-run] [--no-refresh-base] [--base-branch BRANCH] KEY [KEY ...]\n```\n\n- `review-tickets` runs all tabs from the current repository cwd \u2014 it creates no worktrees.\n- The command never runs `wt` or `git-wt` \u2014 but it now requires `git` on PATH (BAPI-474): before spawning any tabs, the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch, so a mid-batch `origin` advance can never mix bases within one run. Pass `--no-refresh-base` to skip the fetch and restore the prior git-free, in-place-grounded behavior.\n- Prerequisites: macOS `osascript` + `git`, Windows `wt.exe` or PowerShell + `git`, Linux `tmux` + `git` (git is not required when `--no-refresh-base` is passed).\n\nPass through the CLI\'s stdout and stderr verbatim. If the CLI exits non-zero, treat it as a critical failure and report the exit code and error output.\n\n## Stage 2 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary:` lines (each shaped like `KEY auto=<true|false> rounds=<1|2> agent=<agent> model=<alias|default> status=<status>`) and render as a markdown table:\n\n```\n| Ticket | Auto | Rounds | Agent | Model | Status |\n|----------|-------|--------|--------|---------|---------|\n| BAPI-1 | false | 2 | claude | default | spawned |\n| BAPI-2 | true | 1 | claude | default | spawned |\n```\n\nRender any CLI `Warnings:` lines below the table. If there were none, omit the warnings section.\n',
14839
+ "review-ticket.md": '---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKey","type":"string","required":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"baseSha","flag":"--base-sha","type":"string"}]}\n---\n\n# Review Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. Parse `$ARGUMENTS` to extract:\n - A required `ticket_key` matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`).\n - An optional position-independent `--auto` flag.\n - An optional position-independent `--rounds=<n>` argument, where `<n>` is `1` or `2`.\n - An optional position-independent `--no-refresh-base` flag.\n - An optional position-independent `--base-branch=<branch>` argument.\n - An optional position-independent `--base-sha=<sha>` argument.\n\n Tokenize `$ARGUMENTS` on whitespace. The first token matching the Jira key pattern is the `ticket_key`; ignore any additional ticket-key tokens. The presence of a `--auto` token (anywhere in `$ARGUMENTS`) sets `auto_approve` to `true`. A token matching `--rounds=1` sets `rounds` to `1`; a token matching `--rounds=2` sets `rounds` to `2`. If no `--rounds` token is present, leave `rounds` unset (omitted) so the backend\'s difficulty-adaptive review policy can decide the review shape when enabled for this repo; when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved, the backend falls back to a full premium second-opinion review. The presence of a `--no-refresh-base` token sets `no_refresh_base` to `true`. A token matching `--base-branch=<branch>` sets `base_branch` to `<branch>`. A token matching `--base-sha=<sha>` sets `base_sha` to `<sha>`.\n\n `--auto`, `--rounds`, `--no-refresh-base`, `--base-branch`, and `--base-sha` are all independent and may be supplied in any combination.\n\n If `$ARGUMENTS` is empty or contains no token matching the Jira key pattern, stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123 [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n If a `--rounds` token is present but its value is not `1` or `2`, stop and display:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2 (omit to let the backend decide adaptively; default falls back to a full review)\n Usage: /review-ticket <ticket_key> [--auto] [--rounds=1|2] [--no-refresh-base] [--base-branch=BRANCH] [--base-sha=SHA]\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `"review-ticket"`\n - `variables`: `{ "ticket_key": "<ticket_key>", "base_branch": "<base_branch or "">", "base_sha": "<base_sha or "">", "no_refresh_base": "<"true" if --no-refresh-base was passed, else "">" }`\n - `auto_approve`: `true` \u2014 only when `--auto` was passed; otherwise omit this field entirely.\n - `rounds`: `1` \u2014 only when `--rounds=1` was explicitly passed on the command; `2` \u2014 only when `--rounds=2` was explicitly passed. When `--rounds` was NOT supplied, omit `rounds` entirely (do not pass `rounds: null`) so the backend\'s difficulty-adaptive review policy can choose the review shape when enabled for this repo. An explicit `rounds` value is forwarded to the backend and forces the review shape: `--rounds=1` requests a single-pass review, and `--rounds=2` forces the full second-opinion review even when adaptive routing is enabled. Do NOT translate `rounds` into `skip_steps` \u2014 the backend executor now owns all round orchestration (including any second-opinion rounds), so the recipe carries a single `request_ticket_review` step and you never pass `skip_steps` for round control.\n\n Example combined-mode payload (`--rounds=1 --auto --base-branch=develop`):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "develop", "base_sha": "", "no_refresh_base": "" },\n "auto_approve": true,\n "rounds": 1\n }\n ```\n\n Example explicit full-review payload (`--rounds=2`), which forces the full second-opinion review even if adaptive routing is enabled for this repo:\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" },\n "rounds": 2\n }\n ```\n\n Example adaptive payload (no `--rounds`), which lets the backend policy executor decide the review shape (falling back to a full premium review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved):\n ```json\n {\n "pipeline": "review-ticket",\n "variables": { "ticket_key": "PROJ-123", "base_branch": "", "base_sha": "", "no_refresh_base": "" }\n }\n ```\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n',
14840
+ "review-tickets.md": '---\nschedulable: true\ninteractive: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"review","flag":"--review","type":"string","repeatable":true},{"name":"agent","flag":"--agent","type":"string"},{"name":"model","flag":"--model","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"},{"name":"noRefreshBase","flag":"--no-refresh-base","type":"boolean"},{"name":"baseBranch","flag":"--base-branch","type":"string"}]}\n---\n\n# Review Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `review-tickets`, which opens one terminal tab per ticket running the selected agent with `/review-ticket <KEY> [--auto] [--rounds=<1|2>]`. By default `--rounds` is omitted so the backend routes each review by difficulty (difficulty-adaptive review); pass an explicit `--rounds=1|2` (globally or per ticket) to force the review shape. Unlike `/start-tickets`, it creates no Worktrunk worktrees \u2014 but it now requires `git` on PATH: the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch before spawning tabs (BAPI-474), so every spawned review grounds its codebase evaluation against the same freshly-fetched base tree. Pass `--no-refresh-base` to skip this and restore the prior git-free, in-place-grounded behavior.\n\n---\n\n# Instructions\n\n## Stage 0 \u2014 Parse Arguments and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** to extract ticket keys, review modes, and pass-through flags:\n\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-1`). If zero keys are found, stop immediately and display:\n ```\n No ticket keys found. Expected one or more keys like BAPI-1.\n Usage: /review-tickets [flags] KEY [KEY ...]\n ```\n\n - **Review mode interpretation** (per ticket or global):\n - `auto` or `--auto` \u2192 per-ticket or global auto-approve flag.\n - `single-pass`, `one-pass`, `rounds=1`, or `--rounds=1` \u2192 `rounds=1` (single-pass review).\n - `full`, `two-pass`, `rounds=2`, or `--rounds=2` \u2192 `rounds=2` (full second-opinion review).\n - **omitted rounds \u2192 `adaptive`**: when no rounds mode is given for a ticket, do NOT choose a round count \u2014 leave it adaptive so the backend\'s difficulty-adaptive review policy decides the shape. `adaptive` is a distinct mode from `1` and `2`.\n - `--auto` and `--rounds` are independent: both may apply to the same ticket.\n\n The backend executor now owns all review round orchestration (including any second-opinion rounds) server-side \u2014 there is no client-side step to skip, so this command never sends `skip_steps` and never translates `--rounds=1` into skipping a second-opinion step. An explicit `--rounds` value forwarded to each spawned `/review-ticket` forces the review shape (`1` = single pass, `2` = full second-opinion review). A spawned `/review-ticket` invoked without any `--rounds` (the default) lets the backend\'s difficulty-adaptive review policy decide the shape (falling back to a full premium second-opinion review when adaptive routing is disabled, unavailable, or the ticket\'s difficulty cannot be resolved). This batch command therefore forwards **no** `--rounds` by default; it only forwards `--rounds` when the caller explicitly supplies a rounds mode (globally via `--rounds`, or per ticket via `--review`).\n\n - **Homogeneous modes**: when all tickets share the same auto and rounds mode, translate into global `--auto` (if all auto) and, only when all tickets share the same *explicit* rounds value, global `--rounds=1|2`. When all tickets are adaptive (no rounds given), omit `--rounds` entirely \u2014 do not synthesize a default.\n\n - **Heterogeneous modes**: when tickets differ in auto or rounds mode, translate into repeatable `--review KEY=auto,rounds=N` overrides. Only emit a `rounds=N` subtoken for tickets given an explicit `1`/`2`; adaptive tickets carry no `rounds` subtoken (a bare `--review KEY=auto` if they are auto, or no override at all). Do NOT set global `--auto` when only some tickets are auto-approved, and do NOT set global `--rounds` when only some tickets have an explicit rounds value.\n\n - **Pass-through flags**: collect `--dry-run`, `--max-parallel N`, `--agent claude|cursor-agent`, `--model VALUE`, `--no-refresh-base`, and `--base-branch VALUE` if supplied, and forward verbatim to the CLI.\n\n2. **Connectivity check**: Call the `ping` MCP tool. If it fails or does not return `"status": "ok"`, stop immediately and display:\n ```\n Connectivity check failed. Please verify:\n - Check that the Bridge API MCP server is configured in your editor\'s MCP settings\n - Check that BAPI_BASE_URL is set and the server is reachable\n - Check that BAPI_API_KEY is valid\n - Check that BAPI_REPO_NAME matches a configured repository\n ```\n\n## Stage 1 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke exactly one CLI invocation:\n\n```\nnpx -y @bridge_gpt/mcp-server review-tickets [--auto] [--rounds=1|2] [--review KEY=auto,rounds=N ...] [--agent <name>] [--model <alias>] [--max-parallel N] [--dry-run] [--no-refresh-base] [--base-branch BRANCH] KEY [KEY ...]\n```\n\n- `review-tickets` runs all tabs from the current repository cwd \u2014 it creates no worktrees.\n- The command never runs `wt` or `git-wt` \u2014 but it now requires `git` on PATH (BAPI-474): before spawning any tabs, the parent process fetches `origin/<base_branch>` once and pins a single `base_sha` for the whole batch, so a mid-batch `origin` advance can never mix bases within one run. Pass `--no-refresh-base` to skip the fetch and restore the prior git-free, in-place-grounded behavior.\n- Prerequisites: macOS `osascript` + `git`, Windows `wt.exe` or PowerShell + `git`, Linux `tmux` + `git` (git is not required when `--no-refresh-base` is passed).\n\nPass through the CLI\'s stdout and stderr verbatim. If the CLI exits non-zero, treat it as a critical failure and report the exit code and error output.\n\n## Stage 2 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary:` lines (each shaped like `KEY auto=<true|false> rounds=<1|2|adaptive> agent=<agent> model=<alias|default> status=<status>`) and render as a markdown table (`rounds=adaptive` means the backend chose the shape by difficulty):\n\n```\n| Ticket | Auto | Rounds | Agent | Model | Status |\n|----------|-------|----------|--------|---------|---------|\n| BAPI-1 | false | adaptive | claude | default | spawned |\n| BAPI-2 | true | 1 | claude | default | spawned |\n```\n\nRender any CLI `Warnings:` lines below the table. If there were none, omit the warnings section.\n',
14648
14841
  "run-tests.md": 'Run the project\'s full test suite (unit and E2E) using the project-configured test stacks, triage failures, fix test-code issues, and produce a structured health-check report.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command discovers how to run tests by reading per-project configuration from the Bridge API, not from hardcoded paths. Stages run only when the project has the corresponding stack configured.\n\n## Stage 0 \u2014 Argument Parsing and Setup\n\n1. **Parse `$ARGUMENTS`** for optional flags. Supported flags:\n - `--skip-e2e` \u2014 skip the E2E test stage even if an E2E stack is configured (e.g., when no local server is running)\n - `--unit-only` \u2014 shorthand that implies `--skip-e2e`\n\n Resolve flags to boolean variables:\n - Start with: `run_unit = true`, `run_e2e = true`\n - If `--unit-only` is present: set `run_e2e = false`\n - If `--skip-e2e` is present: set `run_e2e = false`\n - Unknown flags: note them in the final report as "Unrecognized flag ignored" but do not fail\n\n2. **Generate a run timestamp** using the current date and time in `YYYY-MM-DD-HH-MM` format (e.g., `2026-03-10-14-35`). Store this as `run_timestamp`. Both output documents will use this value.\n\nThis stage has no failure conditions \u2014 proceed to Stage 1.\n\n## Stage 1 \u2014 Resolve Project Config via MCP\n\nRead the per-project test setup from the Bridge database. Every subsequent stage is driven by what these calls return.\n\n1. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n2. **Read unit-test stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_stack`. Store the returned value as `unit_stack` (may be null/empty).\n\n3. **Read unit-test instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `unit_testing_instructions`. Store the returned value as `unit_instructions` (may be null/empty).\n\n4. **Read E2E stack**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_stack`. Store as `e2e_stack`.\n\n5. **Read E2E instructions**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `e2e_testing_instructions`. Store as `e2e_instructions`.\n\n6. **Compute configuration booleans**:\n - `unit_configured` = `true` if either `unit_stack` or `unit_instructions` is a non-empty string; otherwise `false`\n - `e2e_configured` = `true` if either `e2e_stack` or `e2e_instructions` is a non-empty string; otherwise `false`\n\n7. **Create the output directory**:\n ```\n mkdir -p {docs_dir}/testing/\n ```\n If this fails, stop immediately and report: `Cannot create output directory {docs_dir}/testing/ \u2014 check permissions.`\n\nIf any MCP call fails (e.g., the API is unreachable or returns 4xx/5xx), stop immediately and report which call failed. Do not fall back to hardcoded commands \u2014 the whole point of this command is that test setup lives in config.\n\n## Stage 2 \u2014 Unit / Standard Tests\n\nIf `run_unit` is `false`, skip this stage and record: `Unit tests: SKIPPED \u2014 run_unit was set to false (this should not happen in normal use; report as a bug).`\n\nIf `unit_configured` is `false`, skip and record:\n```\nUnit tests: SKIPPED \u2014 no unit_testing_stack or unit_testing_instructions configured for this repo. Configure via /learn-unit-testing or the project setup UI before running /run-tests.\n```\n\nOtherwise:\n\n1. Read `unit_instructions` carefully. It is the source of truth for **how to run unit tests in this repo** \u2014 runner binary, paths, environment activation, sub-suites (if the project distinguishes "unit" from "integration", both belong in this stage), and any flags. Pair it with `unit_stack` (a short label, e.g., `Pytest`, `Jest + React Testing Library`) for context.\n\n2. **Derive the test command(s)**: Extract the literal shell commands the instructions describe. If the instructions describe multiple sub-suites (e.g., a fast unit batch and a slower integration batch), plan to run each as a **separate batch** in the order described. Do not invent runners or paths that the instructions do not mention.\n\n3. **If the instructions do not specify any runnable command**, skip and record:\n ```\n Unit tests: SKIPPED \u2014 unit_testing_instructions does not describe how to invoke tests; please update via /learn-unit-testing.\n ```\n\n4. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output of each batch, including the runner\'s summary line (e.g., `47 passed, 3 failed in 12.4s` or `Tests: 5 failed, 22 passed`).\n\n5. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Stage 3 \u2014 E2E Tests\n\nIf `run_e2e` is `false`, skip this stage and record: `E2E tests: SKIPPED \u2014 --skip-e2e or --unit-only flag was set.`\n\nIf `e2e_configured` is `false`, skip and record:\n```\nE2E tests: SKIPPED \u2014 no e2e_testing_stack or e2e_testing_instructions configured (the project may not have an E2E suite).\n```\n\nOtherwise:\n\n1. Read `e2e_instructions`. It is the source of truth for the E2E runner, spec paths, browser config, and any prerequisites. Pair with `e2e_stack` for context.\n\n2. **Detect server prerequisites**: If `e2e_instructions` indicates that a local server must be running (look for explicit cues such as "server", "running", "localhost", "started", "dev server", a URL, or a port number) and describes a readiness check, perform that check exactly as described. If the instructions describe a server prerequisite but do not describe a check, attempt the check the instructions imply (e.g., curl the URL the instructions mention) and skip the stage if it fails:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions describe a server prerequisite that wasn\'t met. Start the server per the instructions and re-run.\n ```\n\n3. **Derive the test command(s)** from the instructions, including any spec-directory batching the instructions specify.\n\n4. **If the instructions do not specify any runnable command**, skip and record:\n ```\n E2E tests: SKIPPED \u2014 e2e_testing_instructions does not describe how to invoke tests; please update via /learn-e2e-testing.\n ```\n\n5. **Run each batch sequentially** in the terminal. **Continue to the next batch even if the current one has failures.** Capture the full output and summary line of each batch.\n\n6. For each failing test, apply the **Triage Logic** (below), then record the result.\n\n## Triage Logic\n\nFor every failing test, examine the test file and the code it tests. Classify as ONE of the following:\n\n### TEST-CODE ISSUE \u2014 fix it directly\n\nClassify as a test-code issue if ANY of the following applies:\n- The test asserts against a hardcoded value that no longer matches current behavior (outdated mock data)\n- The test imports or calls a function that was renamed, moved, or removed\n- The test asserts on a response field that was restructured\n- The test expects a specific error message string that has since changed\n- A fixture references a removed table column, model field, or schema member\n\n**Action**: Apply a minimal, targeted fix to the test file only. Then re-run just that failing test, using the runner described in the relevant instructions field (`unit_instructions` for unit-test failures, `e2e_instructions` for E2E failures). Adapt the runner invocation that the instructions provide to target a single test, following whatever convention the instructions or stack idiomatically use.\n\nIf the re-run **still fails** after your fix, do not make further edits \u2014 escalate to implementation-code issue instead and revert your change.\n\n### IMPLEMENTATION-CODE ISSUE (or UNCERTAIN) \u2014 document, do not fix\n\nClassify as an implementation issue if ANY of the following applies:\n- The production function raises an unexpected exception\n- A handler returns the wrong status code or response shape for a documented behavior\n- Business logic produces incorrect output that the test correctly asserts against\n- You are not confident the test is wrong\n\n**Action**: Do NOT modify any file outside the test directories described in `unit_testing_instructions` / `e2e_testing_instructions`. When in doubt about whether a path is test-only, treat it as production code and escalate. Record the failure in the implementation-issues document for the user to triage.\n\n## Stage 4 \u2014 Write Output Documents\n\n### Document 1: Test Run Report (always write this)\n\nWrite to: `{docs_dir}/testing/test-run-{run_timestamp}.md`\n\n```markdown\n# Test Run: {run_timestamp}\n\n## Configuration\n- Unit stack: {unit_stack or "not configured"}\n- E2E stack: {e2e_stack or "not configured"}\n- Unit tests: RUN | SKIPPED \u2014 (reason)\n- E2E tests: RUN | SKIPPED \u2014 (reason)\n\n## Unit Tests\n**Stack**: {unit_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**:\n- `path/to/test_file`: brief description of what was fixed\n- (or "none" if no fixes were needed)\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## E2E Tests\n**Stack**: {e2e_stack or "not configured"}\n**Result**: X passed, Y failed (sum across batches)\n\n### Batch 1: `<command>`\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### Batch 2: `<command>`\n...\n\n**Failures escalated as implementation issues**: N\n\n## Overall Summary\n- Total test fixes applied: N\n- Suspected implementation issues found: N\n- Implementation issues document: {docs_dir}/testing/implementation-issues-{run_timestamp}.md\n (or "not created \u2014 no issues found")\n```\n\n### Document 2: Implementation Issues (only write if issues were found)\n\nIf at least one failure was escalated as an implementation-code issue, write to:\n`{docs_dir}/testing/implementation-issues-{run_timestamp}.md`\n\n```markdown\n# Suspected Implementation Issues: {run_timestamp}\n\nThese test failures were NOT fixed. They may indicate bugs in production code.\nA developer should investigate each item before merging.\n\n## Issue 1\n- **Test**: `path/to/test_file::test_function_name`\n- **Tier**: unit | e2e\n- **Failure message**: (paste the key assertion or exception line)\n- **Why not fixed**: (brief reasoning, e.g., "production function raises KeyError on valid input")\n\n## Issue 2\n...\n```\n\nIf no implementation issues were found, do NOT create this file.\n\n## Final Output\n\nAfter writing all documents, print this summary:\n\n```\nTest run complete: {run_timestamp}\nReport saved to: {docs_dir}/testing/test-run-{run_timestamp}.md\nImplementation issues: {docs_dir}/testing/implementation-issues-{run_timestamp}.md (if applicable)\nNo suspected implementation issues found. (if none)\n```\n',
14649
14842
  "scan-test-coverage.md": 'Scan recently shipped tickets from git history and report which features have or could gain integration tests, and which can only be smoke tested.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nScan the git history for recently shipped tickets and, for each shipped feature, determine whether it already has an integration test, whether it *could* gain one (per this repo\'s conventions \u2014 a test that genuinely executes the system end-to-end via real database operations, real LLM calls, or real FastAPI routing), and \u2014 where integration testing is not possible \u2014 how it could be smoke tested so it still genuinely executes the system.\n\nThis is an **investigation and discovery** command. Describe features (citing code) and *how* they would be tested at a high level. Do **not** design tests in detail, build or edit any tests, or modify feature code. Orchestrate this run in the main thread, and **fan out one subagent per shipped feature** for the per-feature investigation.\n\nThe report is written to a **durable, committed** directory (`docs/test-coverage/`), and a marker file records when the analysis last ran so subsequent runs only inspect git history since the last run.\n\n## Stage 0 \u2014 Parse Arguments and Resolve Analysis Window\n\n1. Read `$ARGUMENTS`. All flags are optional and default-safe. If a flag is malformed, ignore it and add a warning:\n - `--since=YYYY-MM-DD` \u2014 override the window start date.\n - `--full` \u2014 ignore the marker and use a default lookback of 6 months.\n - `--limit=N` \u2014 cap the number of features investigated (parse `N` as an integer; ignore if not a valid integer).\n With no arguments, run **incrementally** from the marker.\n\n2. Set the durable directory to `docs/test-coverage/` (relative to the repo root) and the marker file to `docs/test-coverage/STATE.md`. This command deliberately does **not** use `get_docs_dir` \u2014 its default (`docs/tmp`) is ephemeral, and this report must be durable.\n\n3. Read `docs/test-coverage/STATE.md` if it exists. It records two values: `last_run_utc` (an ISO-8601 UTC timestamp) and `last_analyzed_commit` (a git commit SHA).\n\n4. Resolve the analysis window with this precedence:\n - If `--since=YYYY-MM-DD` was given, use `git log --since=<date>`.\n - Else if `STATE.md` provides `last_analyzed_commit`, use the commit range `<last_analyzed_commit>..HEAD`.\n - Else (first run, no marker), default to `git log --since=<3 months ago>` (mirrors the `/scan-tickets` default of 3 months). Format the date as `YYYY-MM-DD`. Example: if today is 2026-07-07, the default `--since` is `2026-04-07`.\n - `--full` overrides the above and uses a 6-month lookback (`--since=<6 months ago>`).\n\n5. Robustness of the marker: capture `head_sha` by running `git rev-parse HEAD`, and capture the current UTC timestamp now. These become the **new** marker values, but only write them after the report is successfully produced (Stage 4). If a stored `last_analyzed_commit` is not present in history (e.g. a rebase/rewrite), fall back to `git log --since=<the date part of last_run_utc>` and add a warning noting the fallback.\n\n6. Initialize tracking variables:\n - `features` = [] (one entry per shipped feature)\n - `warnings` = [] (per-item failures and fallbacks; the run never aborts on these)\n\n7. Display the resolved window, e.g. "Analyzing shipped features in `<range or --since date>` (HEAD = {head_sha})".\n\n## Stage 1 \u2014 Collect Shipped Features from Git History\n\n1. List merged commits in the resolved window with:\n ```bash\n git log <range> --first-parent --pretty=format:"%H|%h|%ad|%s" --date=short\n ```\n `--first-parent` yields roughly one entry per squashed PR merge.\n\n2. For each commit, extract the ticket key by matching `^BAPI-[0-9]+` against the subject. Group commits by ticket key. Commits with no ticket prefix (e.g. `Fix 500 on ...`) each become a standalone feature labeled as an "untracked change".\n\n3. For each group, collect the changed-file footprint across its commit(s) using `git show --stat <sha>` or `git diff --name-only`. This file footprint is the primary input to the per-feature investigation.\n\n4. Best-effort enrichment: for each ticket key, call the `get_ticket` MCP tool to fetch the ticket summary. This is **fail-open** \u2014 Jira tokens can be expired \u2014 so on any error, add a warning and continue without the summary. Do not abort.\n\n5. Build a `features` entry per group: `{ticket_key, subject, commit_shas, changed_files, jira_summary?}`. If `--limit=N` was given, keep only the first `N` features (most recent first).\n\n6. Display: "Found {count} shipped features to investigate."\n\n7. If `git log` returns no commits, skip to Stage 4 and write a report noting an empty window (and still refresh the marker).\n\n## Stage 2 \u2014 Investigate Each Feature (fan out subagents)\n\nFor each feature in `features`, launch an **Explore** subagent (batch several in parallel). Give each subagent the feature\'s `ticket_key`, `subject`, `changed_files`, and `jira_summary`, and instruct it to do read-only investigation only \u2014 no edits, no test design, no solutioning \u2014 and to return a structured finding.\n\nEach subagent must:\n\n1. Read the changed files and describe what the feature does in 2\u20134 sentences, with concrete `file:line` citations.\n\n2. Identify the feature\'s runtime surface \u2014 one or more of: real database operations (`postgres_client` / a DAL in `api/library/db/`), real LLM calls (`src/python/llms/ai_client.py`, `async_send_message_to_ai`), real FastAPI routing (a route handler under `api/routes/`), an MCP tool (`mcp_server/`), a shell-spawned / CLI flow, a frontend / Playwright surface, or pure logic / config / docs / tests.\n\n3. Check whether an **integration test already exists**: search `tests/integration/` for a mirror path or for references to the changed modules/functions. The reliable classifier is a path under `tests/integration/` plus `@pytest.mark.integration` or reliance on the `--run-integration` flag (conventions in `docs/claude/testing-integration.md`). Cite any test found.\n\n4. Classify the feature into exactly one `bucket`:\n - **`has_integration_test`** \u2014 already covered end-to-end; cite the existing integration test file.\n - **`integration_testable`** \u2014 no test yet, but the feature exercises real DB / LLM / routing and fits an existing `tests/integration/<area>/` pattern. Give a **high-level** approach only: which real entrypoint to call, which backend it would exercise, and the relevant cost/guard note (the gpt-5-nano override via `INTEGRATION_TEST_MODEL`; the local-DB `skipif` guard; `save_to_db=False`). Cite the entrypoint in code.\n - **`smoke_only`** \u2014 genuine end-to-end execution is possible but not as an automated integration test (e.g. MCP tool behavior inside a host, cross-platform terminal spawning, a headless agent session, or browser E2E). Describe how to smoke test it so it **genuinely executes the system**, citing the relevant runbook: `mcp_server/smoke-test/SMOKE-TEST.md`, `tests/mcp/`, `docs/claude/self-install-smoke-test.md`, `docs/claude/start-tickets-smoke-test.md`, or Playwright (`tests/playwright/`, which needs a running server plus `npm run build`).\n - **`not_testable`** \u2014 nothing to execute end-to-end (docs-only, a wording/comment change, pure config, or a test-only change); state why.\n\n5. Return a structured finding with these fields: `ticket_key`, `subject`, `description_with_cites`, `surface`, `bucket`, `existing_test`, `approach`, `why_not`.\n\nCollect all findings. If a per-feature subagent fails, add a warning and continue \u2014 never abort the whole run.\n\n## Stage 3 \u2014 Classify and Synthesize\n\n1. Deduplicate features that span multiple commits (merge by `ticket_key`).\n\n2. Sort each finding into the two required report sections:\n - **Section 1 \u2014 Integration Testing (covered or addable):** findings with `bucket` `has_integration_test` (sub-group "Already covered") or `integration_testable` (sub-group "Could be added").\n - **Section 2 \u2014 Not Integration-Testable:** findings with `bucket` `smoke_only` (sub-group "Smoke-testable \u2014 how") or `not_testable` (sub-group "Not testable \u2014 why").\n\n## Stage 4 \u2014 Write the Report and Update the Marker\n\n1. Create the `docs/test-coverage/` directory if it does not exist. Choose the report path `docs/test-coverage/REPORT-<YYYYMMDD>.md`; if a same-day file already exists, append `-<HHMMSS>` to avoid clobbering it.\n\n2. Write the report with this layout:\n - A title and a metadata block: generated-at UTC timestamp; the analysis window (`<from sha or since-date>` \u2192 `HEAD <head_sha>`); the feature count; and per-bucket tallies.\n - **Section 1 \u2014 Integration Testing: Covered or Addable.** One `### BAPI-NNN \u2014 <subject>` heading per feature, each with **What shipped** (with `file:line` citations), **Current coverage** (cite the existing integration test, or state "none"), and **How it could be integration tested (high level)**.\n - **Section 2 \u2014 Not Integration-Testable.** One heading per feature with the same feature description, plus **Why not integration-testable**, and \u2014 for `smoke_only` features \u2014 **How to smoke test (genuinely execute the system)** with the runbook citation.\n - A **Warnings** section listing each warning as a bullet \u2014 only if `warnings` is non-empty.\n\n3. **Only after** the report file is written successfully, update the marker `docs/test-coverage/STATE.md` with the new `last_run_utc` (the UTC timestamp captured in Stage 0) and `last_analyzed_commit` set to `head_sha`. This date/commit marker is what makes the next run incremental. If the report write fails, do not touch `STATE.md`.\n\n## Final Report\n\nPrint a short summary to chat:\n\n```\n**Test-coverage scan complete**\n\n* Features analyzed: {count}\n* Already covered by integration tests: {n_has}\n* Integration-testable (could be added): {n_addable}\n* Smoke-only: {n_smoke}\n* Not testable: {n_none}\n\nReport: docs/test-coverage/REPORT-<YYYYMMDD>.md\nMarker updated: last_analyzed_commit = {head_sha}\n```\n\nIf `warnings` is non-empty, add a "Warnings:" section listing each warning as a bullet. If there are no warnings, omit that section.\n',
14650
14843
  "scan-tickets.md": '$ARGUMENTS\n\n---\n\n# Instructions\n\nSynchronize recently-updated Jira tickets with the local `tickets` database table and backfill missing workflow state timestamps. Perform all work directly in the main thread.\n\n## Stage 0 \u2014 Parse Arguments and Calculate Date\n\n1. Read the value of `$ARGUMENTS`. If it is empty, whitespace-only, or not a valid integer, default `months_back` to `3`. If it contains multiple tokens, extract only the first token and attempt to parse it as an integer. If parsing fails, default to `3`.\n\n2. Calculate `updated_since` by subtracting `months_back` months from today\'s date. Format the result as `YYYY-MM-DD`. Example: if today is 2026-03-07 and `months_back` is 3, then `updated_since` is 2025-12-07.\n\n3. Display the parsed values: "Scanning tickets updated since {updated_since} (months_back = {months_back})"\n\n4. Initialize the following tracking variables:\n - `tickets_scanned` = 0 (total tickets fetched from Jira)\n - `newly_tracked` = 0 (tickets inserted into database for the first time)\n - `state_updated_list` = [] (list of objects with ticket key and fields updated)\n - `warnings` = [] (list of warning strings for any per-ticket failures)\n\n## Stage 1 \u2014 Fetch All Tickets from Jira\n\n1. Initialize an empty list `all_tickets` and set `offset` to `0`.\n\n2. Enter a pagination loop:\n - Call the `get_tickets` MCP tool with: `updated_since` set to the calculated date, `limit` set to `100`, and `offset` set to the current offset value.\n - Parse the JSON response. The response contains a `tickets` array of ticket objects. Each ticket object has a `ticket_number` field (the Jira key, e.g., `BAPI-42`), along with `summary`, `status`, `issue_type`, `assignee`, and `updated_at`.\n - Append all tickets from the response\'s `tickets` array to `all_tickets`.\n - If the number of tickets returned in this page equals `100`, increment `offset` by `100` and repeat the loop.\n - If fewer than `100` tickets are returned, exit the loop.\n\n3. Set `tickets_scanned` to the length of `all_tickets`.\n\n4. Display: "Fetched {tickets_scanned} tickets from Jira. Processing..."\n\n5. If the `get_tickets` call fails at any point during pagination, **stop** and report the error. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Track Each Ticket\n\n1. Iterate over each ticket in `all_tickets`. For each ticket:\n - Call the `track_ticket` MCP tool with `ticket_number` set to the ticket\'s `ticket_number` field. If the ticket object includes a `summary` field, pass it as the `description` parameter.\n - Inspect the response message. If the response indicates the ticket was newly created/inserted (look for words like "created" or "inserted" in the message, as opposed to "already exists" or "updated"), increment `newly_tracked` by 1.\n - If the `track_ticket` call fails for this ticket, add a warning to the `warnings` list (e.g., "Warning: Failed to track ticket {ticket_number}: {error}") and **continue** to the next ticket. Do not abort the scan.\n\n2. Display a brief progress indicator every 25 tickets, e.g., "Tracked {N} of {tickets_scanned} tickets..."\n\n## Stage 3 \u2014 Detect and Backfill Workflow State\n\nDisplay: "Checking workflow state for {tickets_scanned} tickets..."\n\nIterate over each ticket in `all_tickets`. For each ticket (referenced by its `ticket_number` field), perform the following sub-steps. Wrap the entire per-ticket block in error handling: if the `get_ticket_state` call or the subsequent `update_ticket_state` call fails for a ticket, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4a \u2014 Retrieve current state**: Call the `get_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key. The response contains:\n\n- Five timestamp fields (each is a timestamp string or null): `clarify_called`, `clarify_answered`, `critique_called`, `critique_answered`, `plan_generated`\n- Three boolean artifact flags: `has_clarifying_questions`, `has_critique`, `has_plan`\n\nIf the call returns a 404 or any error, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4b \u2014 Build fields_to_update list**: Initialize an empty `fields_to_update` list, then apply the following rules:\n\n- If `has_clarifying_questions` is `true` AND `clarify_called` is null -> add `"clarify_called"` to `fields_to_update`\n- If `has_clarifying_questions` is `true` AND `clarify_answered` is null -> add `"clarify_answered"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_called` is null -> add `"critique_called"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_answered` is null -> add `"critique_answered"` to `fields_to_update`\n- If `has_plan` is `true` AND `plan_generated` is null -> add `"plan_generated"` to `fields_to_update`\n\n**Sub-step 4c \u2014 Call update_ticket_state if needed**: If `fields_to_update` is non-empty, call the `update_ticket_state` MCP tool with `ticket_number` set to the ticket\'s key and `fields` set to the `fields_to_update` array. If this succeeds, add an entry to `state_updated_list` recording the ticket key and the list of fields that were set. If `update_ticket_state` fails, add a warning to `warnings` and continue.\n\nDisplay a progress indicator every 25 tickets that includes the current ticket key, e.g., "Checked state for {TICKET-KEY} ({N} of {tickets_scanned} tickets)"\n\n## Stage 4 \u2014 Report Summary\n\n1. Calculate `state_updated_count` as the length of `state_updated_list`.\n\n2. Display the summary:\n\n ```\n **Scan complete**\n\n * Tickets scanned: {tickets_scanned}\n * Newly tracked: {newly_tracked}\n * State updated: {state_updated_count}\n ```\n\n3. If `state_updated_list` is non-empty, display a section titled "Updated tickets:" with one bullet per ticket showing the ticket key and the comma-separated list of fields that were set. Example:\n\n ```\n Updated tickets:\n * BAPI-101: clarify_called, clarify_answered\n * BAPI-105: critique_called, critique_answered, plan_generated\n ```\n\n4. If the `warnings` list is non-empty, display a section titled "Warnings:" listing each warning string as a bullet. Example:\n\n ```\n Warnings:\n * Warning: Failed to track ticket BAPI-99: Connection timeout\n * Warning: State query failed for BAPI-112: SQL error\n ```\n\n5. If there are no warnings, do not display the "Warnings:" section.\n',
@@ -14662,7 +14855,7 @@ var AGENTS = {
14662
14855
  "model": "opus",
14663
14856
  "color": "blue"
14664
14857
  },
14665
- "body": '\nYou are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.\n\n## Your Mission\n\nGiven a problem description from the user, you will:\n1. Conduct thorough codebase research to understand the existing architecture, patterns, and relevant code\n2. Write a structured Jira ticket as a new markdown file that references specific files, functions, and patterns from the codebase\n\n## Phase 1: Deep Codebase Research\n\nThis is the most critical phase. You MUST spend significant time here before writing anything. Do NOT rush this phase.\n\n### Research Protocol\n\n1. **Understand the Problem Space**: Re-read the user\'s problem description carefully. Identify the domain, the affected areas, and the type of change needed (new feature, bug fix, refactor, enhancement).\n\n2. **Map the Relevant Architecture**: \n - Search for files, modules, and directories related to the problem domain\n - Read the key source files thoroughly \u2014 do not skim\n - Trace code paths: how does data flow through the relevant parts of the system?\n - Identify controller -> helper -> service -> model chains if applicable\n\n3. **Identify Extension Points**:\n - What existing code can be reused or extended?\n - What patterns does the codebase already use for similar functionality?\n - Are there helper functions, utilities, or base classes that should be leveraged?\n - Are there configuration files, metadata definitions, or templates that need modification?\n\n4. **Identify Constraints**:\n - What conventions does the project follow? (Check CLAUDE.md, README, existing patterns)\n - What testing patterns are used?\n - Are there ES5 limitations, specific framework patterns, or platform constraints?\n\n5. **Catalog Your Findings**: Keep mental notes of every relevant file path, function name, pattern, and architectural decision you discover. You will reference these in the ticket.\n\n### Research Depth Guidelines\n- Read at least 5-15 relevant source files in full, more if the problem is complex\n- Follow import chains to understand dependencies\n- Check test files to understand expected behaviors and testing patterns\n- Review configuration and metadata files if relevant\n- Search for TODO comments, known limitations, or related existing issues in the code\n\n## Phase 2: Write the Jira Ticket\n\nAfter completing research, create a new markdown file with the ticket. Use the naming convention `tickets/TICKET-<short-descriptive-name>.md`. If the `tickets/` directory does not exist, create it.\n\n### Ticket Structure\n\nThe markdown file MUST contain exactly these sections:\n\n```markdown\n# [Concise Title Describing the Task]\n\n## Summary\n\n[2-4 sentences describing what this task is about, why it matters, and the high-level approach. Be specific \u2014 reference the actual system components involved.]\n\n## Requirements\n\n[Numbered list of specific, actionable requirements. Each requirement should be a clear unit of work.]\n\n1. **[Requirement Title]**: [Description of what needs to be done.]\n - *Relevant code*: `path/to/file.js` \u2014 `functionName()` [brief note on how this code relates]\n - *Relevant code*: `path/to/other/file.js` \u2014 [brief note]\n\n2. **[Requirement Title]**: [Description]\n - *Relevant code*: ...\n\n[Continue for all requirements]\n\n## Acceptance Criteria\n\n[Bullet list. Each criterion is a testable, verifiable condition.]\n\n- [Specific, testable criterion]\n- [Another criterion]\n- [Continue as needed]\n\n## Materials & Access\n\n[Trailing audit-trail section \u2014 always the LAST section of the draft. Inventory every material the ticket references, grouped by source. Use monospace backticks for file paths and other technical provenance. Redact any embedded secrets.]\n\n### Reachable Local Files\n\n- `path/to/local/file.ext` \u2014 [what it is; will be gathered and attached post-create]\n\n### External/Auth-Gated Links\n\n- [Name or purpose] \u2014 `https://example.com/...` (record-only; external/auth-gated)\n\n### Design/UI Comps (Fetchable)\n\n- `attachment_id: 10421` \u2014 `checkout-comp.png` (`image/png`); fetch via the Jira attachment download capability into a worktree `file_path` at implementation time.\n\n### Binary/Image Materials (Record-Only)\n\n- `path/to/screenshot.png` \u2014 [sanitized location/access note; not attached]\n```\n\n### Writing Guidelines\n\n**Summary**:\n- Be concrete, not abstract. Name the actual components, cartridges, or subsystems involved.\n- State the "why" \u2014 what problem does this solve or what value does it add?\n- Mention the general technical approach if it\'s clear from the research.\n\n**Requirements**:\n- Each requirement should represent a logical unit of work\n- Order requirements in a logical implementation sequence when possible\n- ALWAYS cite relevant existing files and functions when they exist. Use exact file paths relative to the project root.\n- Explain HOW the existing code relates: "extend this function", "follow this pattern", "reuse this helper", "modify this configuration"\n- If a requirement involves creating new files, suggest where they should live based on existing project structure conventions\n- Be specific about what needs to change vs. what needs to be created new\n- Include requirements for tests, documentation, and configuration/metadata changes if applicable\n\n**Acceptance Criteria**:\n- Every criterion must be independently verifiable\n- Cover functional requirements, edge cases, testing, and non-functional requirements\n- Include criteria for backwards compatibility if relevant\n- Include criteria for test coverage\n- Use plain `-` bullets (Jira\'s ADF has no native checkbox, so `- [ ]` renders as literal text)\n- **Design/UI tickets**: whenever the ticket references or attaches a design comp (mockup, wireframe, or design/UI reference), ALWAYS include an explicit **visual-fidelity acceptance criterion**. Word it so the implementing agent must fetch/open the comp by its `attachment_id` or path and verify **class-appropriate** visual fidelity against it \u2014 strict pixel/visual match only for a full comp; layout-only for a wireframe; current-state-plus-delta for an annotated screenshot; the repo design-system floor otherwise. Do not settle for inert "record-only" prose that the implementing agent cannot act on.\n\n**Materials Completeness Inventory**:\n- After the draft is written, INVENTORY every material the ticket references: local file paths, URLs/links, named docs/designs, screenshots, and specs. This pass only INVENTORIES and RECORDS \u2014 it does NOT attach anything. The actual attachment of reachable local files happens post-create (after the Jira `ticket_key` exists) via a separate gather-and-attach step.\n- Classify each material by source using a scheme-based rule (no network probe required):\n - **Local filesystem paths** named in the ticket body are the only **low-risk** materials \u2014 eligible to be gathered and attached post-create.\n - Every **`http(s)` URI is external/auth-gated** \u2014 regardless of whether the user explicitly linked it (an explicitly-linked Confluence or Google Doc URL is still external/auth-gated) \u2014 and is **record-only** here.\n - **Binary/image materials** (ordinary screenshots, PDFs, and unrelated binaries) are **record-only** \u2014 document them with sanitized location/access notes; do NOT attempt to attach them.\n - **Design/UI comps** (a mockup, wireframe, or design reference for a design/UI ticket) are the exception to record-only: when the comp has an `attachment_id`, local path, or other executable fetch path, record it as a **fetchable reference** so the implementing agent can download it into its worktree and open it. For a Jira attachment comp, record its `attachment_id`, filename, and MIME type when known, plus a note that the executor should use the Jira attachment download capability to save it to a worktree `file_path`. Ordinary screenshots/PDFs/unrelated binaries with no fetch path stay record-only.\n- Write the trailing `## Materials & Access` section (the LAST section of the draft) grouping items under the sub-headings *Reachable Local Files*, *External/Auth-Gated Links*, *Design/UI Comps (Fetchable)* (only when a fetchable design/UI comp exists), and *Binary/Image Materials (Record-Only)*, using bulleted lists. Use monospace formatting (backticks) for technical provenance such as file paths.\n- **Redact secrets before writing anything**: before writing any URL or access note, sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. A location/access note must NEVER expose a plaintext secret.\n\n### Regression Completeness Pass (Gated)\n\nAfter the draft (including its `## Materials & Access` section) is written, run this pass. It is a non-blocking, **warn-not-halt** completeness check \u2014 it never blocks or fails ticket creation, and it never modifies the Requirements or Acceptance Criteria text directly.\n\n1. **Check the gate first.** Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `enable_regression_checks`. If the tool returns an error, `null`, or any value other than the literal string `"true"`, **skip this entire pass** \u2014 the draft is produced exactly as it would be without this section (byte-for-byte unchanged). The recommended default for this flag is OFF (unset) for safe rollout; only proceed past this step when it is explicitly `"true"`.\n\n2. **Derive the touched-symbol set.** From the draft\'s Requirements and *Relevant code* citations (or, if the ticket references an existing diff/PR, that diff/PR), extract the specific function/class/symbol names the proposed change touches.\n\n3. **Run the deterministic core.** Execute:\n ```bash\n npx -y @bridge_gpt/mcp-server regression-check --mode lightweight --json --symbols <derived,symbol,names>\n ```\n This is the same subcommand the standalone `regression-reviewer` agent and `regression-check` command use \u2014 do not hand-roll your own `ast-grep`/`ripgrep` discovery.\n\n4. **Fail-open on a degraded or failed run.** If the command errors, or `summary.degraded_flags` is non-empty, record that the pass ran degraded (or could not run) and proceed \u2014 the draft is still produced. Never halt ticket creation because this subcommand was unavailable.\n\n5. **Cross-check against Requirements + Acceptance Criteria.** Parse the JSON `findings` array (`symbol`, `call_sites.by_file`, `broad_mentions`). For each symbol, compare its real call-sites and broad mentions against what the draft\'s Requirements and Acceptance Criteria already cover. Flag any affected caller, migration, or contract (a file with a real call-site or an uninspected broad mention) that the criteria do NOT mention.\n\n6. **Record the flags \u2014 never rewrite Requirements/Acceptance Criteria.** Append a `[WARNING]` block immediately before the `## Materials & Access` section, listing each flagged item:\n ```markdown\n ## Regression Completeness Notes\n\n [WARNING] The following systems were not explicitly addressed in the Requirements or Acceptance Criteria above:\n - `path/to/affected_caller.py` \u2014 calls `changed_symbol` (N real call-sites); not mentioned in Requirements\n - `path/to/config.yml` \u2014 broad mention of `changed_symbol`; verify this reference is unaffected\n\n Degraded: [list summary.degraded_flags, or "none \u2014 full structural analysis ran"]\n ```\n If no flags were raised and the run was not degraded, write a single line instead: `Regression completeness pass: no unaddressed systems found.` If the run was degraded with zero findings either way, state that explicitly rather than implying a clean pass.\n\n### Output Formatting (Jira upload)\n\nThe ticket is uploaded to Jira, which converts the Markdown to Atlassian Document Format (ADF) and hard-caps the description at **32,767 characters**. Keep the output clean and within budget:\n\n- **Length**: aim for under ~30,000 characters. If the scope genuinely needs more, split into a parent ticket plus sub-tickets rather than one oversized ticket.\n- **Acceptance Criteria**: plain `-` bullets, not `- [ ]` (ADF has no native checkbox).\n- **No images**: do not embed images or use relative image links. This "No images" rule applies strictly to inline images in the description body; it does NOT restrict the attachments produced by the Materials Completeness Inventory / gather-and-attach pass, nor does it forbid recording a fetchable design/UI comp reference (its `attachment_id` or path).\n- **No empty headings**: every heading must have text on its line.\n- **Placeholders**: prefer `{placeholder}` over `<placeholder>`.\n\n## Quality Standards\n\n- **No vague language**: Replace "should handle errors properly" with "should catch LLM provider timeouts and return a normalized error response with errorType \'TimeoutError\'"\n- **No assumptions without evidence**: Only reference code you actually read during research. If you\'re unsure about something, say so explicitly in the ticket.\n- **Appropriate scope**: The ticket should represent a coherent, deliverable unit of work. If the problem is too large, note that it may need to be broken into sub-tasks, but still write the parent ticket.\n- **Developer empathy**: Write as if the developer picking this up has general project knowledge but hasn\'t recently worked on this specific area. Give them enough context to get started quickly.\n\n## Important Reminders\n\n- Do NOT skip or abbreviate the research phase. The quality of the ticket depends entirely on the depth of your codebase understanding.\n- Do NOT make up file paths or function names. Only reference code you have actually found and read.\n- DO create the markdown file \u2014 do not just output the content to the chat. Write it to disk.\n- If the project has specific conventions (from CLAUDE.md or similar), ensure your ticket\'s requirements align with those conventions.\n'
14858
+ "body": '\nYou are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.\n\n## Your Mission\n\nGiven a problem description from the user, you will:\n1. Conduct thorough codebase research to understand the existing architecture, patterns, and relevant code\n2. Write a structured Jira ticket as a new markdown file that references specific files, functions, and patterns from the codebase\n\n## Phase 1: Deep Codebase Research\n\nThis is the most critical phase. You MUST spend significant time here before writing anything. Do NOT rush this phase.\n\n### Research Protocol\n\n1. **Understand the Problem Space**: Re-read the user\'s problem description carefully. Identify the domain, the affected areas, and the type of change needed (new feature, bug fix, refactor, enhancement).\n\n2. **Map the Relevant Architecture**: \n - Search for files, modules, and directories related to the problem domain\n - Read the key source files thoroughly \u2014 do not skim\n - Trace code paths: how does data flow through the relevant parts of the system?\n - Identify controller -> helper -> service -> model chains if applicable\n\n3. **Identify Extension Points**:\n - What existing code can be reused or extended?\n - What patterns does the codebase already use for similar functionality?\n - Are there helper functions, utilities, or base classes that should be leveraged?\n - Are there configuration files, metadata definitions, or templates that need modification?\n\n4. **Identify Constraints**:\n - What conventions does the project follow? (Check CLAUDE.md, README, existing patterns)\n - What testing patterns are used?\n - Are there ES5 limitations, specific framework patterns, or platform constraints?\n\n5. **Catalog Your Findings**: Keep mental notes of every relevant file path, function name, pattern, and architectural decision you discover. You will reference these in the ticket.\n\n### Research Depth Guidelines\n- Read at least 5-15 relevant source files in full, more if the problem is complex\n- Follow import chains to understand dependencies\n- Check test files to understand expected behaviors and testing patterns\n- Review configuration and metadata files if relevant\n- Search for TODO comments, known limitations, or related existing issues in the code\n\n### Consuming a Comp\u2192Codebase Map (optional upstream input)\n\nYou may be handed a precomputed comp\u2192codebase map (`comp-analysis.json`) produced by an **upstream orchestrating vision step** (the recipe\'s `comp-analysis.md` step, or the `/write-ticket` Stage 0.5 pre-draft pass). That upstream step is a frontier vision model that already opened the design comp, classified it, and mapped its regions to concrete existing code. You remain **text-only**: you **must not open images**, embed images, download attachments, or perform any vision analysis yourself \u2014 you only read the JSON map as focused research input.\n\n- **When the map is missing or has `applicable: false`** (a backend-only request, a no-comp request, a non-design request, or a degraded/unreadable comp): **ignore the artifact entirely**. Do NOT mention comp analysis, design comps, visual fidelity, map artifacts, or image-derived requirements at all \u2014 unless the user\'s original request independently requires those materials. A backend-only or no-comp ticket must read exactly as it would with no map present.\n- **When the map has `applicable: true`**: read it in full before drafting and treat it as authoritative, focused research. Before citing any file the map names (component, template, token, or route), **inspect/read that concrete file yourself** \u2014 the standing rule that you do not make up file paths, function names, components, tokens, or routes still applies to map-sourced references.\n- **Class-appropriate depth** (mirror the map\'s `fidelity_classification.class`, the same shared taxonomy the downstream final plan reviewer uses):\n - `full comp` (confident) \u2192 you may write exact component/template/token/route Requirements.\n - `wireframe` \u2192 write layout/structure Requirements only; defer color, type, spacing, and component polish to the repo design system, not the wireframe.\n - `annotated-screenshot-of-existing-UI` \u2192 write delta-only Requirements (change only the annotated region; preserve the rest).\n - `unknown` / low confidence \u2192 use the design-system floor rather than pixel-exact Requirements.\n\n Hard rule: exact/strict mapping depth is used ONLY for a confidently-classified full comp. Fail toward the design system, never toward reproducing an ambiguous image.\n\n## Phase 2: Write the Jira Ticket\n\nAfter completing research, create a new markdown file with the ticket. Use the naming convention `tickets/TICKET-<short-descriptive-name>.md`. If the `tickets/` directory does not exist, create it.\n\n### Ticket Structure\n\nThe markdown file MUST contain exactly these sections:\n\n```markdown\n# [Concise Title Describing the Task]\n\n## Summary\n\n[2-4 sentences describing what this task is about, why it matters, and the high-level approach. Be specific \u2014 reference the actual system components involved.]\n\n## Requirements\n\n[Numbered list of specific, actionable requirements. Each requirement should be a clear unit of work.]\n\n1. **[Requirement Title]**: [Description of what needs to be done.]\n - *Relevant code*: `path/to/file.js` \u2014 `functionName()` [brief note on how this code relates]\n - *Relevant code*: `path/to/other/file.js` \u2014 [brief note]\n\n2. **[Requirement Title]**: [Description]\n - *Relevant code*: ...\n\n[Continue for all requirements]\n\n## Acceptance Criteria\n\n[Bullet list. Each criterion is a testable, verifiable condition.]\n\n- [Specific, testable criterion]\n- [Another criterion]\n- [Continue as needed]\n\n## Materials & Access\n\n[Trailing audit-trail section \u2014 always the LAST section of the draft. Inventory every material the ticket references, grouped by source. Use monospace backticks for file paths and other technical provenance. Redact any embedded secrets.]\n\n### Reachable Local Files\n\n[Only files NOT tracked in version control. Do NOT list version-controlled code or in-repo docs here \u2014 those are already in the repo and are cited inline as *Relevant code*.]\n\n- `path/to/local/file.ext` \u2014 [what it is; not in version control; will be gathered and attached post-create]\n\n### External/Auth-Gated Links\n\n- [Name or purpose] \u2014 `https://example.com/...` (record-only; external/auth-gated)\n\n### Design/UI Comps (Fetchable)\n\n- `attachment_id: 10421` \u2014 `checkout-comp.png` (`image/png`); fetch via the Jira attachment download capability into a worktree `file_path` at implementation time.\n\n### Binary/Image Materials (Record-Only)\n\n- `path/to/screenshot.png` \u2014 [sanitized location/access note; not attached]\n```\n\n### Writing Guidelines\n\n**Summary**:\n- Be concrete, not abstract. Name the actual components, cartridges, or subsystems involved.\n- State the "why" \u2014 what problem does this solve or what value does it add?\n- Mention the general technical approach if it\'s clear from the research.\n\n**Requirements**:\n- Each requirement should represent a logical unit of work\n- Order requirements in a logical implementation sequence when possible\n- ALWAYS cite relevant existing files and functions when they exist. Use exact file paths relative to the project root.\n- Explain HOW the existing code relates: "extend this function", "follow this pattern", "reuse this helper", "modify this configuration"\n- If a requirement involves creating new files, suggest where they should live based on existing project structure conventions\n- Be specific about what needs to change vs. what needs to be created new\n- Include requirements for tests, documentation, and configuration/metadata changes if applicable\n- **Design/UI Requirements (when an `applicable: true` comp\u2192codebase map is provided)**: cite the mapped components, Jinja2 templates, CSS/SCSS tokens or design-system styles, and routes from the map with concrete phrasing \u2014 "reuse `X` component", "extend template `Y`", "use token/style `Z`", "wire route `R`" \u2014 so the ticket expresses HOW to realize the comp in code that already exists, not generic "match the comp" prose. Keep the depth class-appropriate per the map\'s classification.\n\n**Acceptance Criteria**:\n- Every criterion must be independently verifiable\n- Cover functional requirements, edge cases, testing, and non-functional requirements\n- Include criteria for backwards compatibility if relevant\n- Include criteria for test coverage\n- Use plain `-` bullets (Jira\'s ADF has no native checkbox, so `- [ ]` renders as literal text)\n- **Design/UI tickets**: whenever the ticket references or attaches a design comp (mockup, wireframe, or design/UI reference), ALWAYS include an explicit **visual-fidelity acceptance criterion**. Word it so the implementing agent must fetch/open the comp by its `attachment_id` or path and verify **class-appropriate** visual fidelity against it \u2014 strict pixel/visual match only for a full comp; layout-only for a wireframe; current-state-plus-delta for an annotated screenshot; the repo design-system floor otherwise. Do not settle for inert "record-only" prose that the implementing agent cannot act on. When an `applicable: true` comp\u2192codebase map (`comp-analysis.json`) is available, the criterion should reference BOTH the concrete comp source AND the comp\u2192codebase map, so the implementing agent verifies fidelity against the same components/tokens the Requirements already cite rather than a bare "match the comp".\n\n**Materials Completeness Inventory**:\n- After the draft is written, INVENTORY every material the ticket references: local file paths, URLs/links, named docs/designs, screenshots, and specs. This pass only INVENTORIES and RECORDS \u2014 it does NOT attach anything. The actual attachment of reachable local files happens post-create (after the Jira `ticket_key` exists) via a separate gather-and-attach step.\n- Classify each material by source using a scheme-based rule (no network probe required):\n - **Local filesystem paths** named in the ticket body are the only **low-risk** materials \u2014 but ONLY when the file is **not tracked in version control**. Before listing a local file as attachable, determine its VCS status by running `git ls-files --error-unmatch -- <path>` (exit code `0` means the file is tracked). A version-controlled file is **already available in the repository** \u2014 source code, in-repo docs, configs, and any other committed file \u2014 and **MUST NOT be attached**; it is cited inline as *Relevant code* in Requirements instead of being re-uploaded. Only local files that are **not tracked in version control** (external technical docs/specs, design comps, or generated artifacts a reviewer dropped locally \u2014 including files outside any repo, untracked, or gitignored) are eligible to be gathered and attached post-create. **Never upload code** or any file already in version control.\n - Every **`http(s)` URI is external/auth-gated** \u2014 regardless of whether the user explicitly linked it (an explicitly-linked Confluence or Google Doc URL is still external/auth-gated) \u2014 and is **record-only** here.\n - **Binary/image materials** (ordinary screenshots, PDFs, and unrelated binaries) are **record-only** \u2014 document them with sanitized location/access notes; do NOT attempt to attach them. This record-only rule does not apply to local design/UI comp images (see next bullet).\n - **Design/UI comps** (a mockup, wireframe, or design reference for a design/UI ticket) are the exception to record-only: when the comp has an `attachment_id`, local path, or other executable fetch path, record it as a **fetchable reference** so the implementing agent can download it into its worktree and open it. For a Jira attachment comp, record its `attachment_id`, filename, and MIME type when known, plus a note that the executor should use the Jira attachment download capability to save it to a worktree `file_path`. A **local design/UI comp image** \u2014 a reachable local file whose executable local path resolves and whose extension maps to an allowlisted image MIME type (`image/png`, `image/jpeg`, `image/webp`, `image/gif`) \u2014 is also recorded under *Design/UI Comps (Fetchable)* and is additionally eligible for post-create attachment through the allowlisted binary upload path (the gather-and-attach step uploads it, not just references it). An external/auth-gated design link or a Jira `attachment_id` reference on another ticket remains fetchable/reference material for implementation-time download, not a local re-upload target. Ordinary screenshots/PDFs/unrelated binaries with no fetch path and no design relevance stay record-only.\n - **Comp\u2192codebase map** (`comp-analysis.json`): when the final ticket references the map, inventory it as a **reachable local text file** under *Reachable Local Files* (it is a low-risk local JSON text artifact, gatherable like any other local file \u2014 distinct from the design-comp material exception above, which governs the fetchable image itself). The version-control gate still applies: attach it only when it is **not tracked in version control** (a generated artifact normally is not); if it happens to be committed, it is already available in the repo and is not re-uploaded.\n- Write the trailing `## Materials & Access` section (the LAST section of the draft) grouping items under the sub-headings *Reachable Local Files*, *External/Auth-Gated Links*, *Design/UI Comps (Fetchable)* (only when a fetchable design/UI comp exists), and *Binary/Image Materials (Record-Only)*, using bulleted lists. Use monospace formatting (backticks) for technical provenance such as file paths.\n- **Redact secrets before writing anything**: before writing any URL or access note, sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. A location/access note must NEVER expose a plaintext secret.\n\n### Regression Completeness Pass (Gated)\n\nAfter the draft (including its `## Materials & Access` section) is written, run this pass. It is a non-blocking, **warn-not-halt** completeness check \u2014 it never blocks or fails ticket creation, and it never modifies the Requirements or Acceptance Criteria text directly.\n\n1. **Check the gate first.** Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `enable_regression_checks`. If the tool returns an error, `null`, or any value other than the literal string `"true"`, **skip this entire pass** \u2014 the draft is produced exactly as it would be without this section (byte-for-byte unchanged). The recommended default for this flag is OFF (unset) for safe rollout; only proceed past this step when it is explicitly `"true"`.\n\n2. **Derive the touched-symbol set.** From the draft\'s Requirements and *Relevant code* citations (or, if the ticket references an existing diff/PR, that diff/PR), extract the specific function/class/symbol names the proposed change touches.\n\n3. **Run the deterministic core.** Execute:\n ```bash\n npx -y @bridge_gpt/mcp-server regression-check --mode lightweight --json --symbols <derived,symbol,names>\n ```\n This is the same subcommand the standalone `regression-reviewer` agent and `regression-check` command use \u2014 do not hand-roll your own `ast-grep`/`ripgrep` discovery.\n\n4. **Fail-open on a degraded or failed run.** If the command errors, or `summary.degraded_flags` is non-empty, record that the pass ran degraded (or could not run) and proceed \u2014 the draft is still produced. Never halt ticket creation because this subcommand was unavailable.\n\n5. **Cross-check against Requirements + Acceptance Criteria.** Parse the JSON `findings` array (`symbol`, `call_sites.by_file`, `broad_mentions`). For each symbol, compare its real call-sites and broad mentions against what the draft\'s Requirements and Acceptance Criteria already cover. Flag any affected caller, migration, or contract (a file with a real call-site or an uninspected broad mention) that the criteria do NOT mention.\n\n6. **Record the flags \u2014 never rewrite Requirements/Acceptance Criteria.** Append a `[WARNING]` block immediately before the `## Materials & Access` section, listing each flagged item:\n ```markdown\n ## Regression Completeness Notes\n\n [WARNING] The following systems were not explicitly addressed in the Requirements or Acceptance Criteria above:\n - `path/to/affected_caller.py` \u2014 calls `changed_symbol` (N real call-sites); not mentioned in Requirements\n - `path/to/config.yml` \u2014 broad mention of `changed_symbol`; verify this reference is unaffected\n\n Degraded: [list summary.degraded_flags, or "none \u2014 full structural analysis ran"]\n ```\n If no flags were raised and the run was not degraded, write a single line instead: `Regression completeness pass: no unaddressed systems found.` If the run was degraded with zero findings either way, state that explicitly rather than implying a clean pass.\n\n### Output Formatting (Jira upload)\n\nThe ticket is uploaded to Jira, which converts the Markdown to Atlassian Document Format (ADF) and hard-caps the description at **32,767 characters**. Keep the output clean and within budget:\n\n- **Length**: aim for under ~30,000 characters. If the scope genuinely needs more, split into a parent ticket plus sub-tickets rather than one oversized ticket.\n- **Acceptance Criteria**: plain `-` bullets, not `- [ ]` (ADF has no native checkbox).\n- **No images**: do not embed images or use relative image links. This "No images" rule applies strictly to inline images in the description body; it does NOT restrict the attachments produced by the Materials Completeness Inventory / gather-and-attach pass, nor does it forbid recording a fetchable design/UI comp reference (its `attachment_id` or path).\n- **No empty headings**: every heading must have text on its line.\n- **Placeholders**: prefer `{placeholder}` over `<placeholder>`.\n\n## Quality Standards\n\n- **No vague language**: Replace "should handle errors properly" with "should catch LLM provider timeouts and return a normalized error response with errorType \'TimeoutError\'"\n- **No assumptions without evidence**: Only reference code you actually read during research. If you\'re unsure about something, say so explicitly in the ticket.\n- **Appropriate scope**: The ticket should represent a coherent, deliverable unit of work. If the problem is too large, note that it may need to be broken into sub-tasks, but still write the parent ticket.\n- **Developer empathy**: Write as if the developer picking this up has general project knowledge but hasn\'t recently worked on this specific area. Give them enough context to get started quickly.\n\n## Important Reminders\n\n- Do NOT skip or abbreviate the research phase. The quality of the ticket depends entirely on the depth of your codebase understanding.\n- Do NOT make up file paths or function names. Only reference code you have actually found and read.\n- DO create the markdown file \u2014 do not just output the content to the chat. Write it to disk.\n- If the project has specific conventions (from CLAUDE.md or similar), ensure your ticket\'s requirements align with those conventions.\n'
14666
14859
  },
14667
14860
  "refactor-reviewer": {
14668
14861
  "frontmatter": {
@@ -17150,6 +17343,9 @@ init_bridge_api_client();
17150
17343
  function positiveIntOrNull(value) {
17151
17344
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
17152
17345
  }
17346
+ function nonNegativeIntOrUndefined(value) {
17347
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
17348
+ }
17153
17349
  function asString(value) {
17154
17350
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
17155
17351
  }
@@ -17166,9 +17362,11 @@ function readMergeJobPayloadFields(job) {
17166
17362
  const method = resolveLocalMergeMethod(payload.method);
17167
17363
  const requiredChecks = Array.isArray(payload.required_checks) ? payload.required_checks.filter((c) => typeof c === "string") : [];
17168
17364
  const actionKey = asString(payload.action_key) ?? `merge:pr-${prNumber}:${expectedHeadSha}`;
17365
+ const ciWaitTimeoutMs = nonNegativeIntOrUndefined(payload.ci_wait_timeout_ms);
17366
+ const ciWaitPollIntervalMs = nonNegativeIntOrUndefined(payload.ci_wait_poll_interval_ms);
17169
17367
  return {
17170
17368
  ok: true,
17171
- fields: { prNumber, expectedHeadSha, method, requiredChecks, actionKey }
17369
+ fields: { prNumber, expectedHeadSha, method, requiredChecks, actionKey, ciWaitTimeoutMs, ciWaitPollIntervalMs }
17172
17370
  };
17173
17371
  }
17174
17372
  function buildConductorMergeRequestForExecutorJob(fields, repoName) {
@@ -17208,10 +17406,32 @@ function buildMergeJobResult(response, fields) {
17208
17406
  }
17209
17407
  return result;
17210
17408
  }
17409
+ var MERGE_RETRYABLE = "MergeRetryable";
17410
+ var MERGE_CONFLICT = "MergeConflict";
17411
+ var MERGE_FAILED = "MergeFailed";
17412
+ var RETRYABLE_MERGE_REASONS = /* @__PURE__ */ new Set([
17413
+ "gh_pr_view_timeout",
17414
+ "gh_pr_view_failed",
17415
+ "gh_pr_view_unparseable",
17416
+ "ci_poll_failed",
17417
+ "ci_not_green",
17418
+ "gh_merge_timeout",
17419
+ "gh_merge_failed",
17420
+ "merge_aborted"
17421
+ ]);
17422
+ function classifyMergeFailureErrorKind(response) {
17423
+ const hasConflictEvent = response.ledger_events.some((ev) => ev.type === "merge.conflict");
17424
+ const reason = asString(response.reason ?? void 0);
17425
+ if (hasConflictEvent || reason === "gh_merge_conflict") return MERGE_CONFLICT;
17426
+ if (reason && (reason.startsWith("ci_poll_") || RETRYABLE_MERGE_REASONS.has(reason))) {
17427
+ return MERGE_RETRYABLE;
17428
+ }
17429
+ return MERGE_FAILED;
17430
+ }
17211
17431
  function buildMergeJobFailure(response) {
17212
17432
  const reason = asString(response.reason ?? void 0) ?? response.status;
17213
17433
  return {
17214
- error_kind: "MergeFailed",
17434
+ error_kind: classifyMergeFailureErrorKind(response),
17215
17435
  error_message: secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),
17216
17436
  classification: "crashed"
17217
17437
  };
@@ -17231,7 +17451,14 @@ async function runExecutorMergeJob(job, seams) {
17231
17451
  const fields = resolution.fields;
17232
17452
  const request = buildConductorMergeRequestForExecutorJob(fields, seams.access.repoName);
17233
17453
  const make = seams.makeExecutor ?? makeLocalMergeExecutor;
17234
- const executor = make({ method: fields.method }, seams.localMergeDeps);
17454
+ const executor = make(
17455
+ {
17456
+ method: fields.method,
17457
+ ciWaitTimeoutMs: fields.ciWaitTimeoutMs,
17458
+ ciWaitPollIntervalMs: fields.ciWaitPollIntervalMs
17459
+ },
17460
+ seams.localMergeDeps
17461
+ );
17235
17462
  let response;
17236
17463
  try {
17237
17464
  response = await executor(seams.access, request);
@@ -17250,8 +17477,8 @@ async function runExecutorMergeJob(job, seams) {
17250
17477
  }
17251
17478
  return { ok: false, failure: buildMergeJobFailure(response) };
17252
17479
  }
17253
- function buildDefaultMergeLocalDeps(deps) {
17254
- return { env: deps.env };
17480
+ function buildDefaultMergeLocalDeps(deps, signal) {
17481
+ return { env: deps.env, signal };
17255
17482
  }
17256
17483
 
17257
17484
  // src/executor/observation.ts
@@ -18110,6 +18337,17 @@ function isImplementationStyleJobType(jobType) {
18110
18337
  }
18111
18338
 
18112
18339
  // src/executor/worker-finalization.ts
18340
+ var DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS = 3;
18341
+ var DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS = 500;
18342
+ function sleepMs(ms) {
18343
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
18344
+ }
18345
+ function normalizeAttempts(value) {
18346
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS;
18347
+ }
18348
+ function normalizeRetryDelay(value) {
18349
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS;
18350
+ }
18113
18351
  function extractPrUrl(result) {
18114
18352
  const raw = result.pr_url;
18115
18353
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
@@ -18117,15 +18355,41 @@ function extractPrUrl(result) {
18117
18355
  function normalizeBranchRef(branch) {
18118
18356
  return branch.startsWith("refs/heads/") ? branch : `refs/heads/${branch}`;
18119
18357
  }
18358
+ function parseLsRemoteHeadSha(stdout, expectedRef) {
18359
+ for (const line of stdout.split("\n")) {
18360
+ const trimmed = line.trim();
18361
+ if (trimmed.length === 0) continue;
18362
+ const parts = trimmed.split(/\s+/);
18363
+ if (parts.length < 2) continue;
18364
+ const [sha, ref] = parts;
18365
+ if (ref === expectedRef) {
18366
+ const normalized = sha.trim().toLowerCase();
18367
+ return normalized.length > 0 ? normalized : null;
18368
+ }
18369
+ }
18370
+ return null;
18371
+ }
18120
18372
  async function resolveOriginBranchSha(runCommand, worktreePath, branch) {
18373
+ const normalizedRef = normalizeBranchRef(branch);
18121
18374
  const result = await runCommand(
18122
18375
  "git",
18123
- ["ls-remote", "--exit-code", "--heads", "origin", normalizeBranchRef(branch)],
18376
+ ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef],
18124
18377
  { cwd: worktreePath }
18125
18378
  );
18126
18379
  if (result.exitCode !== 0) return null;
18127
- const sha = result.stdout.trim().split(/\s+/)[0];
18128
- return sha && sha.length > 0 ? sha : null;
18380
+ return parseLsRemoteHeadSha(result.stdout, normalizedRef);
18381
+ }
18382
+ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, branch, trimmedHeadSha, attempts, retryDelayMs, sleep3) {
18383
+ const total = Math.max(1, attempts);
18384
+ let remoteSha = null;
18385
+ for (let attempt = 0; attempt < total; attempt++) {
18386
+ remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, branch);
18387
+ if (remoteSha !== null) {
18388
+ if (!trimmedHeadSha || remoteSha === trimmedHeadSha) return remoteSha;
18389
+ }
18390
+ if (attempt < total - 1) await sleep3(retryDelayMs);
18391
+ }
18392
+ return remoteSha;
18129
18393
  }
18130
18394
  function missingBranchAndPrFailure(job, detail) {
18131
18395
  const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
@@ -18153,9 +18417,21 @@ async function validateWorkerFinalization(input) {
18153
18417
  )
18154
18418
  };
18155
18419
  }
18420
+ const trimmedHeadSha = typeof headSha === "string" ? headSha.trim().toLowerCase() : "";
18421
+ const attempts = normalizeAttempts(input.originResolveAttempts);
18422
+ const retryDelayMs = normalizeRetryDelay(input.originResolveRetryDelayMs);
18423
+ const sleep3 = input.sleep ?? sleepMs;
18156
18424
  let remoteSha;
18157
18425
  try {
18158
- remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, trimmedBranch);
18426
+ remoteSha = await resolveOriginBranchShaForFinalization(
18427
+ runCommand,
18428
+ worktreePath,
18429
+ trimmedBranch,
18430
+ trimmedHeadSha,
18431
+ attempts,
18432
+ retryDelayMs,
18433
+ sleep3
18434
+ );
18159
18435
  } catch (err) {
18160
18436
  return {
18161
18437
  ok: false,
@@ -18174,7 +18450,6 @@ async function validateWorkerFinalization(input) {
18174
18450
  )
18175
18451
  };
18176
18452
  }
18177
- const trimmedHeadSha = typeof headSha === "string" ? headSha.trim() : "";
18178
18453
  if (trimmedHeadSha && remoteSha !== trimmedHeadSha) {
18179
18454
  return {
18180
18455
  ok: false,
@@ -18349,7 +18624,7 @@ function buildJobLogRegistryDeps(deps) {
18349
18624
  platform: deps.platform
18350
18625
  };
18351
18626
  }
18352
- async function defaultRunMergeForClaimed(job, deps, _options) {
18627
+ async function defaultRunMergeForClaimed(job, deps, _options, controls) {
18353
18628
  const accessResult = await buildConductorMergeAccessForExecutorJob({
18354
18629
  env: deps.env,
18355
18630
  cwd: deps.cwd,
@@ -18362,7 +18637,7 @@ async function defaultRunMergeForClaimed(job, deps, _options) {
18362
18637
  return {
18363
18638
  ok: false,
18364
18639
  failure: {
18365
- error_kind: "MergeAccessUnavailable",
18640
+ error_kind: MERGE_RETRYABLE,
18366
18641
  error_message: accessResult.error,
18367
18642
  classification: "crashed"
18368
18643
  }
@@ -18370,7 +18645,9 @@ async function defaultRunMergeForClaimed(job, deps, _options) {
18370
18645
  }
18371
18646
  return runExecutorMergeJob(job, {
18372
18647
  access: accessResult.access,
18373
- localMergeDeps: buildDefaultMergeLocalDeps(deps)
18648
+ // Thread the overall-timeout abort signal into local merge deps WITHOUT
18649
+ // changing the local `gh` credential model (the signal is not a credential).
18650
+ localMergeDeps: buildDefaultMergeLocalDeps(deps, controls?.signal)
18374
18651
  });
18375
18652
  }
18376
18653
  function createNoopProcess(deps, durationMs, message) {
@@ -18444,7 +18721,7 @@ async function runClaimedJob(job, httpClient, options, deps, _report, seams = {}
18444
18721
  { advisoryParserEnabled: options.advisoryParserEnabled }
18445
18722
  );
18446
18723
  if (job.job_type === "merge") {
18447
- return runMergeJob(job, httpClient, options, deps, ownership, seams);
18724
+ return runMergeJob(job, httpClient, options, deps, ownership, observation, seams);
18448
18725
  }
18449
18726
  if (job.job_type === "smoke") {
18450
18727
  return runSmokeJob(job, httpClient, options, deps, ownership, observation);
@@ -18459,18 +18736,123 @@ async function runClaimedJob(job, httpClient, options, deps, _report, seams = {}
18459
18736
  });
18460
18737
  return { status: "failed", reason: "unsupported_job_type" };
18461
18738
  }
18462
- async function runMergeJob(job, httpClient, options, deps, ownership, seams) {
18463
- const runMerge = seams.runMerge ?? defaultRunMergeForClaimed;
18739
+ function createMergeFlowProcess(startMerge) {
18740
+ const controller = new AbortController();
18464
18741
  let outcome;
18465
- try {
18466
- outcome = await runMerge(job, deps, options);
18467
- } catch (err) {
18742
+ let error;
18743
+ let done = false;
18744
+ let settleWait = () => {
18745
+ };
18746
+ const waitPromise = new Promise(
18747
+ (resolve2) => {
18748
+ settleWait = resolve2;
18749
+ }
18750
+ );
18751
+ startMerge(controller.signal).then(
18752
+ (result) => {
18753
+ outcome = result;
18754
+ if (!done) {
18755
+ done = true;
18756
+ settleWait({ exitCode: result.ok ? 0 : 1, signal: null });
18757
+ }
18758
+ },
18759
+ (err) => {
18760
+ error = err;
18761
+ if (!done) {
18762
+ done = true;
18763
+ settleWait({ exitCode: 1, signal: null });
18764
+ }
18765
+ }
18766
+ );
18767
+ const proc = {
18768
+ pid: void 0,
18769
+ stdout: null,
18770
+ stderr: null,
18771
+ async wait() {
18772
+ return waitPromise;
18773
+ },
18774
+ kill(signal) {
18775
+ if (done) return;
18776
+ controller.abort();
18777
+ done = true;
18778
+ settleWait({ exitCode: null, signal });
18779
+ }
18780
+ };
18781
+ return {
18782
+ proc,
18783
+ getOutcome: () => outcome,
18784
+ getError: () => error
18785
+ };
18786
+ }
18787
+ async function runMergeJob(job, httpClient, options, deps, ownership, observation, seams) {
18788
+ const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
18789
+ if (!timeout.ok) {
18790
+ await httpClient.fail(job, {
18791
+ error_kind: "ContractError.Timeout",
18792
+ error_message: timeout.error,
18793
+ classification: "crashed"
18794
+ });
18795
+ return { status: "failed", reason: "timeout_contract" };
18796
+ }
18797
+ const timeoutSeconds = timeout.timeoutSeconds;
18798
+ const runMerge = seams.runMerge ?? defaultRunMergeForClaimed;
18799
+ const flow = createMergeFlowProcess((signal) => runMerge(job, deps, options, { signal }));
18800
+ const procResult = await superviseProcess({
18801
+ job,
18802
+ httpClient,
18803
+ options,
18804
+ deps,
18805
+ ownership,
18806
+ observation,
18807
+ proc: flow.proc,
18808
+ timeoutSeconds,
18809
+ collectTelemetry: async () => ({})
18810
+ });
18811
+ if (ownership.abandoned) {
18812
+ return { status: "abandoned", reason: ownership.abandonReason };
18813
+ }
18814
+ if (procResult.classification === "timeout") {
18468
18815
  const terminal2 = await sendTerminalMutationWithRetry({
18469
18816
  kind: "fail",
18470
18817
  send: () => httpClient.fail(job, {
18471
- error_kind: "MergeError",
18472
- error_message: secretFreeErrorMessage(err),
18473
- classification: "crashed"
18818
+ error_kind: MERGE_RETRYABLE,
18819
+ error_message: `merge flow timed out after ${timeoutSeconds} seconds`,
18820
+ classification: "timeout",
18821
+ telemetry: observation.snapshot()
18822
+ }),
18823
+ deps,
18824
+ options,
18825
+ ownership,
18826
+ log: deps.log
18827
+ });
18828
+ return terminalToRunResult(terminal2, "failed");
18829
+ }
18830
+ const flowError = flow.getError();
18831
+ if (flowError !== void 0) {
18832
+ const terminal2 = await sendTerminalMutationWithRetry({
18833
+ kind: "fail",
18834
+ send: () => httpClient.fail(job, {
18835
+ error_kind: MERGE_FAILED,
18836
+ error_message: secretFreeErrorMessage(flowError),
18837
+ classification: "crashed",
18838
+ telemetry: observation.snapshot()
18839
+ }),
18840
+ deps,
18841
+ options,
18842
+ ownership,
18843
+ log: deps.log
18844
+ });
18845
+ return terminalToRunResult(terminal2, "failed");
18846
+ }
18847
+ const outcome = flow.getOutcome();
18848
+ if (outcome === void 0) {
18849
+ const terminal2 = await sendTerminalMutationWithRetry({
18850
+ kind: "fail",
18851
+ send: () => httpClient.fail(job, {
18852
+ error_kind: MERGE_FAILED,
18853
+ error_message: "merge flow completed without an outcome",
18854
+ classification: "crashed",
18855
+ telemetry: observation.snapshot()
18474
18856
  }),
18475
18857
  deps,
18476
18858
  options,
@@ -18484,7 +18866,8 @@ async function runMergeJob(job, httpClient, options, deps, ownership, seams) {
18484
18866
  job_type: "merge",
18485
18867
  exit_code: 0,
18486
18868
  classification: "clean_exit",
18487
- result: outcome.result
18869
+ result: outcome.result,
18870
+ telemetry: observation.snapshot()
18488
18871
  };
18489
18872
  const terminal2 = await sendTerminalMutationWithRetry({
18490
18873
  kind: "complete",
@@ -18498,7 +18881,7 @@ async function runMergeJob(job, httpClient, options, deps, ownership, seams) {
18498
18881
  }
18499
18882
  const terminal = await sendTerminalMutationWithRetry({
18500
18883
  kind: "fail",
18501
- send: () => httpClient.fail(job, outcome.failure),
18884
+ send: () => httpClient.fail(job, { ...outcome.failure, telemetry: observation.snapshot() }),
18502
18885
  deps,
18503
18886
  options,
18504
18887
  ownership,
@@ -19033,7 +19416,7 @@ async function runExecutor(options, deps, httpClient, seams = {}) {
19033
19416
  }
19034
19417
 
19035
19418
  // src/executor/watch-cli.ts
19036
- import { spawn as spawn4 } from "node:child_process";
19419
+ import { spawn as spawn5 } from "node:child_process";
19037
19420
  import { access } from "node:fs/promises";
19038
19421
  import os10 from "node:os";
19039
19422
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
@@ -19070,7 +19453,7 @@ function parseExecutorWatchArgs(argv) {
19070
19453
  }
19071
19454
  return { kind: "ok", jobId: Number(raw) };
19072
19455
  }
19073
- function spawnTailFollow(logPath, spawnImpl = spawn4) {
19456
+ function spawnTailFollow(logPath, spawnImpl = spawn5) {
19074
19457
  return new Promise((resolve2) => {
19075
19458
  const child = spawnImpl("tail", ["-f", logPath], { stdio: "inherit" });
19076
19459
  child.on("close", (code) => resolve2(code ?? 0));
@@ -20014,7 +20397,7 @@ async function runRegressionCheckCli(argv, overrides = {}) {
20014
20397
 
20015
20398
  // src/install-bridge.ts
20016
20399
  import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7, stat as stat7, rename, chmod, unlink } from "fs/promises";
20017
- import { spawn as spawn5 } from "child_process";
20400
+ import { spawn as spawn6 } from "child_process";
20018
20401
  import os12 from "os";
20019
20402
  import path22 from "path";
20020
20403
  import readline from "readline";
@@ -20171,7 +20554,7 @@ function spawnPrewarmDefault(command, args, env) {
20171
20554
  const sanitizedEnv = { ...env };
20172
20555
  delete sanitizedEnv.BAPI_API_KEY;
20173
20556
  try {
20174
- const child = spawn5(command, args, {
20557
+ const child = spawn6(command, args, {
20175
20558
  shell: false,
20176
20559
  stdio: "ignore",
20177
20560
  timeout: 6e4,
@@ -20605,7 +20988,7 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20605
20988
 
20606
20989
  // src/upgrade-cli.ts
20607
20990
  init_version_generated();
20608
- import { spawn as spawn6 } from "child_process";
20991
+ import { spawn as spawn7 } from "child_process";
20609
20992
  import { stat as stat8 } from "fs/promises";
20610
20993
  import path23 from "path";
20611
20994
  init_start_tickets();
@@ -20647,7 +21030,7 @@ async function runUpgradeCli(argv) {
20647
21030
  "--old-version",
20648
21031
  VERSION
20649
21032
  ];
20650
- const child = spawn6(npxCmd, childArgs, { stdio: "inherit", cwd });
21033
+ const child = spawn7(npxCmd, childArgs, { stdio: "inherit", cwd });
20651
21034
  child.on("close", (code) => resolve2(code ?? 0));
20652
21035
  child.on("error", (err) => {
20653
21036
  console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
@@ -20700,7 +21083,7 @@ async function runUpgradeCli(argv) {
20700
21083
  );
20701
21084
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
20702
21085
  await new Promise((resolve2) => {
20703
- const child = spawn6(npmCmd, ["uninstall", "@bridge_gpt/mcp-server"], {
21086
+ const child = spawn7(npmCmd, ["uninstall", "@bridge_gpt/mcp-server"], {
20704
21087
  stdio: "inherit",
20705
21088
  cwd
20706
21089
  });
@@ -21586,7 +21969,7 @@ function registerConductorTools(registerTool2) {
21586
21969
  }
21587
21970
 
21588
21971
  // src/sfcc/register.ts
21589
- import { z as z6 } from "zod";
21972
+ import { z as z13 } from "zod";
21590
21973
 
21591
21974
  // src/sfcc/config.ts
21592
21975
  var SFCC_VERSIONS = ["sfra", "pwakit", "sitegenesis", "storefrontnext", "hybrid"];
@@ -21701,6 +22084,46 @@ async function resolveSfccCredentials(explicitHostname, env = process.env, deps
21701
22084
  };
21702
22085
  }
21703
22086
 
22087
+ // src/sfcc/ocapi-write-faults.ts
22088
+ var KNOWN_WRITE_FAULTS = {
22089
+ 400: "MalformedKeyParameterException",
22090
+ 404: "AttributeDefinitionNotFoundException",
22091
+ 409: "IfMatchRequiredException",
22092
+ 412: "InvalidIfMatchException"
22093
+ };
22094
+ function extractOcapiFaultType(body) {
22095
+ if (body === null || typeof body !== "object") return void 0;
22096
+ const record = body;
22097
+ const fault = record.fault;
22098
+ if (typeof fault === "string" && fault.length > 0) return fault;
22099
+ if (fault !== null && typeof fault === "object") {
22100
+ const faultType = fault.type;
22101
+ if (typeof faultType === "string" && faultType.length > 0) return faultType;
22102
+ }
22103
+ const type = record.type;
22104
+ if (typeof type === "string" && type.length > 0) return type;
22105
+ return void 0;
22106
+ }
22107
+ function mapOcapiWriteFault(status, body) {
22108
+ const faultType = extractOcapiFaultType(body);
22109
+ const expected = KNOWN_WRITE_FAULTS[status];
22110
+ const known = expected !== void 0 && faultType === expected;
22111
+ return {
22112
+ status,
22113
+ faultType,
22114
+ known,
22115
+ errorCode: known ? expected : "OCAPI_WRITE_FAULT"
22116
+ };
22117
+ }
22118
+ function buildSyntheticIfMatchRequiredBody(path33) {
22119
+ return {
22120
+ fault: {
22121
+ type: "IfMatchRequiredException",
22122
+ message: `PATCH ${path33} requires an ETag (If-Match) captured from the GET round trip, but the GET response returned no ETag header. Cannot safely issue a conditional PATCH.`
22123
+ }
22124
+ };
22125
+ }
22126
+
21704
22127
  // src/sfcc/client.ts
21705
22128
  var tokenMutex = /* @__PURE__ */ new Map();
21706
22129
  var tokenCache = /* @__PURE__ */ new Map();
@@ -21746,26 +22169,87 @@ async function getAmToken(credentials) {
21746
22169
  function invalidateAmToken(credentials) {
21747
22170
  tokenCache.delete(credentials.hostname);
21748
22171
  }
21749
- async function ocapiGet(path33, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22172
+ function buildOcapiUrl(hostname, ocapiVersion, path33) {
22173
+ const baseUrl = `https://${hostname}/s/-/dw/data/${ocapiVersion}`;
22174
+ return `${baseUrl}${path33.startsWith("/") ? path33 : "/" + path33}`;
22175
+ }
22176
+ async function parseOcapiResponse(resp) {
22177
+ try {
22178
+ return await resp.json();
22179
+ } catch {
22180
+ return null;
22181
+ }
22182
+ }
22183
+ function captureOcapiHeaders(resp) {
22184
+ const headers = {};
22185
+ const respHeaders = resp.headers;
22186
+ if (respHeaders && typeof respHeaders.forEach === "function") {
22187
+ respHeaders.forEach((value, key) => {
22188
+ headers[key.toLowerCase()] = value;
22189
+ });
22190
+ return { headers, etag: respHeaders.get("etag") };
22191
+ }
22192
+ return { headers, etag: null };
22193
+ }
22194
+ function sleep2(ms) {
22195
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
22196
+ }
22197
+ var MAX_RETRY_AFTER_MS = 5e3;
22198
+ function parseRetryAfterMs(headerValue) {
22199
+ if (!headerValue) return void 0;
22200
+ const trimmed = headerValue.trim();
22201
+ if (trimmed === "") return void 0;
22202
+ if (/^\d+$/.test(trimmed)) {
22203
+ return Number(trimmed) * 1e3;
22204
+ }
22205
+ const dateMs = Date.parse(trimmed);
22206
+ if (!Number.isNaN(dateMs)) {
22207
+ const delta = dateMs - Date.now();
22208
+ return delta > 0 ? delta : 0;
22209
+ }
22210
+ return void 0;
22211
+ }
22212
+ var BACKOFF_SCHEDULE_MS = [250, 500, 1e3];
22213
+ async function fetchWith429Backoff(url, init) {
22214
+ let resp = await fetch(url, init);
22215
+ for (let attempt = 0; attempt < BACKOFF_SCHEDULE_MS.length; attempt++) {
22216
+ if (resp.status !== 429) return resp;
22217
+ const retryAfterHeader = resp.headers && typeof resp.headers.get === "function" ? resp.headers.get("retry-after") : null;
22218
+ const retryAfter = parseRetryAfterMs(retryAfterHeader);
22219
+ const backoff = retryAfter !== void 0 ? Math.min(retryAfter, MAX_RETRY_AFTER_MS) : BACKOFF_SCHEDULE_MS[attempt];
22220
+ await sleep2(backoff);
22221
+ resp = await fetch(url, init);
22222
+ }
22223
+ return resp;
22224
+ }
22225
+ async function ocapiRequest(method, path33, body, credentials, ocapiVersion, extraHeaders) {
21750
22226
  const doRequest = async () => {
21751
22227
  const token = await getAmToken(credentials);
21752
- const baseUrl = `https://${credentials.hostname}/s/-/dw/data/${ocapiVersion}`;
21753
- const url = `${baseUrl}${path33.startsWith("/") ? path33 : "/" + path33}`;
21754
- const resp = await fetch(url, {
21755
- method: "GET",
22228
+ const url = buildOcapiUrl(credentials.hostname, ocapiVersion, path33);
22229
+ const init = {
22230
+ method,
21756
22231
  headers: {
21757
22232
  Authorization: `Bearer ${token}`,
21758
- "Content-Type": "application/json"
22233
+ "Content-Type": "application/json",
22234
+ ...extraHeaders ?? {}
21759
22235
  }
21760
- });
22236
+ };
22237
+ if (body !== void 0) {
22238
+ init.body = JSON.stringify(body);
22239
+ }
22240
+ const resp = await fetchWith429Backoff(url, init);
21761
22241
  const status = resp.status;
21762
- let body = null;
21763
- try {
21764
- body = await resp.json();
21765
- } catch {
21766
- body = null;
22242
+ const respBody = await parseOcapiResponse(resp);
22243
+ const { headers, etag } = captureOcapiHeaders(resp);
22244
+ const result = { ok: resp.ok, status, body: respBody, headers, etag };
22245
+ if ((method === "PUT" || method === "PATCH") && resp.ok) {
22246
+ if (status === 201) result.outcome = "created";
22247
+ else if (status === 200) result.outcome = "updated";
22248
+ }
22249
+ if (!resp.ok) {
22250
+ result.fault = mapOcapiWriteFault(status, respBody);
21767
22251
  }
21768
- return { ok: resp.ok, status, body };
22252
+ return result;
21769
22253
  };
21770
22254
  const first = await doRequest();
21771
22255
  if (first.status === 401) {
@@ -21774,34 +22258,34 @@ async function ocapiGet(path33, credentials, ocapiVersion = DEFAULT_OCAPI_VERSIO
21774
22258
  }
21775
22259
  return first;
21776
22260
  }
22261
+ async function ocapiGet(path33, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22262
+ return ocapiRequest("GET", path33, void 0, credentials, ocapiVersion);
22263
+ }
21777
22264
  async function ocapiPost(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
21778
- const doRequest = async () => {
21779
- const token = await getAmToken(credentials);
21780
- const baseUrl = `https://${credentials.hostname}/s/-/dw/data/${ocapiVersion}`;
21781
- const url = `${baseUrl}${path33.startsWith("/") ? path33 : "/" + path33}`;
21782
- const resp = await fetch(url, {
21783
- method: "POST",
21784
- headers: {
21785
- Authorization: `Bearer ${token}`,
21786
- "Content-Type": "application/json"
21787
- },
21788
- body: JSON.stringify(body)
21789
- });
21790
- const status = resp.status;
21791
- let respBody = null;
21792
- try {
21793
- respBody = await resp.json();
21794
- } catch {
21795
- respBody = null;
21796
- }
21797
- return { ok: resp.ok, status, body: respBody };
21798
- };
21799
- const first = await doRequest();
21800
- if (first.status === 401) {
21801
- invalidateAmToken(credentials);
21802
- return doRequest();
22265
+ return ocapiRequest("POST", path33, body, credentials, ocapiVersion);
22266
+ }
22267
+ async function ocapiPut(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22268
+ return ocapiRequest("PUT", path33, body, credentials, ocapiVersion);
22269
+ }
22270
+ async function ocapiPatch(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22271
+ const getResult = await ocapiGet(path33, credentials, ocapiVersion);
22272
+ if (!getResult.ok) {
22273
+ return getResult;
21803
22274
  }
21804
- return first;
22275
+ const etag = getResult.etag;
22276
+ if (etag === null || etag === void 0 || etag.trim() === "") {
22277
+ const syntheticBody = buildSyntheticIfMatchRequiredBody(path33);
22278
+ return {
22279
+ ok: false,
22280
+ status: 409,
22281
+ body: syntheticBody,
22282
+ fault: mapOcapiWriteFault(409, syntheticBody)
22283
+ };
22284
+ }
22285
+ return ocapiRequest("PATCH", path33, body, credentials, ocapiVersion, { "If-Match": etag });
22286
+ }
22287
+ async function ocapiPatchDirect(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22288
+ return ocapiRequest("PATCH", path33, body, credentials, ocapiVersion);
21805
22289
  }
21806
22290
 
21807
22291
  // src/sfcc/setup-status.ts
@@ -21962,6 +22446,53 @@ function normalizeOcapiBody(body) {
21962
22446
  return { items: [], count: 0, total: void 0, hasMore: false };
21963
22447
  }
21964
22448
 
22449
+ // src/sfcc/write-grants.ts
22450
+ var OCAPI_WRITE_RESOURCE_IDS = [
22451
+ "/system_object_definitions",
22452
+ "/system_object_definitions/**",
22453
+ "/custom_object_definitions/**",
22454
+ "/site_preferences/**"
22455
+ ];
22456
+ function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
22457
+ return {
22458
+ _v: ocapiVersion,
22459
+ clients: [
22460
+ {
22461
+ client_id: clientIdPlaceholder,
22462
+ resources: OCAPI_WRITE_RESOURCE_IDS.map((resource_id) => ({
22463
+ resource_id,
22464
+ methods: ["get", "put", "patch", "delete"],
22465
+ read_attributes: "(**)",
22466
+ write_attributes: "(**)"
22467
+ }))
22468
+ }
22469
+ ]
22470
+ };
22471
+ }
22472
+ function formatOcapiWriteGrantJson(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
22473
+ return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder), null, 2);
22474
+ }
22475
+ function buildOcapiWriteGrant403Text(params) {
22476
+ const { operation, path: path33, ocapiVersion, body } = params;
22477
+ const bodyLine = body === void 0 ? "" : `
22478
+ Response body:
22479
+ ${JSON.stringify(body, null, 2)}
22480
+ `;
22481
+ return `HTTP 403: OCAPI write access denied for ${operation} ${path33}.
22482
+ ` + bodyLine + `
22483
+ To grant write access, paste the JSON below in Business Manager:
22484
+ Administration > Site Development > Open Commerce API Settings \u2192 Data API tab
22485
+
22486
+ ${formatOcapiWriteGrantJson(ocapiVersion)}
22487
+
22488
+ Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`;
22489
+ }
22490
+ function writeGrantForbiddenResult(params) {
22491
+ return {
22492
+ content: [{ type: "text", text: buildOcapiWriteGrant403Text(params) }]
22493
+ };
22494
+ }
22495
+
21965
22496
  // src/sfcc/permissions.ts
21966
22497
  var OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify(
21967
22498
  {
@@ -22001,32 +22532,7 @@ var OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify(
22001
22532
  null,
22002
22533
  2
22003
22534
  );
22004
- var OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => JSON.stringify(
22005
- {
22006
- _v: ocapiVersion,
22007
- clients: [
22008
- {
22009
- client_id: "<YOUR_CLIENT_ID>",
22010
- resources: [
22011
- {
22012
- resource_id: "/system_object_definitions",
22013
- methods: ["get", "put", "patch", "delete"],
22014
- read_attributes: "(**)",
22015
- write_attributes: "(**)"
22016
- },
22017
- {
22018
- resource_id: "/system_object_definitions/**",
22019
- methods: ["get", "put", "patch", "delete"],
22020
- read_attributes: "(**)",
22021
- write_attributes: "(**)"
22022
- }
22023
- ]
22024
- }
22025
- ]
22026
- },
22027
- null,
22028
- 2
22029
- );
22535
+ var OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => formatOcapiWriteGrantJson(ocapiVersion);
22030
22536
  async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22031
22537
  let result;
22032
22538
  try {
@@ -22578,6 +23084,800 @@ function registerSitePreferenceTools(registerTool2, deps) {
22578
23084
  );
22579
23085
  }
22580
23086
 
23087
+ // src/sfcc/writes-system-object.ts
23088
+ import { z as z7 } from "zod";
23089
+
23090
+ // src/sfcc/write-guard.ts
23091
+ function textResult4(text) {
23092
+ return { content: [{ type: "text", text }] };
23093
+ }
23094
+ function rejectIfNotSandboxForWrite(instance) {
23095
+ const effective = instance === void 0 ? "sandbox" : instance;
23096
+ if (effective === "sandbox") return null;
23097
+ return textResult4(
23098
+ JSON.stringify({
23099
+ error: "VALIDATION_ERROR",
23100
+ status: 400,
23101
+ message: `SFCC write tools are sandbox-only. Refusing to write against instance '${effective}'. Re-run the write against a developer sandbox instance.`
23102
+ })
23103
+ );
23104
+ }
23105
+
23106
+ // src/sfcc/write-result.ts
23107
+ function textResult5(text) {
23108
+ return { content: [{ type: "text", text }] };
23109
+ }
23110
+ function formatOcapiWriteToolResult(result, operation, path33, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23111
+ if (result.status === 403) {
23112
+ return writeGrantForbiddenResult({
23113
+ operation,
23114
+ path: path33,
23115
+ ocapiVersion,
23116
+ body: result.body
23117
+ });
23118
+ }
23119
+ if (result.ok) {
23120
+ return textResult5(
23121
+ JSON.stringify({
23122
+ status: result.status,
23123
+ outcome: result.outcome,
23124
+ body: result.body
23125
+ })
23126
+ );
23127
+ }
23128
+ return textResult5(
23129
+ JSON.stringify({
23130
+ error: "OCAPI_WRITE_ERROR",
23131
+ status: result.status,
23132
+ fault: result.fault,
23133
+ body: result.body
23134
+ })
23135
+ );
23136
+ }
23137
+
23138
+ // src/sfcc/writes-system-object-payloads.ts
23139
+ import { z as z6 } from "zod";
23140
+ var localizedStringSchema = z6.record(z6.string(), z6.string());
23141
+ var objectAttributeValueTypeSchema = z6.enum([
23142
+ "string",
23143
+ "int",
23144
+ "double",
23145
+ "text",
23146
+ "html",
23147
+ "date",
23148
+ "image",
23149
+ "boolean",
23150
+ "money",
23151
+ "quantity",
23152
+ "datetime",
23153
+ "email",
23154
+ "password",
23155
+ "set_of_string",
23156
+ "set_of_int",
23157
+ "set_of_double",
23158
+ "enum_of_string",
23159
+ "enum_of_int"
23160
+ ]);
23161
+ var OUTPUT_ONLY_BODY_PROPERTIES = ["html", "image"];
23162
+ function rejectOutputOnlyProperties(body, ctx) {
23163
+ for (const key of OUTPUT_ONLY_BODY_PROPERTIES) {
23164
+ if (Object.prototype.hasOwnProperty.call(body, key)) {
23165
+ ctx.addIssue({
23166
+ code: z6.ZodIssueCode.custom,
23167
+ path: [key],
23168
+ message: `'${key}' is an output-only OCAPI property and cannot be set on a write body. (Use value_type: "${key}" to declare an ${key} attribute instead.)`
23169
+ });
23170
+ }
23171
+ }
23172
+ }
23173
+ var objectAttributeDefinitionCommonShape = {
23174
+ id: z6.string().optional(),
23175
+ system: z6.boolean().optional(),
23176
+ display_name: localizedStringSchema.optional(),
23177
+ description: localizedStringSchema.optional(),
23178
+ mandatory: z6.boolean().optional(),
23179
+ localizable: z6.boolean().optional(),
23180
+ site_specific: z6.boolean().optional(),
23181
+ default_value: z6.any().optional()
23182
+ };
23183
+ var objectAttributeDefinitionCreateBodySchema = z6.object({
23184
+ value_type: objectAttributeValueTypeSchema.describe("Required OCAPI attribute value type."),
23185
+ ...objectAttributeDefinitionCommonShape
23186
+ }).passthrough().superRefine(rejectOutputOnlyProperties);
23187
+ var objectAttributeDefinitionPatchBodySchema = z6.object({
23188
+ value_type: objectAttributeValueTypeSchema.optional(),
23189
+ ...objectAttributeDefinitionCommonShape
23190
+ }).passthrough().superRefine((body, ctx) => {
23191
+ rejectOutputOnlyProperties(body, ctx);
23192
+ if (Object.keys(body).length === 0) {
23193
+ ctx.addIssue({
23194
+ code: z6.ZodIssueCode.custom,
23195
+ message: "Patch body must contain at least one field to update."
23196
+ });
23197
+ }
23198
+ });
23199
+ var attributeGroupPutBodySchema = z6.object({
23200
+ display_name: localizedStringSchema.describe("Localized group display name."),
23201
+ internal: z6.boolean().describe("Whether the group is internal (BM-only).")
23202
+ });
23203
+ var attributeGroupPatchBodySchema = z6.object({
23204
+ display_name: localizedStringSchema.optional(),
23205
+ internal: z6.boolean().optional()
23206
+ }).superRefine((body, ctx) => {
23207
+ if (Object.keys(body).length === 0) {
23208
+ ctx.addIssue({
23209
+ code: z6.ZodIssueCode.custom,
23210
+ message: "Patch body must contain at least one field to update."
23211
+ });
23212
+ }
23213
+ });
23214
+ var SfccWritePayloadFault = class extends Error {
23215
+ status;
23216
+ faultType;
23217
+ constructor(faultType, message, status = 400) {
23218
+ super(message);
23219
+ this.name = "SfccWritePayloadFault";
23220
+ this.faultType = faultType;
23221
+ this.status = status;
23222
+ }
23223
+ };
23224
+ function buildObjectAttributeDefinitionCreatePayload(urlId, body) {
23225
+ if (body.id !== void 0 && body.id !== urlId) {
23226
+ throw new SfccWritePayloadFault(
23227
+ "IdConflictException",
23228
+ `Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`
23229
+ );
23230
+ }
23231
+ if (body.system === true) {
23232
+ throw new SfccWritePayloadFault(
23233
+ "AttributeDefinitionKeyReadOnlyException",
23234
+ "Cannot create a system attribute definition (system: true) over OCAPI writes."
23235
+ );
23236
+ }
23237
+ return { ...body, id: urlId, system: false };
23238
+ }
23239
+ function buildObjectAttributeDefinitionPatchPayload(urlId, body) {
23240
+ if (body.id !== void 0 && body.id !== urlId) {
23241
+ throw new SfccWritePayloadFault(
23242
+ "IdConflictException",
23243
+ `Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`
23244
+ );
23245
+ }
23246
+ if (body.system === true) {
23247
+ throw new SfccWritePayloadFault(
23248
+ "AttributeDefinitionKeyReadOnlyException",
23249
+ "Cannot patch an attribute definition to system: true over OCAPI writes."
23250
+ );
23251
+ }
23252
+ return { ...body };
23253
+ }
23254
+ function buildAttributeGroupPutPayload(body) {
23255
+ return { display_name: body.display_name, internal: body.internal };
23256
+ }
23257
+ function buildAttributeGroupPatchPayload(body) {
23258
+ return { ...body };
23259
+ }
23260
+ function buildEmptyRelationPayload() {
23261
+ return {};
23262
+ }
23263
+
23264
+ // src/sfcc/writes-system-object.ts
23265
+ var WRITE_ANNOTATIONS = {
23266
+ readOnlyHint: false,
23267
+ destructiveHint: true,
23268
+ idempotentHint: false,
23269
+ openWorldHint: true
23270
+ };
23271
+ function textResult6(text) {
23272
+ return { content: [{ type: "text", text }] };
23273
+ }
23274
+ function zodValidationEnvelope(err) {
23275
+ return textResult6(
23276
+ JSON.stringify({
23277
+ error: "VALIDATION_ERROR",
23278
+ status: 400,
23279
+ message: "Invalid SFCC write tool input.",
23280
+ issues: err.issues
23281
+ })
23282
+ );
23283
+ }
23284
+ function payloadFaultEnvelope(fault) {
23285
+ return textResult6(
23286
+ JSON.stringify({
23287
+ error: "OCAPI_WRITE_ERROR",
23288
+ status: fault.status,
23289
+ fault: { type: fault.faultType, message: fault.message }
23290
+ })
23291
+ );
23292
+ }
23293
+ function unexpectedEnvelope() {
23294
+ return textResult6(
23295
+ JSON.stringify({
23296
+ error: "INTERNAL_ERROR",
23297
+ status: 500,
23298
+ message: "Unexpected SFCC write tool failure"
23299
+ })
23300
+ );
23301
+ }
23302
+ function preTransportErrorEnvelope(err) {
23303
+ if (err instanceof z7.ZodError) return zodValidationEnvelope(err);
23304
+ if (err instanceof SfccWritePayloadFault) return payloadFaultEnvelope(err);
23305
+ return unexpectedEnvelope();
23306
+ }
23307
+ function encodedSegment(segment) {
23308
+ return encodeURIComponent(segment);
23309
+ }
23310
+ function attributeDefinitionPath(objectType, attributeId) {
23311
+ return `/system_object_definitions/${encodedSegment(objectType)}/attribute_definitions/${encodedSegment(attributeId)}`;
23312
+ }
23313
+ function attributeGroupPath(objectType, groupId) {
23314
+ return `/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}`;
23315
+ }
23316
+ function attributeGroupAssignmentPath(objectType, groupId, attributeId) {
23317
+ return `/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}/attribute_definitions/${encodedSegment(attributeId)}`;
23318
+ }
23319
+ function preferenceObjectTypeForScope(scope) {
23320
+ return scope === "site" ? "SitePreferences" : "OrganizationPreferences";
23321
+ }
23322
+ var instanceSchema = z7.string().optional().describe('Sandbox-only: omit (defaults to sandbox) or pass "sandbox".');
23323
+ var createAttributeDefinitionInput = z7.object({
23324
+ object_type: z7.string().describe('System object type, e.g. "Product".'),
23325
+ attribute_id: z7.string().describe("Attribute id (URL id); the body id must match it."),
23326
+ definition: objectAttributeDefinitionCreateBodySchema.describe(
23327
+ "OCAPI ObjectAttributeDefinition create body (requires value_type)."
23328
+ ),
23329
+ instance: instanceSchema
23330
+ });
23331
+ var updateAttributeDefinitionInput = z7.object({
23332
+ object_type: z7.string().describe("System object type."),
23333
+ attribute_id: z7.string().describe("Attribute id (URL id)."),
23334
+ patch: objectAttributeDefinitionPatchBodySchema.describe(
23335
+ "Partial ObjectAttributeDefinition body (>=1 field)."
23336
+ ),
23337
+ instance: instanceSchema
23338
+ });
23339
+ var createAttributeGroupInput = z7.object({
23340
+ object_type: z7.string().describe("System object type."),
23341
+ group_id: z7.string().describe("Attribute group id (URL id)."),
23342
+ display_name: localizedStringSchema.describe("Localized group display name."),
23343
+ internal: z7.boolean().describe("Whether the group is internal (BM-only)."),
23344
+ instance: instanceSchema
23345
+ });
23346
+ var updateAttributeGroupInput = z7.object({
23347
+ object_type: z7.string().describe("System object type."),
23348
+ group_id: z7.string().describe("Attribute group id (URL id)."),
23349
+ patch: attributeGroupPatchBodySchema.describe(
23350
+ "Partial group body (display_name and/or internal; >=1 field)."
23351
+ ),
23352
+ instance: instanceSchema
23353
+ });
23354
+ var assignAttributeToGroupInput = z7.object({
23355
+ object_type: z7.string().describe("System object type."),
23356
+ group_id: z7.string().describe("Attribute group id."),
23357
+ attribute_id: z7.string().describe("Attribute definition id to assign."),
23358
+ instance: instanceSchema
23359
+ });
23360
+ var createCustomPreferenceDefinitionInput = z7.object({
23361
+ preference_scope: z7.enum(["site", "organization"]).describe("site \u2192 SitePreferences; organization \u2192 OrganizationPreferences."),
23362
+ preference_id: z7.string().describe("Preference (attribute) id."),
23363
+ definition: objectAttributeDefinitionCreateBodySchema.describe(
23364
+ "OCAPI ObjectAttributeDefinition create body (requires value_type). Set default_value for booleans (else they resolve to null)."
23365
+ ),
23366
+ instance: instanceSchema
23367
+ });
23368
+ function buildCreateAttributeDefinitionHandler(gateDeps) {
23369
+ return withSfccGate(
23370
+ gateDeps,
23371
+ async (args, credentials) => {
23372
+ let path33;
23373
+ let body;
23374
+ try {
23375
+ const parsed = createAttributeDefinitionInput.parse(args);
23376
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23377
+ if (guard) return guard;
23378
+ path33 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23379
+ body = buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id, parsed.definition);
23380
+ } catch (err) {
23381
+ return preTransportErrorEnvelope(err);
23382
+ }
23383
+ try {
23384
+ const result = await ocapiPut(path33, body, credentials);
23385
+ return formatOcapiWriteToolResult(result, "PUT", path33);
23386
+ } catch {
23387
+ return unexpectedEnvelope();
23388
+ }
23389
+ }
23390
+ );
23391
+ }
23392
+ function buildUpdateAttributeDefinitionHandler(gateDeps) {
23393
+ return withSfccGate(
23394
+ gateDeps,
23395
+ async (args, credentials) => {
23396
+ let path33;
23397
+ let body;
23398
+ try {
23399
+ const parsed = updateAttributeDefinitionInput.parse(args);
23400
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23401
+ if (guard) return guard;
23402
+ path33 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23403
+ body = buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id, parsed.patch);
23404
+ } catch (err) {
23405
+ return preTransportErrorEnvelope(err);
23406
+ }
23407
+ try {
23408
+ const result = await ocapiPatch(path33, body, credentials);
23409
+ return formatOcapiWriteToolResult(result, "PATCH", path33);
23410
+ } catch {
23411
+ return unexpectedEnvelope();
23412
+ }
23413
+ }
23414
+ );
23415
+ }
23416
+ function buildCreateAttributeGroupHandler(gateDeps) {
23417
+ return withSfccGate(
23418
+ gateDeps,
23419
+ async (args, credentials) => {
23420
+ let path33;
23421
+ let body;
23422
+ try {
23423
+ const parsed = createAttributeGroupInput.parse(args);
23424
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23425
+ if (guard) return guard;
23426
+ path33 = attributeGroupPath(parsed.object_type, parsed.group_id);
23427
+ body = buildAttributeGroupPutPayload({
23428
+ display_name: parsed.display_name,
23429
+ internal: parsed.internal
23430
+ });
23431
+ } catch (err) {
23432
+ return preTransportErrorEnvelope(err);
23433
+ }
23434
+ try {
23435
+ const result = await ocapiPut(path33, body, credentials);
23436
+ return formatOcapiWriteToolResult(result, "PUT", path33);
23437
+ } catch {
23438
+ return unexpectedEnvelope();
23439
+ }
23440
+ }
23441
+ );
23442
+ }
23443
+ function buildUpdateAttributeGroupHandler(gateDeps) {
23444
+ return withSfccGate(
23445
+ gateDeps,
23446
+ async (args, credentials) => {
23447
+ let path33;
23448
+ let body;
23449
+ try {
23450
+ const parsed = updateAttributeGroupInput.parse(args);
23451
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23452
+ if (guard) return guard;
23453
+ path33 = attributeGroupPath(parsed.object_type, parsed.group_id);
23454
+ body = buildAttributeGroupPatchPayload(parsed.patch);
23455
+ } catch (err) {
23456
+ return preTransportErrorEnvelope(err);
23457
+ }
23458
+ try {
23459
+ const result = await ocapiPatch(path33, body, credentials);
23460
+ return formatOcapiWriteToolResult(result, "PATCH", path33);
23461
+ } catch {
23462
+ return unexpectedEnvelope();
23463
+ }
23464
+ }
23465
+ );
23466
+ }
23467
+ function buildAssignAttributeToGroupHandler(gateDeps) {
23468
+ return withSfccGate(
23469
+ gateDeps,
23470
+ async (args, credentials) => {
23471
+ let path33;
23472
+ try {
23473
+ const parsed = assignAttributeToGroupInput.parse(args);
23474
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23475
+ if (guard) return guard;
23476
+ path33 = attributeGroupAssignmentPath(
23477
+ parsed.object_type,
23478
+ parsed.group_id,
23479
+ parsed.attribute_id
23480
+ );
23481
+ } catch (err) {
23482
+ return preTransportErrorEnvelope(err);
23483
+ }
23484
+ try {
23485
+ const result = await ocapiPut(path33, buildEmptyRelationPayload(), credentials);
23486
+ return formatOcapiWriteToolResult(result, "PUT", path33);
23487
+ } catch {
23488
+ return unexpectedEnvelope();
23489
+ }
23490
+ }
23491
+ );
23492
+ }
23493
+ function buildCreateCustomPreferenceDefinitionHandler(gateDeps) {
23494
+ return withSfccGate(
23495
+ gateDeps,
23496
+ async (args, credentials) => {
23497
+ let path33;
23498
+ let body;
23499
+ try {
23500
+ const parsed = createCustomPreferenceDefinitionInput.parse(args);
23501
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23502
+ if (guard) return guard;
23503
+ const objectType = preferenceObjectTypeForScope(parsed.preference_scope);
23504
+ path33 = attributeDefinitionPath(objectType, parsed.preference_id);
23505
+ body = buildObjectAttributeDefinitionCreatePayload(parsed.preference_id, parsed.definition);
23506
+ } catch (err) {
23507
+ return preTransportErrorEnvelope(err);
23508
+ }
23509
+ try {
23510
+ const result = await ocapiPut(path33, body, credentials);
23511
+ return formatOcapiWriteToolResult(result, "PUT", path33);
23512
+ } catch {
23513
+ return unexpectedEnvelope();
23514
+ }
23515
+ }
23516
+ );
23517
+ }
23518
+ var SYSTEM_OBJECT_WRITE_TOOL_NAMES = [
23519
+ "system_object_attribute_definition_create",
23520
+ "system_object_attribute_definition_update",
23521
+ "system_object_attribute_group_create",
23522
+ "system_object_attribute_group_update",
23523
+ "system_object_attribute_assign_to_group",
23524
+ "custom_preference_definition_create"
23525
+ ];
23526
+ function registerSystemObjectWriteTools(registerTool2, deps) {
23527
+ const { gateDeps } = deps;
23528
+ registerTool2(
23529
+ "system_object_attribute_definition_create",
23530
+ {
23531
+ description: "Create a system-object attribute definition (sandbox-only, destructive). PUT .../attribute_definitions/{id}; body id must equal URL id and system must be false. Returns 201. HTTP 403 echoes the OCAPI write-grant JSON.",
23532
+ inputSchema: createAttributeDefinitionInput,
23533
+ annotations: WRITE_ANNOTATIONS
23534
+ },
23535
+ buildCreateAttributeDefinitionHandler(gateDeps)
23536
+ );
23537
+ registerTool2(
23538
+ "system_object_attribute_definition_update",
23539
+ {
23540
+ description: "Update a system-object attribute definition (sandbox-only, destructive). PATCH .../attribute_definitions/{id} via the ETag GET-then-If-Match round trip. Returns 200; surfaces 409/412 on ETag conflicts. HTTP 403 echoes the write-grant JSON.",
23541
+ inputSchema: updateAttributeDefinitionInput,
23542
+ annotations: WRITE_ANNOTATIONS
23543
+ },
23544
+ buildUpdateAttributeDefinitionHandler(gateDeps)
23545
+ );
23546
+ registerTool2(
23547
+ "system_object_attribute_group_create",
23548
+ {
23549
+ description: "Create a system-object attribute group (sandbox-only, destructive). PUT .../attribute_groups/{id} with a minimal body (display_name + internal). HTTP 403 echoes the OCAPI write-grant JSON.",
23550
+ inputSchema: createAttributeGroupInput,
23551
+ annotations: WRITE_ANNOTATIONS
23552
+ },
23553
+ buildCreateAttributeGroupHandler(gateDeps)
23554
+ );
23555
+ registerTool2(
23556
+ "system_object_attribute_group_update",
23557
+ {
23558
+ description: "Update a system-object attribute group (sandbox-only, destructive). PATCH .../attribute_groups/{id} via the ETag round trip (display_name and/or internal). HTTP 403 echoes the OCAPI write-grant JSON.",
23559
+ inputSchema: updateAttributeGroupInput,
23560
+ annotations: WRITE_ANNOTATIONS
23561
+ },
23562
+ buildUpdateAttributeGroupHandler(gateDeps)
23563
+ );
23564
+ registerTool2(
23565
+ "system_object_attribute_assign_to_group",
23566
+ {
23567
+ description: "Assign an attribute definition to an attribute group (sandbox-only, destructive). PUT .../attribute_groups/{group}/attribute_definitions/{def} with an empty body. HTTP 403 echoes the OCAPI write-grant JSON.",
23568
+ inputSchema: assignAttributeToGroupInput,
23569
+ annotations: WRITE_ANNOTATIONS
23570
+ },
23571
+ buildAssignAttributeToGroupHandler(gateDeps)
23572
+ );
23573
+ registerTool2(
23574
+ "custom_preference_definition_create",
23575
+ {
23576
+ description: "Create a custom preference definition (sandbox-only, destructive). PUT .../{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}. Booleans resolve to null unless default_value is set. HTTP 403 echoes the write-grant JSON.",
23577
+ inputSchema: createCustomPreferenceDefinitionInput,
23578
+ annotations: WRITE_ANNOTATIONS
23579
+ },
23580
+ buildCreateCustomPreferenceDefinitionHandler(gateDeps)
23581
+ );
23582
+ }
23583
+
23584
+ // src/sfcc/writes-custom-object-def.ts
23585
+ import { z as z10 } from "zod";
23586
+
23587
+ // src/sfcc/writes-object-attribute-payloads.ts
23588
+ import { z as z8 } from "zod";
23589
+ var objectAttributeValueTypeSchema2 = z8.enum([
23590
+ "string",
23591
+ "int",
23592
+ "double",
23593
+ "boolean",
23594
+ "date",
23595
+ "datetime",
23596
+ "email",
23597
+ "enum_of_int",
23598
+ "enum_of_string",
23599
+ "html",
23600
+ "image",
23601
+ "money",
23602
+ "password",
23603
+ "quantity",
23604
+ "set_of_int",
23605
+ "set_of_string",
23606
+ "set_of_double",
23607
+ "text"
23608
+ ]);
23609
+ var objectAttributeDefinitionCreateBodySchema2 = z8.object({
23610
+ id: z8.string().min(1).optional(),
23611
+ value_type: objectAttributeValueTypeSchema2
23612
+ }).passthrough();
23613
+ var objectAttributeDefinitionPatchBodySchema2 = z8.object({
23614
+ value_type: objectAttributeValueTypeSchema2.optional()
23615
+ }).passthrough().superRefine((body, ctx) => {
23616
+ if (Object.keys(body).length === 0) {
23617
+ ctx.addIssue({
23618
+ code: z8.ZodIssueCode.custom,
23619
+ message: "Patch body must contain at least one attribute-definition field to update."
23620
+ });
23621
+ }
23622
+ });
23623
+ function buildObjectAttributeDefinitionCreatePayload2(attributeId, body) {
23624
+ return { ...body, id: attributeId };
23625
+ }
23626
+ function buildObjectAttributeDefinitionPatchPayload2(body) {
23627
+ return { ...body };
23628
+ }
23629
+
23630
+ // src/sfcc/write-tool-common.ts
23631
+ import { z as z9 } from "zod";
23632
+ var WRITE_ANNOTATIONS2 = {
23633
+ readOnlyHint: false,
23634
+ destructiveHint: true,
23635
+ idempotentHint: false,
23636
+ openWorldHint: true
23637
+ };
23638
+ function textResult7(text) {
23639
+ return { content: [{ type: "text", text }] };
23640
+ }
23641
+ function zodValidationEnvelope2(err) {
23642
+ return textResult7(
23643
+ JSON.stringify(
23644
+ {
23645
+ error: "VALIDATION_ERROR",
23646
+ status: 400,
23647
+ message: "Input failed schema validation before any OCAPI call.",
23648
+ issues: err.issues.map((issue) => ({
23649
+ path: issue.path.join("."),
23650
+ message: issue.message
23651
+ }))
23652
+ },
23653
+ null,
23654
+ 2
23655
+ )
23656
+ );
23657
+ }
23658
+ function validationEnvelope(message) {
23659
+ return textResult7(
23660
+ JSON.stringify({ error: "VALIDATION_ERROR", status: 400, message }, null, 2)
23661
+ );
23662
+ }
23663
+ function unexpectedEnvelope2() {
23664
+ return textResult7(
23665
+ JSON.stringify(
23666
+ {
23667
+ error: "INTERNAL_ERROR",
23668
+ status: 500,
23669
+ message: "Unexpected SFCC write tool failure."
23670
+ },
23671
+ null,
23672
+ 2
23673
+ )
23674
+ );
23675
+ }
23676
+ function preTransportErrorEnvelope2(err) {
23677
+ if (err instanceof z9.ZodError) return zodValidationEnvelope2(err);
23678
+ return unexpectedEnvelope2();
23679
+ }
23680
+ function encodedSegment2(segment) {
23681
+ return encodeURIComponent(segment);
23682
+ }
23683
+
23684
+ // src/sfcc/writes-custom-object-def.ts
23685
+ var INSTANCE_DESCRIBE2 = "OCAPI instance context. SFCC writes are sandbox-only; omit for sandbox. Any other value is rejected before OCAPI is called.";
23686
+ var createCustomObjectAttributeDefinitionInput = z10.object({
23687
+ object_type: z10.string().describe(
23688
+ "Known custom object type identifier (must already exist). OCAPI cannot enumerate or create custom object types \u2014 only attribute definitions on a known type."
23689
+ ),
23690
+ attribute_id: z10.string().describe("URL attribute-definition id. If the body also carries `id`, it must match."),
23691
+ definition: objectAttributeDefinitionCreateBodySchema2.describe(
23692
+ "ObjectAttributeDefinition body; `value_type` is required for a create."
23693
+ ),
23694
+ instance: z10.string().optional().describe(INSTANCE_DESCRIBE2)
23695
+ });
23696
+ var updateCustomObjectAttributeDefinitionInput = z10.object({
23697
+ object_type: z10.string().describe(
23698
+ "Known custom object type identifier (must already exist). OCAPI cannot create types."
23699
+ ),
23700
+ attribute_id: z10.string().describe("URL attribute-definition id to update."),
23701
+ patch: objectAttributeDefinitionPatchBodySchema2.describe(
23702
+ "Partial ObjectAttributeDefinition body; must change at least one field."
23703
+ ),
23704
+ instance: z10.string().optional().describe(INSTANCE_DESCRIBE2)
23705
+ });
23706
+ function customObjectAttributeDefinitionPath(objectType, attributeId) {
23707
+ return `/custom_object_definitions/${encodedSegment2(objectType)}/attribute_definitions/${encodedSegment2(attributeId)}`;
23708
+ }
23709
+ function buildCreateCustomObjectAttributeDefinitionHandler(gateDeps) {
23710
+ return withSfccGate(
23711
+ gateDeps,
23712
+ async (args, credentials) => {
23713
+ let parsed;
23714
+ try {
23715
+ parsed = createCustomObjectAttributeDefinitionInput.parse(args);
23716
+ } catch (err) {
23717
+ return preTransportErrorEnvelope2(err);
23718
+ }
23719
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23720
+ if (guard) return guard;
23721
+ if (parsed.definition.id !== void 0 && parsed.definition.id !== parsed.attribute_id) {
23722
+ return validationEnvelope(
23723
+ `Body id '${parsed.definition.id}' does not match URL attribute_id '${parsed.attribute_id}'.`
23724
+ );
23725
+ }
23726
+ const path33 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23727
+ const body = buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id, parsed.definition);
23728
+ try {
23729
+ const result = await ocapiPut(path33, body, credentials);
23730
+ return formatOcapiWriteToolResult(result, "PUT", path33);
23731
+ } catch {
23732
+ return unexpectedEnvelope2();
23733
+ }
23734
+ }
23735
+ );
23736
+ }
23737
+ function buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps) {
23738
+ return withSfccGate(
23739
+ gateDeps,
23740
+ async (args, credentials) => {
23741
+ let parsed;
23742
+ try {
23743
+ parsed = updateCustomObjectAttributeDefinitionInput.parse(args);
23744
+ } catch (err) {
23745
+ return preTransportErrorEnvelope2(err);
23746
+ }
23747
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23748
+ if (guard) return guard;
23749
+ const path33 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23750
+ const body = buildObjectAttributeDefinitionPatchPayload2(parsed.patch);
23751
+ try {
23752
+ const result = await ocapiPatch(path33, body, credentials);
23753
+ return formatOcapiWriteToolResult(result, "PATCH", path33);
23754
+ } catch {
23755
+ return unexpectedEnvelope2();
23756
+ }
23757
+ }
23758
+ );
23759
+ }
23760
+ var CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES = [
23761
+ "custom_object_definition_attribute_create",
23762
+ "custom_object_definition_attribute_update"
23763
+ ];
23764
+ function registerSfccCustomObjectDefWriteTools(registerTool2, deps) {
23765
+ const { gateDeps } = deps;
23766
+ registerTool2(
23767
+ "custom_object_definition_attribute_create",
23768
+ {
23769
+ description: "Create an attribute definition on a KNOWN custom object type via PUT /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; the type must pre-exist (OCAPI cannot create types). Echoes paste-ready grant JSON on 403.",
23770
+ inputSchema: createCustomObjectAttributeDefinitionInput,
23771
+ annotations: WRITE_ANNOTATIONS2
23772
+ },
23773
+ buildCreateCustomObjectAttributeDefinitionHandler(gateDeps)
23774
+ );
23775
+ registerTool2(
23776
+ "custom_object_definition_attribute_update",
23777
+ {
23778
+ description: "Update an attribute definition on a KNOWN custom object type via an ETag-conditional PATCH /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; surfaces 409/412 conflicts and echoes grant JSON on 403.",
23779
+ inputSchema: updateCustomObjectAttributeDefinitionInput,
23780
+ annotations: WRITE_ANNOTATIONS2
23781
+ },
23782
+ buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps)
23783
+ );
23784
+ }
23785
+
23786
+ // src/sfcc/writes-site-preference.ts
23787
+ import { z as z12 } from "zod";
23788
+
23789
+ // src/sfcc/writes-site-preference-payloads.ts
23790
+ import { z as z11 } from "zod";
23791
+ var sitePreferenceValueSchema = z11.union([
23792
+ z11.string(),
23793
+ z11.number().finite(),
23794
+ z11.boolean(),
23795
+ z11.array(z11.string())
23796
+ ]);
23797
+ var sitePreferenceValuesPatchBodySchema = z11.record(z11.string(), sitePreferenceValueSchema).superRefine((values, ctx) => {
23798
+ const keys = Object.keys(values);
23799
+ if (keys.length === 0) {
23800
+ ctx.addIssue({
23801
+ code: z11.ZodIssueCode.custom,
23802
+ message: "At least one preference value is required."
23803
+ });
23804
+ }
23805
+ for (const key of keys) {
23806
+ if (!key.startsWith("c_")) {
23807
+ ctx.addIssue({
23808
+ code: z11.ZodIssueCode.custom,
23809
+ path: [key],
23810
+ message: `Preference id '${key}' must be a custom preference starting with 'c_'.`
23811
+ });
23812
+ }
23813
+ }
23814
+ });
23815
+ function buildSitePreferenceValuesPatchPayload(values) {
23816
+ return { ...values };
23817
+ }
23818
+
23819
+ // src/sfcc/writes-site-preference.ts
23820
+ var INSTANCE_ENUM2 = z12.enum(["staging", "development", "sandbox", "production"]);
23821
+ var INSTANCE_DESCRIBE3 = "OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected before OCAPI is called. Defaults to 'sandbox'.";
23822
+ var sitePreferenceValuesSetInput = z12.object({
23823
+ group: z12.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),
23824
+ instance: INSTANCE_ENUM2.optional().default("sandbox").describe(INSTANCE_DESCRIBE3),
23825
+ values: sitePreferenceValuesPatchBodySchema.describe(
23826
+ "Flat map of c_-prefixed preference ids to values (string, number, boolean, or string[])."
23827
+ )
23828
+ });
23829
+ function sitePreferenceGroupPath(group) {
23830
+ return `/site_preferences/preference_groups/${encodedSegment2(group)}/sandbox`;
23831
+ }
23832
+ function buildSitePreferenceValuesSetHandler(gateDeps) {
23833
+ return withSfccGate(
23834
+ gateDeps,
23835
+ async (args, credentials) => {
23836
+ let parsed;
23837
+ try {
23838
+ parsed = sitePreferenceValuesSetInput.parse(args);
23839
+ } catch (err) {
23840
+ return preTransportErrorEnvelope2(err);
23841
+ }
23842
+ const guard = rejectIfNotSandboxForWrite(parsed.instance);
23843
+ if (guard) return guard;
23844
+ const path33 = sitePreferenceGroupPath(parsed.group);
23845
+ const body = buildSitePreferenceValuesPatchPayload(parsed.values);
23846
+ try {
23847
+ const result = await ocapiPatchDirect(path33, body, credentials);
23848
+ return formatOcapiWriteToolResult(result, "PATCH", path33);
23849
+ } catch {
23850
+ return unexpectedEnvelope2();
23851
+ }
23852
+ }
23853
+ );
23854
+ }
23855
+ var SITE_PREFERENCE_WRITE_TOOL_NAMES = ["site_preference_values_set"];
23856
+ function registerSitePreferenceWriteTools(registerTool2, deps) {
23857
+ const { gateDeps } = deps;
23858
+ registerTool2(
23859
+ "site_preference_values_set",
23860
+ {
23861
+ description: "Set custom site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. Sandbox-only, destructive; body is a flat map of c_-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 CustomPreferenceGroupNotFoundException; echoes grant JSON on 403.",
23862
+ inputSchema: sitePreferenceValuesSetInput,
23863
+ annotations: WRITE_ANNOTATIONS2
23864
+ },
23865
+ buildSitePreferenceValuesSetHandler(gateDeps)
23866
+ );
23867
+ }
23868
+
23869
+ // src/sfcc/writes.ts
23870
+ var SFCC_WRITE_TOOL_NAMES = [
23871
+ ...SYSTEM_OBJECT_WRITE_TOOL_NAMES,
23872
+ ...CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES,
23873
+ ...SITE_PREFERENCE_WRITE_TOOL_NAMES
23874
+ ];
23875
+ function registerSfccWriteTools(registerTool2, deps) {
23876
+ registerSystemObjectWriteTools(registerTool2, { gateDeps: deps.gateDeps });
23877
+ registerSfccCustomObjectDefWriteTools(registerTool2, { gateDeps: deps.gateDeps });
23878
+ registerSitePreferenceWriteTools(registerTool2, { gateDeps: deps.gateDeps });
23879
+ }
23880
+
22581
23881
  // src/sfcc/register.ts
22582
23882
  function registerSfccTools(registerTool2, deps) {
22583
23883
  const gateDeps = {
@@ -22589,7 +23889,7 @@ function registerSfccTools(registerTool2, deps) {
22589
23889
  "sfcc_setup_status",
22590
23890
  {
22591
23891
  description: "Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Always-registered; returns status without requiring full SFCC configuration to be complete.",
22592
- inputSchema: z6.object({}),
23892
+ inputSchema: z13.object({}),
22593
23893
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
22594
23894
  },
22595
23895
  buildSfccSetupStatusHandler(
@@ -22607,8 +23907,8 @@ function registerSfccTools(registerTool2, deps) {
22607
23907
  "check_permissions",
22608
23908
  {
22609
23909
  description: "Probe SFCC OCAPI access via GET /system_object_definitions. On 200: reports OK and the detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).",
22610
- inputSchema: z6.object({
22611
- instance: z6.string().optional().describe("Explicit sandbox hostname to use instead of dw.json auto-detection.")
23910
+ inputSchema: z13.object({
23911
+ instance: z13.string().optional().describe("Explicit sandbox hostname to use instead of dw.json auto-detection.")
22612
23912
  }),
22613
23913
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
22614
23914
  },
@@ -22627,6 +23927,10 @@ function registerSfccTools(registerTool2, deps) {
22627
23927
  gateDeps,
22628
23928
  getDocsDir: deps.getDocsDir
22629
23929
  });
23930
+ registerSfccWriteTools(registerTool2, {
23931
+ gateDeps,
23932
+ getDocsDir: deps.getDocsDir
23933
+ });
22630
23934
  }
22631
23935
  }
22632
23936
 
@@ -22914,7 +24218,7 @@ async function runReferenceTransactionHookProducer(args, deps = {}) {
22914
24218
 
22915
24219
  // src/conductor/doctor.ts
22916
24220
  init_store();
22917
- import { spawnSync as spawnSync3 } from "node:child_process";
24221
+ import { spawnSync as spawnSync2 } from "node:child_process";
22918
24222
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
22919
24223
  init_mcp_server_invocation();
22920
24224
  function skippedDenyEnforcementResult() {
@@ -23059,7 +24363,7 @@ function inspectMcpProfile(env, epicTick) {
23059
24363
  function inspectLocalMerge(runCommand) {
23060
24364
  const run = runCommand ?? ((cmd, args) => {
23061
24365
  try {
23062
- const r = spawnSync3(cmd, args, {
24366
+ const r = spawnSync2(cmd, args, {
23063
24367
  encoding: "utf8",
23064
24368
  timeout: 1e4,
23065
24369
  env: { ...process.env, GH_PROMPT_DISABLED: "1" }
@@ -24081,128 +25385,128 @@ init_pr_ci_producer();
24081
25385
  import { generateDecisionPageHtml } from "./decision-page-template.js";
24082
25386
 
24083
25387
  // src/decision-page-schema.ts
24084
- import { z as z7 } from "zod";
24085
- var ActionableItemSchema = z7.object({
24086
- id: z7.string().min(1).regex(
25388
+ import { z as z14 } from "zod";
25389
+ var ActionableItemSchema = z14.object({
25390
+ id: z14.string().min(1).regex(
24087
25391
  /^[A-Za-z0-9_-]+$/,
24088
25392
  "id must contain only letters, digits, hyphens, or underscores"
24089
25393
  ),
24090
- question: z7.string().min(1),
24091
- original_question: z7.string().optional().describe(
25394
+ question: z14.string().min(1),
25395
+ original_question: z14.string().optional().describe(
24092
25396
  "Optional display-only field: the clarifying question or critique point as originally raised; soft cap ~30 words. Omit it (or pass an empty string) for non-review callers \u2014 the renderer omits the section when it is absent or blank."
24093
25397
  ),
24094
- why_it_matters: z7.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),
24095
- recommendation_explanation: z7.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),
24096
- codebase_evidence: z7.string().optional().describe(
25398
+ why_it_matters: z14.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),
25399
+ recommendation_explanation: z14.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),
25400
+ codebase_evidence: z14.string().optional().describe(
24097
25401
  "Optional display-only field: combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block, which is omitted when this field is absent or blank."
24098
25402
  ),
24099
- source: z7.string().optional().describe(
25403
+ source: z14.string().optional().describe(
24100
25404
  `Optional source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'. When absent the rendered card emits data-source="".`
24101
25405
  ),
24102
- recommendation_index: z7.number().int().min(0).describe("0-based index of the recommended option in the options array"),
24103
- options: z7.array(z7.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),
24104
- option_consequences: z7.array(z7.string().min(1)).min(2).max(4).describe(
25406
+ recommendation_index: z14.number().int().min(0).describe("0-based index of the recommended option in the options array"),
25407
+ options: z14.array(z14.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),
25408
+ option_consequences: z14.array(z14.string().min(1)).min(2).max(4).describe(
24105
25409
  "Behavioral consequence per branch, parallel to options. Must have 2\u20134 entries; length must equal options.length."
24106
25410
  )
24107
25411
  }).superRefine((item, ctx) => {
24108
25412
  if (item.option_consequences.length !== item.options.length) {
24109
25413
  ctx.addIssue({
24110
- code: z7.ZodIssueCode.custom,
25414
+ code: z14.ZodIssueCode.custom,
24111
25415
  path: ["option_consequences"],
24112
25416
  message: `option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`
24113
25417
  });
24114
25418
  }
24115
25419
  if (item.recommendation_index >= item.options.length) {
24116
25420
  ctx.addIssue({
24117
- code: z7.ZodIssueCode.custom,
25421
+ code: z14.ZodIssueCode.custom,
24118
25422
  path: ["recommendation_index"],
24119
25423
  message: `recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`
24120
25424
  });
24121
25425
  }
24122
25426
  });
24123
- var DecisionPageLabelsSchema = z7.object({
24124
- title: z7.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),
24125
- intro: z7.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),
24126
- section_heading: z7.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),
24127
- improvements_heading: z7.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')
25427
+ var DecisionPageLabelsSchema = z14.object({
25428
+ title: z14.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),
25429
+ intro: z14.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),
25430
+ section_heading: z14.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),
25431
+ improvements_heading: z14.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')
24128
25432
  });
24129
- var SystemGoalNfrSchema = z7.object({
24130
- category: z7.string().min(1).describe(
25433
+ var SystemGoalNfrSchema = z14.object({
25434
+ category: z14.string().min(1).describe(
24131
25435
  "Canonical NFR category, e.g. security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility."
24132
25436
  ),
24133
- requirement: z7.string().min(1).describe("The non-functional requirement itself."),
24134
- implication: z7.string().min(1).describe(
25437
+ requirement: z14.string().min(1).describe("The non-functional requirement itself."),
25438
+ implication: z14.string().min(1).describe(
24135
25439
  "What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."
24136
25440
  ),
24137
- status: z7.enum(["confirmed", "assumed", "open"]).describe(
25441
+ status: z14.enum(["confirmed", "assumed", "open"]).describe(
24138
25442
  "confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved (also surface as an actionable_items card)."
24139
25443
  )
24140
25444
  });
24141
- var SystemGoalsSchema = z7.object({
24142
- business_goal: z7.string().min(1).describe("The business goal this work serves."),
24143
- desired_end_state: z7.string().min(1).describe("The end-state the system should reach."),
24144
- system_behavior: z7.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),
24145
- nfrs: z7.array(SystemGoalNfrSchema).optional().default([])
25445
+ var SystemGoalsSchema = z14.object({
25446
+ business_goal: z14.string().min(1).describe("The business goal this work serves."),
25447
+ desired_end_state: z14.string().min(1).describe("The end-state the system should reach."),
25448
+ system_behavior: z14.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),
25449
+ nfrs: z14.array(SystemGoalNfrSchema).optional().default([])
24146
25450
  });
24147
- var ImplementationOrderItemSchema = z7.object({
24148
- title: z7.string().min(1).describe("Short title of the slice / child ticket."),
24149
- depends_on: z7.array(z7.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),
24150
- recommended_after: z7.array(z7.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),
24151
- rationale: z7.string().min(1).describe("Why this slice sits at this point in the order.")
25451
+ var ImplementationOrderItemSchema = z14.object({
25452
+ title: z14.string().min(1).describe("Short title of the slice / child ticket."),
25453
+ depends_on: z14.array(z14.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),
25454
+ recommended_after: z14.array(z14.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),
25455
+ rationale: z14.string().min(1).describe("Why this slice sits at this point in the order.")
24152
25456
  });
24153
25457
  var DecisionPageInputShape = {
24154
- ticket_key: z7.string().describe("Jira ticket key, e.g. BAPI-123"),
24155
- artifact_type: z7.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
25458
+ ticket_key: z14.string().describe("Jira ticket key, e.g. BAPI-123"),
25459
+ artifact_type: z14.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
24156
25460
  'Which flavor of page to render. "review_decisions" (default) is the ticket-review decision-capture page and is unaffected by the planning fields. "pre_ticket_planning" additionally renders the read-only system_goals and implementation_order sections for pre-ticket epic/task framing.'
24157
25461
  ),
24158
25462
  system_goals: SystemGoalsSchema.optional().describe(
24159
25463
  "pre_ticket_planning only: read-only business goal, desired end-state, system behavior, and classified NFRs. Unresolved (open) NFRs should ALSO be passed as actionable_items so the human can decide them."
24160
25464
  ),
24161
- implementation_order: z7.array(ImplementationOrderItemSchema).optional().describe(
25465
+ implementation_order: z14.array(ImplementationOrderItemSchema).optional().describe(
24162
25466
  "pre_ticket_planning epic surfaces only: read-only recommended implementation order (hard depends_on vs soft recommended_after). No Jira links are created from this."
24163
25467
  ),
24164
- output_subdir: z7.string().optional().default("review").describe(
25468
+ output_subdir: z14.string().optional().default("review").describe(
24165
25469
  'Optional docs-relative subdirectory to write the page under (default "review"). Validated strictly: no absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'
24166
25470
  ),
24167
- output_filename: z7.string().optional().describe(
25471
+ output_filename: z14.string().optional().describe(
24168
25472
  'Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html and contain no path separators; the .html suffix is required and never auto-appended.'
24169
25473
  ),
24170
25474
  labels: DecisionPageLabelsSchema.optional().describe(
24171
25475
  "Optional presentation-label overrides (title, intro, section_heading, improvements_heading). Presentation-only; does not change data-testid hooks or the submitted JSON shape."
24172
25476
  ),
24173
- actionable_items: z7.array(ActionableItemSchema).optional().default([]).describe(
25477
+ actionable_items: z14.array(ActionableItemSchema).optional().default([]).describe(
24174
25478
  "Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."
24175
25479
  ),
24176
- clear_improvements: z7.array(
24177
- z7.object({
24178
- id: z7.string().min(1).describe(
25480
+ clear_improvements: z14.array(
25481
+ z14.object({
25482
+ id: z14.string().min(1).describe(
24179
25483
  "Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."
24180
25484
  ),
24181
- title: z7.string().min(1),
24182
- action: z7.string().min(1),
24183
- confidence: z7.string().min(1),
24184
- source: z7.string().min(1).describe(
25485
+ title: z14.string().min(1),
25486
+ action: z14.string().min(1),
25487
+ confidence: z14.string().min(1),
25488
+ source: z14.string().min(1).describe(
24185
25489
  "Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user \u2014 the confirmed-improvements list shows title/confidence/action only."
24186
25490
  )
24187
25491
  })
24188
25492
  ).optional().default([]).describe("Confirmed improvements displayed as informational list, not submitted.")
24189
25493
  };
24190
- var DecisionPageInputSchema = z7.object(DecisionPageInputShape);
25494
+ var DecisionPageInputSchema = z14.object(DecisionPageInputShape);
24191
25495
  var DecisionPageLeanInputShape = {
24192
- ticket_key: z7.string().describe("Jira ticket key, e.g. BAPI-123"),
24193
- artifact_type: z7.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
25496
+ ticket_key: z14.string().describe("Jira ticket key, e.g. BAPI-123"),
25497
+ artifact_type: z14.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
24194
25498
  'Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'
24195
25499
  ),
24196
- output_subdir: z7.string().optional().default("review").describe(
25500
+ output_subdir: z14.string().optional().default("review").describe(
24197
25501
  'Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'
24198
25502
  ),
24199
- output_filename: z7.string().optional().describe(
25503
+ output_filename: z14.string().optional().describe(
24200
25504
  'Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'
24201
25505
  ),
24202
25506
  labels: DecisionPageLabelsSchema.optional().describe(
24203
25507
  "Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."
24204
25508
  ),
24205
- content: z7.record(z7.string(), z7.unknown()).optional().describe(
25509
+ content: z14.record(z14.string(), z14.unknown()).optional().describe(
24206
25510
  "Contains deferred heavy payloads like actionable_items or system_goals."
24207
25511
  )
24208
25512
  };
@@ -26754,6 +28058,60 @@ async function runVisualDiff(input, deps) {
26754
28058
  }
26755
28059
  }
26756
28060
 
28061
+ // src/estimate-epic.ts
28062
+ function validateEstimateEpicInput(input) {
28063
+ const hasEpic = typeof input.epic_key === "string" && input.epic_key.trim().length > 0;
28064
+ const hasKeys = Array.isArray(input.ticket_keys);
28065
+ if (hasEpic && hasKeys) {
28066
+ return "epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.";
28067
+ }
28068
+ if (!hasEpic && !hasKeys) {
28069
+ return "Exactly one of epic_key or ticket_keys is required.";
28070
+ }
28071
+ if (hasKeys && input.ticket_keys.length === 0) {
28072
+ return "ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.";
28073
+ }
28074
+ return null;
28075
+ }
28076
+ function buildEstimateEpicErrorEnvelope(code, message, extras) {
28077
+ return JSON.stringify({ error: code, message, ...extras ?? {} }, null, 2);
28078
+ }
28079
+ async function runEstimateEpic(input, deps) {
28080
+ const validationError = validateEstimateEpicInput(input);
28081
+ if (validationError) {
28082
+ return {
28083
+ content: [{ type: "text", text: buildEstimateEpicErrorEnvelope("VALIDATION_ERROR", validationError) }]
28084
+ };
28085
+ }
28086
+ const payload = { repo_name: deps.repoName };
28087
+ if (typeof input.epic_key === "string") payload.epic_key = input.epic_key;
28088
+ if (Array.isArray(input.ticket_keys)) payload.ticket_keys = input.ticket_keys;
28089
+ if (typeof input.allow_partial === "boolean") payload.allow_partial = input.allow_partial;
28090
+ const fetchImpl = deps.fetchImpl ?? fetch;
28091
+ let resp;
28092
+ try {
28093
+ resp = await fetchImpl(deps.buildUrl("/estimate-epic"), {
28094
+ method: "POST",
28095
+ headers: await deps.getPostHeaders(),
28096
+ body: JSON.stringify(payload)
28097
+ });
28098
+ } catch {
28099
+ return {
28100
+ content: [
28101
+ {
28102
+ type: "text",
28103
+ text: buildEstimateEpicErrorEnvelope(
28104
+ "NETWORK_ERROR",
28105
+ "Failed to reach the Bridge API estimate-epic endpoint."
28106
+ )
28107
+ }
28108
+ ]
28109
+ };
28110
+ }
28111
+ const text = await deps.handleResponse(resp);
28112
+ return { content: [{ type: "text", text }] };
28113
+ }
28114
+
26757
28115
  // src/index.ts
26758
28116
  var PIPELINES2 = { ...PIPELINES };
26759
28117
  var INSTRUCTIONS2 = { ...INSTRUCTIONS };
@@ -27110,7 +28468,21 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
27110
28468
  ".xlsx",
27111
28469
  ".pptx"
27112
28470
  ]);
27113
- async function resolveUploadAttachment(textValue, filePath, textLabel) {
28471
+ var ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION = {
28472
+ ".png": "image/png",
28473
+ ".jpg": "image/jpeg",
28474
+ ".jpeg": "image/jpeg",
28475
+ ".webp": "image/webp",
28476
+ ".gif": "image/gif"
28477
+ };
28478
+ var ALLOWED_BINARY_UPLOAD_MIME_TYPES = Array.from(
28479
+ new Set(Object.values(ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION))
28480
+ ).sort();
28481
+ function deriveAllowedBinaryUploadMimeType(effectiveFileName) {
28482
+ const ext = path32.extname(effectiveFileName).toLowerCase();
28483
+ return ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION[ext];
28484
+ }
28485
+ async function resolveUploadAttachment(textValue, filePath, textLabel, effectiveFileName) {
27114
28486
  if (!filePath && !textValue) {
27115
28487
  return {
27116
28488
  ok: false,
@@ -27152,7 +28524,23 @@ async function resolveUploadAttachment(textValue, filePath, textLabel) {
27152
28524
 
27153
28525
  Note: Both file_path and ${textLabel} were provided. file_path content was used.` : "";
27154
28526
  if (isBinary) {
27155
- return { ok: true, text: buf.toString("base64"), encoding: "base64", note };
28527
+ const contentType = deriveAllowedBinaryUploadMimeType(effectiveFileName);
28528
+ if (!contentType) {
28529
+ return {
28530
+ ok: false,
28531
+ errorResponse: {
28532
+ content: [{
28533
+ type: "text",
28534
+ text: JSON.stringify({
28535
+ error: "BAD_REQUEST",
28536
+ status: 400,
28537
+ message: `Unsupported attachment type for binary upload: ${effectiveFileName}. Allowed types: ${ALLOWED_BINARY_UPLOAD_MIME_TYPES.join(", ")}.`
28538
+ })
28539
+ }]
28540
+ }
28541
+ };
28542
+ }
28543
+ return { ok: true, text: buf.toString("base64"), encoding: "base64", note, contentType };
27156
28544
  }
27157
28545
  if (buf.length > 1048576) {
27158
28546
  return {
@@ -27204,6 +28592,11 @@ async function pollForResult(getUrl, timeoutMs, label) {
27204
28592
  }
27205
28593
  }
27206
28594
  }
28595
+ function normalizeReviewRounds(rounds) {
28596
+ if (rounds === 1 || rounds === "1") return 1;
28597
+ if (rounds === 2 || rounds === "2") return 2;
28598
+ return void 0;
28599
+ }
27207
28600
  var TICKET_ARTIFACTS = {
27208
28601
  plan: {
27209
28602
  kind: "single",
@@ -27296,6 +28689,10 @@ function buildTicketArtifactRequestBody(args) {
27296
28689
  if (trimmedProvider && !trimmedSecondOpinion) {
27297
28690
  body.provider = trimmedProvider;
27298
28691
  }
28692
+ const normalizedRounds = normalizeReviewRounds(args.rounds);
28693
+ if (normalizedRounds !== void 0) {
28694
+ body.rounds = normalizedRounds;
28695
+ }
27299
28696
  return body;
27300
28697
  }
27301
28698
  async function getTicketArtifactDocsPath(subdir) {
@@ -27641,16 +29038,16 @@ var registerTool = ((name, config, handler) => {
27641
29038
  return toolHandle;
27642
29039
  });
27643
29040
  var commonFields = {
27644
- ticket_number: z8.string(),
27645
- repo_name: z8.string().optional(),
27646
- save_locally: z8.boolean().optional().default(true),
27647
- wait_for_result: z8.boolean().optional().default(false).describe(
29041
+ ticket_number: z15.string(),
29042
+ repo_name: z15.string().optional(),
29043
+ save_locally: z15.boolean().optional().default(true),
29044
+ wait_for_result: z15.boolean().optional().default(false).describe(
27648
29045
  "When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."
27649
29046
  ),
27650
- second_opinion: z8.string().optional().describe(
29047
+ second_opinion: z15.string().optional().describe(
27651
29048
  "Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."
27652
29049
  ),
27653
- provider: z8.string().optional().describe(
29050
+ provider: z15.string().optional().describe(
27654
29051
  "Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics."
27655
29052
  )
27656
29053
  };
@@ -27713,13 +29110,13 @@ registerTool(
27713
29110
  },
27714
29111
  description: "Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider. This does NOT create or retrieve a Bridge artifact (use request_* tools for that).",
27715
29112
  inputSchema: {
27716
- prompt: z8.string().describe(
29113
+ prompt: z15.string().describe(
27717
29114
  "The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."
27718
29115
  ),
27719
- provider: z8.enum(["anthropic", "openai", "gemini"]).describe(
29116
+ provider: z15.enum(["anthropic", "openai", "gemini"]).describe(
27720
29117
  "LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."
27721
29118
  ),
27722
- model: z8.enum(["CHEAP_MODEL", "BASIC_MODEL", "PREMIUM_MODEL"]).describe(
29119
+ model: z15.enum(["CHEAP_MODEL", "BASIC_MODEL", "PREMIUM_MODEL"]).describe(
27723
29120
  "Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback."
27724
29121
  )
27725
29122
  }
@@ -27800,10 +29197,10 @@ registerTool(
27800
29197
  },
27801
29198
  description: "Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",
27802
29199
  inputSchema: {
27803
- prompt: z8.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),
27804
- provider: z8.enum(["openai", "gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),
27805
- quality: z8.enum(["low", "medium", "high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),
27806
- size: z8.enum(["1024x1024", "1024x1536", "1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")
29200
+ prompt: z15.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),
29201
+ provider: z15.enum(["openai", "gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),
29202
+ quality: z15.enum(["low", "medium", "high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),
29203
+ size: z15.enum(["1024x1024", "1024x1536", "1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")
27807
29204
  }
27808
29205
  },
27809
29206
  async ({ prompt, provider, quality, size }) => {
@@ -27931,16 +29328,16 @@ registerTool(
27931
29328
  },
27932
29329
  description: "Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",
27933
29330
  inputSchema: {
27934
- target_url: z8.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),
27935
- comp_ref: z8.string().min(1).describe(
29331
+ target_url: z15.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),
29332
+ comp_ref: z15.string().min(1).describe(
27936
29333
  "The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."
27937
29334
  ),
27938
- viewport: z8.object({
27939
- width: z8.number().int().positive(),
27940
- height: z8.number().int().positive()
29335
+ viewport: z15.object({
29336
+ width: z15.number().int().positive(),
29337
+ height: z15.number().int().positive()
27941
29338
  }).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),
27942
- mask_selectors: z8.array(z8.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),
27943
- threshold: z8.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")
29339
+ mask_selectors: z15.array(z15.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),
29340
+ threshold: z15.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")
27944
29341
  }
27945
29342
  },
27946
29343
  async (args) => {
@@ -27997,16 +29394,16 @@ registerTool(
27997
29394
  },
27998
29395
  description: "Search for and list Jira tickets from the configured project. Filters by query text, status name, label, or date. Returns up to 'limit' tickets ordered by most recently updated. All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",
27999
29396
  inputSchema: {
28000
- query: z8.string().optional().describe(
29397
+ query: z15.string().optional().describe(
28001
29398
  `Free-text search string. Filters tickets via JQL text ~ '...' (searches summary, description, comments). Examples: "authentication error", "login page crash", "payment timeout"`
28002
29399
  ),
28003
- status: z8.string().optional().describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),
28004
- labels: z8.string().optional().describe(
29400
+ status: z15.string().optional().describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),
29401
+ labels: z15.string().optional().describe(
28005
29402
  'Comma-separated Jira labels. Filters tickets via JQL labels in (...) (matches tickets carrying any of the given labels). Labels cannot contain spaces. Example: "bapi-idea-to-ticket-fa-1a2b3c"'
28006
29403
  ),
28007
- limit: z8.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),
28008
- offset: z8.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),
28009
- updated_since: z8.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")
29404
+ limit: z15.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),
29405
+ offset: z15.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),
29406
+ updated_since: z15.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")
28010
29407
  }
28011
29408
  },
28012
29409
  async ({ query, status, labels, limit, offset, updated_since }) => {
@@ -28142,18 +29539,18 @@ registerTool(
28142
29539
  },
28143
29540
  description: "Create a new Jira ticket in the configured project. Requires either description or file_path (or both \u2014 file_path takes precedence). Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. The ticket is created immediately in Jira \u2014 confirm details with the user before calling. The description field supports Jira markdown formatting. Pass parent_key ONLY when creating a child ticket under an existing Jira Epic; omit it for standalone tickets and for Epic parent creation itself.",
28144
29541
  inputSchema: {
28145
- summary: z8.string().describe("Ticket title \u2014 keep under 100 characters"),
28146
- description: z8.string().optional().describe(
29542
+ summary: z15.string().describe("Ticket title \u2014 keep under 100 characters"),
29543
+ description: z15.string().optional().describe(
28147
29544
  "Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"
28148
29545
  ),
28149
- file_path: z8.string().optional().describe(
29546
+ file_path: z15.string().optional().describe(
28150
29547
  "Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
28151
29548
  ),
28152
- issue_type: z8.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),
28153
- priority: z8.string().optional().describe("One of: 'Highest', 'High', 'Medium', 'Low', 'Lowest'. Omit to use Jira project default"),
28154
- labels: z8.array(z8.string()).optional().describe("List of Jira labels to apply (e.g. ['frontend', 'tech-debt'])"),
28155
- assignee: z8.string().optional().describe("Jira username or account ID of the assignee. Omit to leave unassigned"),
28156
- parent_key: z8.string().optional().describe(
29549
+ issue_type: z15.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),
29550
+ priority: z15.string().optional().describe("One of: 'Highest', 'High', 'Medium', 'Low', 'Lowest'. Omit to use Jira project default"),
29551
+ labels: z15.array(z15.string()).optional().describe("List of Jira labels to apply (e.g. ['frontend', 'tech-debt'])"),
29552
+ assignee: z15.string().optional().describe("Jira username or account ID of the assignee. Omit to leave unassigned"),
29553
+ parent_key: z15.string().optional().describe(
28157
29554
  "Optional Jira Epic key to set as the parent of the newly created child issue. Omit for standalone tickets and Epic parent creation."
28158
29555
  )
28159
29556
  }
@@ -28252,7 +29649,7 @@ registerTool(
28252
29649
  },
28253
29650
  description: "Queue a background job to parse and index the repository for Bridge API's AI agents. The API only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. This should be run after major codebase changes so that plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. The job runs asynchronously \u2014 there is no completion callback; poll get_parse_status to observe when it reaches terminal success or terminal failure. For large repositories this may take several minutes. Confirm with the user before triggering.",
28254
29651
  inputSchema: {
28255
- directory_path: z8.string().optional().describe(
29652
+ directory_path: z15.string().optional().describe(
28256
29653
  "Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository"
28257
29654
  )
28258
29655
  }
@@ -28329,14 +29726,14 @@ registerTool(
28329
29726
  description: "Post a comment on a Jira ticket. The comment appears immediately in Jira. Supports markdown formatting. For long comments (over ~2000 characters), set attach_as_file to true \u2014 this attaches the comment as a .md file instead of posting inline, which avoids Jira's comment length limitations.\n\nTip: To generate plans, clarifying questions, or ticket critiques, use the dedicated request_plan_generation, request_clarifying_questions, or request_ticket_critique tools.",
28330
29727
  inputSchema: {
28331
29728
  ticket_number: commonFields.ticket_number,
28332
- comment: z8.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),
28333
- file_path: z8.string().optional().describe(
29729
+ comment: z15.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),
29730
+ file_path: z15.string().optional().describe(
28334
29731
  "Path to a local markdown file whose contents will be used as the comment. If both file_path and comment are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
28335
29732
  ),
28336
- attach_as_file: z8.boolean().optional().default(false).describe(
29733
+ attach_as_file: z15.boolean().optional().default(false).describe(
28337
29734
  "Set to true to attach the comment as a .md file instead of posting inline. Recommended for comments over 2000 characters"
28338
29735
  ),
28339
- file_name: z8.string().optional().describe(
29736
+ file_name: z15.string().optional().describe(
28340
29737
  "Custom filename for the attached .md file (only used when attach_as_file is true). Defaults to {ticket_number}-comment.md if not provided. Example: 'PROJ-123-clarifying-questions.md'"
28341
29738
  )
28342
29739
  }
@@ -28376,8 +29773,8 @@ registerTool(
28376
29773
  description: "Update the description of an existing Jira ticket. This is a direct, synchronous update that overwrites the existing description with the provided text. The description should be in markdown format \u2014 it will be automatically converted to Jira wiki markup. This does NOT create a new ticket. Use create_ticket for that. Returns a success message with the ticket number, or an error if the update fails.",
28377
29774
  inputSchema: {
28378
29775
  ticket_number: commonFields.ticket_number,
28379
- description: z8.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),
28380
- file_path: z8.string().optional().describe(
29776
+ description: z15.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),
29777
+ file_path: z15.string().optional().describe(
28381
29778
  "Path to a local markdown file whose contents will be used as the new description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
28382
29779
  )
28383
29780
  }
@@ -28406,40 +29803,40 @@ registerTool(
28406
29803
  idempotentHint: false,
28407
29804
  openWorldHint: true
28408
29805
  },
28409
- description: "Manages Jira attachments. Operations: upload, download, list. download saves binary/image attachments (e.g. PNG/JPEG design comps) as raw bytes to a file_path inside the worktree/project root and reports the saved path; UTF-8 text attachments are returned inline.",
28410
- inputSchema: z8.discriminatedUnion("operation", [
28411
- z8.object({
28412
- operation: z8.literal("upload"),
29806
+ description: "Manages Jira attachments. Operations: upload, download, list. upload: text or image/png, image/jpeg, image/webp, image/gif (10 MB max), else rejected. download saves binary/image attachments (design comps) as raw bytes to a file_path inside the worktree/project root and reports the saved path; UTF-8 text attachments are returned inline.",
29807
+ inputSchema: z15.discriminatedUnion("operation", [
29808
+ z15.object({
29809
+ operation: z15.literal("upload"),
28413
29810
  ticket_number: commonFields.ticket_number,
28414
- file_path: z8.string().optional().describe(
28415
- "Path to a local file to upload. Binary files up to `10 MB`; text up to `1 MB`. If both file_path and content are provided, file_path takes precedence."
29811
+ file_path: z15.string().optional().describe(
29812
+ "Path to a local file to upload. Binary uploads are restricted to the allowlisted image types image/png, image/jpeg, image/webp, image/gif, up to `10 MB`; other binaries such as PDFs and ZIPs are rejected as unsupported attachment types. Text uploads are up to `1 MB`. If both file_path and content are provided, file_path takes precedence."
28416
29813
  ),
28417
- content: z8.string().max(1048576).optional().describe("Inline text content to upload (max `1 MB`). Optional if file_path is provided."),
28418
- file_name: z8.string().optional().describe(
29814
+ content: z15.string().max(1048576).optional().describe("Inline text content to upload (max `1 MB`). Optional if file_path is provided."),
29815
+ file_name: z15.string().optional().describe(
28419
29816
  "Filename for the attachment in Jira. Defaults to the basename of file_path if provided, or {ticket_number}-attachment.md otherwise."
28420
29817
  ),
28421
- link_type: z8.string().optional().describe(
29818
+ link_type: z15.string().optional().describe(
28422
29819
  "When provided, also syncs the content to Bridge API's tickets_links table. Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. Cannot be used with binary file uploads."
28423
29820
  ),
28424
- replace_existing: z8.boolean().optional().default(true).describe(
29821
+ replace_existing: z15.boolean().optional().default(true).describe(
28425
29822
  "When true (default), deletes any existing attachment with the same filename before uploading."
28426
29823
  )
28427
29824
  }).strict(),
28428
- z8.object({
28429
- operation: z8.literal("download"),
29825
+ z15.object({
29826
+ operation: z15.literal("download"),
28430
29827
  ticket_number: commonFields.ticket_number,
28431
- attachment_id: z8.string().optional().describe(
29828
+ attachment_id: z15.string().optional().describe(
28432
29829
  "Jira attachment ID. Mutually exclusive with filename. For design/UI tickets, pass the attachment_id from the plan's DESIGN COMP CANDIDATES section to fetch the design comp."
28433
29830
  ),
28434
- filename: z8.string().optional().describe("Attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),
28435
- file_path: z8.string().optional().describe(
29831
+ filename: z15.string().optional().describe("Attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),
29832
+ file_path: z15.string().optional().describe(
28436
29833
  "Override the default save location (must stay within the project root/worktree). Pass a file_path when you need to open an image/design comp locally. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}."
28437
29834
  )
28438
29835
  }).strict(),
28439
- z8.object({
28440
- operation: z8.literal("list"),
29836
+ z15.object({
29837
+ operation: z15.literal("list"),
28441
29838
  ticket_number: commonFields.ticket_number,
28442
- include_ai_generated: z8.boolean().optional().describe("Include AI-generated attachments in the list (default: false)")
29839
+ include_ai_generated: z15.boolean().optional().describe("Include AI-generated attachments in the list (default: false)")
28443
29840
  }).strict()
28444
29841
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28445
29842
  ])
@@ -28449,9 +29846,9 @@ registerTool(
28449
29846
  switch (args.operation) {
28450
29847
  case "upload": {
28451
29848
  const { ticket_number, file_path, content, file_name, link_type, replace_existing } = args;
28452
- const resolved = await resolveUploadAttachment(content, file_path, "content");
28453
- if (!resolved.ok) return resolved.errorResponse;
28454
29849
  const derivedFileName = file_name || (file_path ? path32.basename(file_path) : `${ticket_number}-attachment.md`);
29850
+ const resolved = await resolveUploadAttachment(content, file_path, "content", derivedFileName);
29851
+ if (!resolved.ok) return resolved.errorResponse;
28455
29852
  const payload = {
28456
29853
  repo_name: REPO_NAME,
28457
29854
  content: resolved.text,
@@ -28461,6 +29858,9 @@ registerTool(
28461
29858
  if (resolved.encoding) {
28462
29859
  payload.encoding = resolved.encoding;
28463
29860
  }
29861
+ if (resolved.contentType) {
29862
+ payload.content_type = resolved.contentType;
29863
+ }
28464
29864
  if (link_type) {
28465
29865
  payload.link_type = link_type;
28466
29866
  }
@@ -28601,6 +30001,31 @@ registerTool(
28601
30001
  return requestTicketArtifact("plan", args);
28602
30002
  }
28603
30003
  );
30004
+ registerTool(
30005
+ "estimate_epic",
30006
+ {
30007
+ annotations: {
30008
+ readOnlyHint: false,
30009
+ destructiveHint: false,
30010
+ idempotentHint: false,
30011
+ openWorldHint: true
30012
+ },
30013
+ description: "Estimate a Jira Epic or ticket-key group via the epic estimation orchestrator. Exactly one of epic_key/ticket_keys required (never both). allow_partial allows a partial result on child failures (default: fail-closed). No mode field; source is inferred.",
30014
+ inputSchema: {
30015
+ epic_key: z15.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),
30016
+ ticket_keys: z15.array(z15.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),
30017
+ allow_partial: z15.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")
30018
+ }
30019
+ },
30020
+ async (args) => {
30021
+ return await runEstimateEpic(args, {
30022
+ repoName: REPO_NAME,
30023
+ buildUrl,
30024
+ getPostHeaders,
30025
+ handleResponse
30026
+ });
30027
+ }
30028
+ );
28604
30029
  registerTool(
28605
30030
  "request_architecture",
28606
30031
  {
@@ -28657,15 +30082,15 @@ registerTool(
28657
30082
  description: "Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",
28658
30083
  inputSchema: {
28659
30084
  ticket_number: commonFields.ticket_number,
28660
- doc_type: z8.enum(["tdd", "fsd", "prd"]).describe(
30085
+ doc_type: z15.enum(["tdd", "fsd", "prd"]).describe(
28661
30086
  "Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."
28662
30087
  ),
28663
30088
  wait_for_result: commonFields.wait_for_result,
28664
30089
  save_locally: commonFields.save_locally,
28665
- second_opinion: z8.string().optional().describe(
30090
+ second_opinion: z15.string().optional().describe(
28666
30091
  "Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."
28667
30092
  ),
28668
- provider: z8.string().optional().describe(
30093
+ provider: z15.string().optional().describe(
28669
30094
  "Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence."
28670
30095
  )
28671
30096
  }
@@ -28686,7 +30111,7 @@ registerTool(
28686
30111
  description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",
28687
30112
  inputSchema: {
28688
30113
  ticket_number: commonFields.ticket_number,
28689
- doc_type: z8.enum(["tdd", "fsd", "prd"]).describe(
30114
+ doc_type: z15.enum(["tdd", "fsd", "prd"]).describe(
28690
30115
  "Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."
28691
30116
  ),
28692
30117
  save_locally: commonFields.save_locally
@@ -28774,7 +30199,16 @@ registerTool(
28774
30199
  wait_for_result: commonFields.wait_for_result,
28775
30200
  save_locally: commonFields.save_locally,
28776
30201
  second_opinion: commonFields.second_opinion,
28777
- provider: commonFields.provider
30202
+ provider: commonFields.provider,
30203
+ rounds: z15.union([
30204
+ z15.literal(1),
30205
+ z15.literal(2),
30206
+ z15.literal("1"),
30207
+ z15.literal("2"),
30208
+ z15.literal("")
30209
+ ]).optional().describe(
30210
+ "Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing."
30211
+ )
28778
30212
  }
28779
30213
  },
28780
30214
  async (args) => {
@@ -28834,7 +30268,7 @@ registerTool(
28834
30268
  description: "Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",
28835
30269
  inputSchema: {
28836
30270
  ticket_number: commonFields.ticket_number,
28837
- description: z8.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")
30271
+ description: z15.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")
28838
30272
  }
28839
30273
  },
28840
30274
  async ({ ticket_number, description }) => {
@@ -28864,7 +30298,7 @@ registerTool(
28864
30298
  description: "Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",
28865
30299
  inputSchema: {
28866
30300
  ticket_number: commonFields.ticket_number,
28867
- fields: z8.array(z8.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")
30301
+ fields: z15.array(z15.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")
28868
30302
  }
28869
30303
  },
28870
30304
  async ({ ticket_number, fields }) => {
@@ -28941,8 +30375,8 @@ registerTool(
28941
30375
  description: 'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',
28942
30376
  inputSchema: {
28943
30377
  ticket_number: commonFields.ticket_number,
28944
- target_status: z8.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),
28945
- transition_id: z8.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")
30378
+ target_status: z15.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),
30379
+ transition_id: z15.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")
28946
30380
  }
28947
30381
  },
28948
30382
  async ({ ticket_number, target_status, transition_id }) => {
@@ -28973,7 +30407,7 @@ registerTool(
28973
30407
  description: "Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",
28974
30408
  inputSchema: {
28975
30409
  ticket_number: commonFields.ticket_number,
28976
- force_rerun: z8.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")
30410
+ force_rerun: z15.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")
28977
30411
  }
28978
30412
  },
28979
30413
  async ({ ticket_number, force_rerun }) => {
@@ -29040,35 +30474,35 @@ registerTool(
29040
30474
  openWorldHint: true
29041
30475
  },
29042
30476
  description: "Manages Bridge API configuration fields. Operations: get, update, list.",
29043
- inputSchema: z8.discriminatedUnion("operation", [
29044
- z8.object({
29045
- operation: z8.literal("get"),
29046
- field_name: z8.string().describe(
30477
+ inputSchema: z15.discriminatedUnion("operation", [
30478
+ z15.object({
30479
+ operation: z15.literal("get"),
30480
+ field_name: z15.string().describe(
29047
30481
  `Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`
29048
30482
  )
29049
30483
  }).strict(),
29050
- z8.object({
29051
- operation: z8.literal("update"),
29052
- field_name: z8.string().describe(
30484
+ z15.object({
30485
+ operation: z15.literal("update"),
30486
+ field_name: z15.string().describe(
29053
30487
  `The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`
29054
30488
  ),
29055
- value: z8.union([
29056
- z8.string(),
29057
- z8.boolean(),
29058
- z8.array(z8.string()),
29059
- z8.record(z8.string(), z8.union([z8.string(), z8.null()]))
30489
+ value: z15.union([
30490
+ z15.string(),
30491
+ z15.boolean(),
30492
+ z15.array(z15.string()),
30493
+ z15.record(z15.string(), z15.union([z15.string(), z15.null()]))
29060
30494
  ]).optional().describe(
29061
30495
  `The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`
29062
30496
  ),
29063
- file_path: z8.string().optional().describe(
30497
+ file_path: z15.string().optional().describe(
29064
30498
  "Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."
29065
30499
  ),
29066
- only_if_null: z8.boolean().optional().describe(
30500
+ only_if_null: z15.boolean().optional().describe(
29067
30501
  "Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest."
29068
30502
  )
29069
30503
  }).strict(),
29070
- z8.object({
29071
- operation: z8.literal("list")
30504
+ z15.object({
30505
+ operation: z15.literal("list")
29072
30506
  }).strict()
29073
30507
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29074
30508
  ])
@@ -29278,10 +30712,10 @@ registerTool(
29278
30712
  },
29279
30713
  description: 'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',
29280
30714
  inputSchema: {
29281
- snapshot_token: z8.string().describe(
30715
+ snapshot_token: z15.string().describe(
29282
30716
  "The exact snapshot_token returned by get_install_manifest for this repository."
29283
30717
  ),
29284
- fields: z8.record(z8.string(), z8.any()).describe(
30718
+ fields: z15.record(z15.string(), z15.any()).describe(
29285
30719
  'Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.'
29286
30720
  )
29287
30721
  }
@@ -29328,7 +30762,7 @@ registerTool(
29328
30762
  },
29329
30763
  description: "Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",
29330
30764
  inputSchema: {
29331
- repo_name: z8.string().describe(
30765
+ repo_name: z15.string().describe(
29332
30766
  "The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process."
29333
30767
  )
29334
30768
  }
@@ -29463,10 +30897,10 @@ registerTool(
29463
30897
  },
29464
30898
  description: "Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",
29465
30899
  inputSchema: {
29466
- query: z8.string().describe(
30900
+ query: z15.string().describe(
29467
30901
  "The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"
29468
30902
  ),
29469
- context: z8.string().optional().describe(
30903
+ context: z15.string().optional().describe(
29470
30904
  "Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"
29471
30905
  ),
29472
30906
  ticket_number: commonFields.ticket_number.optional(),
@@ -29561,10 +30995,10 @@ registerTool(
29561
30995
  },
29562
30996
  description: "RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",
29563
30997
  inputSchema: {
29564
- task_id: z8.number().describe(
30998
+ task_id: z15.number().describe(
29565
30999
  "The task ID returned by request_deep_research."
29566
31000
  ),
29567
- query_slug: z8.string().optional().describe(
31001
+ query_slug: z15.string().optional().describe(
29568
31002
  "Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."
29569
31003
  ),
29570
31004
  save_locally: commonFields.save_locally
@@ -29686,32 +31120,32 @@ registerTool(
29686
31120
  },
29687
31121
  description: "Use to start an async brainstorm that fans out a task to opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_brainstorm to retrieve. Generates and persists a retrievable artifact.",
29688
31122
  inputSchema: {
29689
- task_description: z8.string().describe(
31123
+ task_description: z15.string().describe(
29690
31124
  "Free-form description of the task to brainstorm about. Sent verbatim \u2014 this tool does NOT read task_description from a file."
29691
31125
  ),
29692
31126
  repo_name: commonFields.repo_name,
29693
31127
  ticket_number: commonFields.ticket_number.optional(),
29694
- providers: z8.array(z8.string()).optional().describe(
31128
+ providers: z15.array(z15.string()).optional().describe(
29695
31129
  "Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."
29696
31130
  ),
29697
- concerns: z8.string().optional().describe(
31131
+ concerns: z15.string().optional().describe(
29698
31132
  "Optional caller-supplied concerns to surface to the brainstorm agents."
29699
31133
  ),
29700
31134
  wait_for_result: commonFields.wait_for_result,
29701
31135
  save_locally: commonFields.save_locally,
29702
- prior_brainstorm_id: z8.string().optional().describe(
31136
+ prior_brainstorm_id: z15.string().optional().describe(
29703
31137
  "Optional brainstorm_id from an earlier brainstorm to refine. When provided, the prior brainstorm's completed opinion-provider markdowns are concatenated and supplied as prior context."
29704
31138
  ),
29705
- mode: z8.enum(["technical", "design", "discovery"]).optional().describe(
31139
+ mode: z15.enum(["technical", "design", "discovery"]).optional().describe(
29706
31140
  "Preferred brainstorm-mode selector for new callers. 'technical' (default) is the implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped technical and business/stakeholder discovery questions for early/vague tasks. Takes precedence over the legacy boolean design field."
29707
31141
  ),
29708
- design: z8.boolean().optional().describe(
31142
+ design: z15.boolean().optional().describe(
29709
31143
  'Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'
29710
31144
  ),
29711
- lenses: z8.array(z8.string()).optional().describe(
31145
+ lenses: z15.array(z15.string()).optional().describe(
29712
31146
  "Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."
29713
31147
  ),
29714
- debate: z8.boolean().optional().describe(
31148
+ debate: z15.boolean().optional().describe(
29715
31149
  "Opt-in to trigger a second cross-examination debate round between providers (default off). When true, after round 1 completes each provider critiques the OTHER provider(s)' round-1 output, and the critique is appended to that provider's markdown under a '## Cross-examination' section."
29716
31150
  )
29717
31151
  }
@@ -29807,7 +31241,7 @@ registerTool(
29807
31241
  },
29808
31242
  description: "Use to retrieve the result envelope for a previously submitted brainstorm by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new brainstorm \u2014 use request_brainstorm first if none exists. Returns not-found when still processing.",
29809
31243
  inputSchema: {
29810
- brainstorm_id: z8.string().describe(
31244
+ brainstorm_id: z15.string().describe(
29811
31245
  "The brainstorm_id (UUID) returned by request_brainstorm."
29812
31246
  ),
29813
31247
  repo_name: commonFields.repo_name,
@@ -29850,10 +31284,10 @@ registerTool(
29850
31284
  },
29851
31285
  description: "Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",
29852
31286
  inputSchema: {
29853
- head_branch: z8.string().describe("The source branch name for the pull request"),
29854
- base_branch: z8.string().describe("The target/destination branch name for the pull request"),
29855
- title: z8.string().describe("The title of the pull request"),
29856
- body: z8.string().optional().describe("The description/body of the pull request")
31287
+ head_branch: z15.string().describe("The source branch name for the pull request"),
31288
+ base_branch: z15.string().describe("The target/destination branch name for the pull request"),
31289
+ title: z15.string().describe("The title of the pull request"),
31290
+ body: z15.string().optional().describe("The description/body of the pull request")
29857
31291
  }
29858
31292
  },
29859
31293
  async ({ head_branch, base_branch, title, body }) => {
@@ -29887,8 +31321,8 @@ var resolveCiChecksTool = registerTool(
29887
31321
  },
29888
31322
  description: "Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",
29889
31323
  inputSchema: {
29890
- commit_ref: z8.string().describe("Git commit SHA to discover checks for"),
29891
- force_rerun: z8.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")
31324
+ commit_ref: z15.string().describe("Git commit SHA to discover checks for"),
31325
+ force_rerun: z15.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")
29892
31326
  }
29893
31327
  },
29894
31328
  async ({ commit_ref, force_rerun }) => {
@@ -29927,7 +31361,7 @@ var pollCiChecksTool = registerTool(
29927
31361
  },
29928
31362
  description: "Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",
29929
31363
  inputSchema: {
29930
- commit_ref: z8.string().describe("Git commit SHA to poll CI checks for")
31364
+ commit_ref: z15.string().describe("Git commit SHA to poll CI checks for")
29931
31365
  }
29932
31366
  },
29933
31367
  async ({ commit_ref }) => {
@@ -30024,19 +31458,22 @@ registerTool(
30024
31458
  },
30025
31459
  description: "Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",
30026
31460
  inputSchema: {
30027
- pipeline: z8.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
30028
- variables: z8.record(z8.string(), z8.string()).optional().describe(
31461
+ pipeline: z15.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
31462
+ variables: z15.record(z15.string(), z15.string()).optional().describe(
30029
31463
  "Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"
30030
31464
  ),
30031
- skip_steps: z8.array(z8.string()).optional().describe(
31465
+ skip_steps: z15.array(z15.string()).optional().describe(
30032
31466
  "Step tool names or descriptions to omit from the recipe"
30033
31467
  ),
30034
- auto_approve: z8.boolean().optional().describe(
30035
- "When true, automatically approve all approval-gated steps. For implement-ticket this skips the commit/push approval pause; for review-ticket this skips the HTML decision page and selects each item's recommended option. Pass via this top-level parameter, not via the variables map."
31468
+ auto_approve: z15.boolean().optional().describe(
31469
+ "When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."
31470
+ ),
31471
+ rounds: z15.union([z15.literal(1), z15.literal(2)]).optional().describe(
31472
+ "Round count (1|2); wins over adaptive routing. Omit for backend auto-routing."
30036
31473
  )
30037
31474
  }
30038
31475
  },
30039
- async ({ pipeline: pipelineName, variables, skip_steps, auto_approve }) => {
31476
+ async ({ pipeline: pipelineName, variables, skip_steps, auto_approve, rounds }) => {
30040
31477
  await ensureCustomPipelinesLoaded();
30041
31478
  const pipelineDef = PIPELINES2[pipelineName];
30042
31479
  if (!pipelineDef) {
@@ -30068,6 +31505,7 @@ registerTool(
30068
31505
  const mergedVariables = {
30069
31506
  docs_dir: await getDocsDir(),
30070
31507
  provider: "",
31508
+ rounds: "",
30071
31509
  second_opinion: "",
30072
31510
  auto_approve: auto_approve ? "true" : "",
30073
31511
  // BAPI-474: default the fresh-base materialization variables to "" so
@@ -30081,11 +31519,15 @@ registerTool(
30081
31519
  if ("idea" in mergedVariables) {
30082
31520
  mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
30083
31521
  }
31522
+ if (rounds === 1 || rounds === 2) {
31523
+ mergedVariables.rounds = String(rounds);
31524
+ }
31525
+ const effectiveSkipSteps = skip_steps ? [...skip_steps] : [];
30084
31526
  const recipe = resolveRecipe(
30085
31527
  pipelineDef,
30086
31528
  INSTRUCTIONS2,
30087
31529
  mergedVariables,
30088
- skip_steps,
31530
+ effectiveSkipSteps,
30089
31531
  !!auto_approve,
30090
31532
  { includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED }
30091
31533
  );
@@ -30143,13 +31585,13 @@ registerTool(
30143
31585
  },
30144
31586
  description: 'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',
30145
31587
  inputSchema: {
30146
- base_branch: z8.string().optional().describe(
31588
+ base_branch: z15.string().optional().describe(
30147
31589
  `Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`
30148
31590
  ),
30149
- base_sha: z8.string().optional().describe(
31591
+ base_sha: z15.string().optional().describe(
30150
31592
  "Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."
30151
31593
  ),
30152
- no_refresh_base: z8.string().optional().describe(
31594
+ no_refresh_base: z15.string().optional().describe(
30153
31595
  'Pass "true" to skip fetch/materialization and fall back to the local project root as-is.'
30154
31596
  )
30155
31597
  }
@@ -30307,7 +31749,7 @@ registerTool(
30307
31749
  },
30308
31750
  description: "Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",
30309
31751
  inputSchema: {
30310
- fresh_base_root: z8.string().describe(
31752
+ fresh_base_root: z15.string().describe(
30311
31753
  "The fresh_base_root path returned by a prior materialize_fresh_base call."
30312
31754
  )
30313
31755
  }
@@ -30378,14 +31820,14 @@ if (ACTIVE_GROUPS.has("pipeline-authoring")) {
30378
31820
  },
30379
31821
  description: "Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",
30380
31822
  inputSchema: {
30381
- pipeline: z8.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
30382
- variables: z8.record(z8.string(), z8.string()).optional().describe(
31823
+ pipeline: z15.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
31824
+ variables: z15.record(z15.string(), z15.string()).optional().describe(
30383
31825
  "Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."
30384
31826
  ),
30385
- auto_approve: z8.union([z8.boolean(), z8.literal("true"), z8.literal("false")]).optional().describe(
31827
+ auto_approve: z15.union([z15.boolean(), z15.literal("true"), z15.literal("false")]).optional().describe(
30386
31828
  "When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."
30387
31829
  ),
30388
- ttl_seconds: z8.number().int().positive().optional().describe(
31830
+ ttl_seconds: z15.number().int().positive().optional().describe(
30389
31831
  "Override the default 24-hour idle TTL for this run. Must be a positive integer."
30390
31832
  )
30391
31833
  }
@@ -30413,8 +31855,8 @@ if (ACTIVE_GROUPS.has("pipeline-authoring")) {
30413
31855
  },
30414
31856
  description: "Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",
30415
31857
  inputSchema: {
30416
- pipeline_run_id: z8.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),
30417
- agent_result: z8.string().describe(
31858
+ pipeline_run_id: z15.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),
31859
+ agent_result: z15.string().describe(
30418
31860
  "The string the paused instruction's ## Return section asked you to produce"
30419
31861
  )
30420
31862
  }
@@ -30442,7 +31884,7 @@ if (ACTIVE_GROUPS.has("pipeline-authoring")) {
30442
31884
  },
30443
31885
  description: "List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",
30444
31886
  inputSchema: {
30445
- status: z8.enum(["running", "paused", "completed", "failed", "expired"]).optional().describe("Optional status filter")
31887
+ status: z15.enum(["running", "paused", "completed", "failed", "expired"]).optional().describe("Optional status filter")
30446
31888
  }
30447
31889
  },
30448
31890
  async (input) => {
@@ -30468,7 +31910,7 @@ if (ACTIVE_GROUPS.has("pipeline-authoring")) {
30468
31910
  },
30469
31911
  description: "Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",
30470
31912
  inputSchema: {
30471
- pipeline_run_id: z8.string().describe("UUID of the pipeline run to delete.")
31913
+ pipeline_run_id: z15.string().describe("UUID of the pipeline run to delete.")
30472
31914
  }
30473
31915
  },
30474
31916
  async (input) => {
@@ -30495,14 +31937,14 @@ registerTool(
30495
31937
  },
30496
31938
  description: "Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",
30497
31939
  inputSchema: {
30498
- idea: z8.string().optional(),
30499
- idea_file: z8.string().optional(),
30500
- auto_approve: z8.union([z8.boolean(), z8.literal("true"), z8.literal("false")]).optional(),
30501
- scheduled_at: z8.string().optional(),
30502
- max_children: z8.number().int().positive().optional(),
30503
- allow_duplicate: z8.boolean().optional(),
30504
- agent: z8.enum(["claude"]).optional(),
30505
- ttl_seconds: z8.number().int().positive().optional()
31940
+ idea: z15.string().optional(),
31941
+ idea_file: z15.string().optional(),
31942
+ auto_approve: z15.union([z15.boolean(), z15.literal("true"), z15.literal("false")]).optional(),
31943
+ scheduled_at: z15.string().optional(),
31944
+ max_children: z15.number().int().positive().optional(),
31945
+ allow_duplicate: z15.boolean().optional(),
31946
+ agent: z15.enum(["claude"]).optional(),
31947
+ ttl_seconds: z15.number().int().positive().optional()
30506
31948
  }
30507
31949
  },
30508
31950
  async (input) => {
@@ -30545,8 +31987,8 @@ registerTool(
30545
31987
  },
30546
31988
  description: "Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",
30547
31989
  inputSchema: {
30548
- chain_run_id: z8.string(),
30549
- agent_result: z8.string()
31990
+ chain_run_id: z15.string(),
31991
+ agent_result: z15.string()
30550
31992
  }
30551
31993
  },
30552
31994
  async (input) => {
@@ -30670,7 +32112,7 @@ registerTool(
30670
32112
  try {
30671
32113
  parsed = DecisionPageInputSchema.parse(rawPayload);
30672
32114
  } catch (err) {
30673
- if (err instanceof z8.ZodError) {
32115
+ if (err instanceof z15.ZodError) {
30674
32116
  return validationError(formatDecisionPageValidationError(err));
30675
32117
  }
30676
32118
  throw err;