@bridge_gpt/mcp-server 0.2.21 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.21";
22
+ VERSION = "0.2.23";
23
23
  }
24
24
  });
25
25
 
@@ -523,6 +523,357 @@ function defaultTempSuffix() {
523
523
  tempSuffixCounter += 1;
524
524
  return `${process.pid}.${tempSuffixCounter}`;
525
525
  }
526
+ function getCredentialStoreLockPath(deps) {
527
+ return `${getPrimaryCredentialStorePath(deps)}.lock`;
528
+ }
529
+ async function acquireCredentialStoreLock(deps) {
530
+ const open3 = deps.open;
531
+ if (!open3) return { ok: true, release: async () => {
532
+ } };
533
+ const lockPath = getCredentialStoreLockPath(deps);
534
+ const isPosix = deps.platform !== "win32";
535
+ const now = deps.now ?? (() => Date.now());
536
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
537
+ const release = async () => {
538
+ if (!deps.unlink) return;
539
+ try {
540
+ await deps.unlink(lockPath);
541
+ } catch {
542
+ }
543
+ };
544
+ const tryAcquire = async () => {
545
+ try {
546
+ const handle = await open3(lockPath, "wx", isPosix ? 384 : void 0);
547
+ await handle.close();
548
+ return { ok: true };
549
+ } catch (err) {
550
+ const code = err && typeof err === "object" ? err.code : void 0;
551
+ return { ok: false, contended: code === "EEXIST" };
552
+ }
553
+ };
554
+ try {
555
+ await deps.mkdir(path6.dirname(lockPath), { recursive: true });
556
+ } catch {
557
+ return { ok: false, error: `Unable to prepare the credentials directory for ${lockPath}.` };
558
+ }
559
+ const deadline = now() + LOCK_TIMEOUT_MS;
560
+ for (; ; ) {
561
+ const attempt = await tryAcquire();
562
+ if (attempt.ok) return { ok: true, release };
563
+ if (!attempt.contended) {
564
+ return { ok: false, error: `Unable to acquire the credentials lock at ${lockPath}.` };
565
+ }
566
+ if (now() >= deadline) break;
567
+ await sleep3(LOCK_POLL_INTERVAL_MS);
568
+ }
569
+ await release();
570
+ const stolen = await tryAcquire();
571
+ if (stolen.ok) return { ok: true, release };
572
+ return {
573
+ ok: false,
574
+ error: `Timed out waiting for the credentials lock at ${lockPath} (another install may be running).`
575
+ };
576
+ }
577
+ async function withCredentialStoreLock(deps, fn, onLockError) {
578
+ const lock = await acquireCredentialStoreLock(deps);
579
+ if (!lock.ok) return onLockError(lock.error);
580
+ try {
581
+ return await fn();
582
+ } finally {
583
+ await lock.release();
584
+ }
585
+ }
586
+ async function durablyReplaceCredentialStoreJson(primaryPath, value, deps) {
587
+ const open3 = deps.open;
588
+ if (!open3) {
589
+ return {
590
+ ok: false,
591
+ kind: "durable-unavailable",
592
+ error: `Cannot durably write ${primaryPath}: no file-handle primitive is available to fsync the write.`
593
+ };
594
+ }
595
+ const dir = path6.dirname(primaryPath);
596
+ const suffix = (deps.tempSuffix ?? defaultTempSuffix)();
597
+ const tempPath = path6.join(dir, `${path6.basename(primaryPath)}.${suffix}.tmp`);
598
+ const json = formatCredentialStoreJson(value);
599
+ const isPosix = deps.platform !== "win32";
600
+ let handle;
601
+ try {
602
+ await deps.mkdir(dir, { recursive: true });
603
+ handle = await open3(tempPath, "w", isPosix ? 384 : void 0);
604
+ await handle.writeFile(json, { encoding: "utf-8" });
605
+ await handle.sync();
606
+ await handle.close();
607
+ handle = void 0;
608
+ if (isPosix) await deps.chmod(tempPath, 384);
609
+ await deps.rename(tempPath, primaryPath);
610
+ } catch {
611
+ if (handle) {
612
+ try {
613
+ await handle.close();
614
+ } catch {
615
+ }
616
+ }
617
+ if (deps.unlink) {
618
+ try {
619
+ await deps.unlink(tempPath);
620
+ } catch {
621
+ }
622
+ }
623
+ return {
624
+ ok: false,
625
+ kind: "write-error",
626
+ error: `Failed to durably write the credentials file at ${primaryPath}.`
627
+ };
628
+ }
629
+ try {
630
+ const dirHandle = await open3(dir, "r");
631
+ try {
632
+ await dirHandle.sync();
633
+ } finally {
634
+ await dirHandle.close();
635
+ }
636
+ } catch {
637
+ }
638
+ return { ok: true };
639
+ }
640
+ function getBootstrapPendingTarget(repoName) {
641
+ return `${BOOTSTRAP_PENDING_TARGET_PREFIX}${(repoName ?? "").trim()}`;
642
+ }
643
+ function getBapiTarget(repoName) {
644
+ return `bapi:${(repoName ?? "").trim()}`;
645
+ }
646
+ async function loadStoreForMutation(deps) {
647
+ const primaryPath = getPrimaryCredentialStorePath(deps);
648
+ const primary = await readCredentialStoreJsonIfPresent(primaryPath, deps);
649
+ if (primary.state === "error") {
650
+ return { ok: false, kind: primary.kind, error: primary.error };
651
+ }
652
+ if (primary.state === "present") {
653
+ return { ok: true, base: { ...primary.value } };
654
+ }
655
+ const seeded = await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);
656
+ return { ok: true, base: seeded.base };
657
+ }
658
+ function hasExistingBapiKey(store, repoName) {
659
+ const entry = store[getBapiTarget(repoName)];
660
+ return !!entry && typeof entry.BAPI_API_KEY === "string" && entry.BAPI_API_KEY.trim().length > 0;
661
+ }
662
+ function readMatchingPending(store, repoName, inviteFingerprint) {
663
+ const entry = store[getBootstrapPendingTarget(repoName)];
664
+ if (!entry) return null;
665
+ const secret = entry[BOOTSTRAP_PENDING_SECRET_FIELD];
666
+ const fingerprint = entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD];
667
+ if (typeof secret !== "string" || secret.trim().length === 0) return null;
668
+ if (fingerprint !== inviteFingerprint) return null;
669
+ return { keySecret: secret };
670
+ }
671
+ function hasConflictingPending(store, repoName, inviteFingerprint) {
672
+ const entry = store[getBootstrapPendingTarget(repoName)];
673
+ if (!entry) return false;
674
+ const secret = entry[BOOTSTRAP_PENDING_SECRET_FIELD];
675
+ if (typeof secret !== "string" || secret.trim().length === 0) return false;
676
+ return entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD] !== inviteFingerprint;
677
+ }
678
+ function pendingConflictError(target, primaryPath) {
679
+ return `A pending bootstrap-invite credential for a DIFFERENT invite already exists at ${target} in ${primaryPath}. It is the only proof that can replay that redemption, so it will not be overwritten. Complete that redemption first, or \u2014 only if you are certain its invite was never exchanged \u2014 remove the entry from the store by hand.`;
680
+ }
681
+ async function prepareBootstrapPendingCredential(params, deps) {
682
+ const primaryPath = getPrimaryCredentialStorePath(deps);
683
+ const repoName = (params.repoName ?? "").trim();
684
+ const fingerprint = (params.inviteFingerprint ?? "").trim();
685
+ const target = getBootstrapPendingTarget(repoName);
686
+ if (repoName.length === 0) {
687
+ return {
688
+ ok: false,
689
+ path: primaryPath,
690
+ target,
691
+ kind: "invalid-repo",
692
+ error: "Cannot prepare a bootstrap-invite credential: a non-empty repo name is required."
693
+ };
694
+ }
695
+ if (fingerprint.length === 0) {
696
+ return {
697
+ ok: false,
698
+ path: primaryPath,
699
+ target,
700
+ kind: "invalid-fingerprint",
701
+ error: "Cannot prepare a bootstrap-invite credential: the invite fingerprint was empty."
702
+ };
703
+ }
704
+ return withCredentialStoreLock(
705
+ deps,
706
+ async () => {
707
+ const loaded = await loadStoreForMutation(deps);
708
+ if (!loaded.ok) {
709
+ return { ok: false, path: primaryPath, target, kind: loaded.kind, error: loaded.error };
710
+ }
711
+ const base = loaded.base;
712
+ if (hasExistingBapiKey(base, repoName) && !params.allowOverwriteExistingCredential) {
713
+ return {
714
+ ok: false,
715
+ path: primaryPath,
716
+ target: getBapiTarget(repoName),
717
+ kind: "credential-conflict",
718
+ error: `A credential already exists for ${getBapiTarget(repoName)} in ${primaryPath}.`
719
+ };
720
+ }
721
+ const existing = readMatchingPending(base, repoName, fingerprint);
722
+ if (existing) {
723
+ return { ok: true, path: primaryPath, target, keySecret: existing.keySecret, reused: true };
724
+ }
725
+ if (hasConflictingPending(base, repoName, fingerprint)) {
726
+ return {
727
+ ok: false,
728
+ path: primaryPath,
729
+ target,
730
+ kind: "pending-conflict",
731
+ error: pendingConflictError(target, primaryPath)
732
+ };
733
+ }
734
+ const keySecret = params.generateKeySecret();
735
+ const next = {
736
+ ...base,
737
+ [target]: {
738
+ ...base[target] ?? {},
739
+ [BOOTSTRAP_PENDING_SECRET_FIELD]: keySecret,
740
+ [BOOTSTRAP_PENDING_FINGERPRINT_FIELD]: fingerprint
741
+ }
742
+ };
743
+ const written = await durablyReplaceCredentialStoreJson(primaryPath, next, deps);
744
+ if (!written.ok) {
745
+ return { ok: false, path: primaryPath, target, kind: written.kind, error: written.error };
746
+ }
747
+ return { ok: true, path: primaryPath, target, keySecret, reused: false };
748
+ },
749
+ (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error })
750
+ );
751
+ }
752
+ async function repointBootstrapPendingCredential(params, deps) {
753
+ const primaryPath = getPrimaryCredentialStorePath(deps);
754
+ const fromRepo = (params.fromRepoName ?? "").trim();
755
+ const toRepo = (params.toRepoName ?? "").trim();
756
+ const fingerprint = (params.inviteFingerprint ?? "").trim();
757
+ const target = getBootstrapPendingTarget(toRepo);
758
+ if (fromRepo.length === 0 || toRepo.length === 0) {
759
+ return {
760
+ ok: false,
761
+ path: primaryPath,
762
+ target,
763
+ kind: "invalid-repo",
764
+ error: "Cannot re-point a bootstrap-invite credential: a non-empty repo name is required."
765
+ };
766
+ }
767
+ return withCredentialStoreLock(
768
+ deps,
769
+ async () => {
770
+ const loaded = await loadStoreForMutation(deps);
771
+ if (!loaded.ok) {
772
+ return { ok: false, path: primaryPath, target, kind: loaded.kind, error: loaded.error };
773
+ }
774
+ const base = loaded.base;
775
+ const pending = readMatchingPending(base, fromRepo, fingerprint);
776
+ if (!pending) {
777
+ return {
778
+ ok: false,
779
+ path: primaryPath,
780
+ target,
781
+ kind: "pending-missing",
782
+ error: `No pending bootstrap-invite credential for ${getBootstrapPendingTarget(fromRepo)} in ${primaryPath}.`
783
+ };
784
+ }
785
+ if (toRepo === fromRepo) {
786
+ return { ok: true, path: primaryPath, target, keySecret: pending.keySecret };
787
+ }
788
+ if (hasExistingBapiKey(base, toRepo) && !params.allowOverwriteExistingCredential) {
789
+ return {
790
+ ok: false,
791
+ path: primaryPath,
792
+ target: getBapiTarget(toRepo),
793
+ kind: "credential-conflict",
794
+ error: `A credential already exists for ${getBapiTarget(toRepo)} in ${primaryPath}.`
795
+ };
796
+ }
797
+ const destination = base[getBootstrapPendingTarget(toRepo)];
798
+ if (hasConflictingPending(base, toRepo, fingerprint)) {
799
+ return {
800
+ ok: false,
801
+ path: primaryPath,
802
+ target,
803
+ kind: "pending-conflict",
804
+ error: pendingConflictError(target, primaryPath)
805
+ };
806
+ }
807
+ const next = { ...base };
808
+ delete next[getBootstrapPendingTarget(fromRepo)];
809
+ next[target] = {
810
+ ...destination ?? {},
811
+ [BOOTSTRAP_PENDING_SECRET_FIELD]: pending.keySecret,
812
+ [BOOTSTRAP_PENDING_FINGERPRINT_FIELD]: fingerprint
813
+ };
814
+ const written = await durablyReplaceCredentialStoreJson(primaryPath, next, deps);
815
+ if (!written.ok) {
816
+ return { ok: false, path: primaryPath, target, kind: written.kind, error: written.error };
817
+ }
818
+ return { ok: true, path: primaryPath, target, keySecret: pending.keySecret };
819
+ },
820
+ (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error })
821
+ );
822
+ }
823
+ async function promoteBootstrapPendingCredential(params, deps) {
824
+ const primaryPath = getPrimaryCredentialStorePath(deps);
825
+ const repoName = (params.repoName ?? "").trim();
826
+ const fingerprint = (params.inviteFingerprint ?? "").trim();
827
+ const target = getBapiTarget(repoName);
828
+ if (repoName.length === 0) {
829
+ return {
830
+ ok: false,
831
+ path: primaryPath,
832
+ target,
833
+ kind: "invalid-repo",
834
+ error: "Cannot promote a bootstrap-invite credential: a non-empty repo name is required."
835
+ };
836
+ }
837
+ return withCredentialStoreLock(
838
+ deps,
839
+ async () => {
840
+ const loaded = await loadStoreForMutation(deps);
841
+ if (!loaded.ok) {
842
+ return { ok: false, path: primaryPath, target, kind: loaded.kind, error: loaded.error };
843
+ }
844
+ const base = loaded.base;
845
+ const pending = readMatchingPending(base, repoName, fingerprint);
846
+ if (!pending) {
847
+ return {
848
+ ok: false,
849
+ path: primaryPath,
850
+ target,
851
+ kind: "pending-missing",
852
+ error: `No pending bootstrap-invite credential for ${getBootstrapPendingTarget(repoName)} in ${primaryPath}.`
853
+ };
854
+ }
855
+ const hadKey = hasExistingBapiKey(base, repoName);
856
+ if (hadKey && !params.allowOverwriteExistingCredential) {
857
+ return {
858
+ ok: false,
859
+ path: primaryPath,
860
+ target,
861
+ kind: "credential-conflict",
862
+ error: `A credential already exists for ${target} in ${primaryPath}.`
863
+ };
864
+ }
865
+ const next = { ...base };
866
+ delete next[getBootstrapPendingTarget(repoName)];
867
+ next[target] = { ...base[target] ?? {}, BAPI_API_KEY: pending.keySecret };
868
+ const written = await durablyReplaceCredentialStoreJson(primaryPath, next, deps);
869
+ if (!written.ok) {
870
+ return { ok: false, path: primaryPath, target, kind: written.kind, error: written.error };
871
+ }
872
+ return { ok: true, path: primaryPath, target, action: hadKey ? "updated" : "created" };
873
+ },
874
+ (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error })
875
+ );
876
+ }
526
877
  async function upsertBapiCredential(repoName, apiKey, deps) {
527
878
  const primaryPath = getPrimaryCredentialStorePath(deps);
528
879
  const trimmedRepo = (repoName ?? "").trim();
@@ -546,59 +897,70 @@ async function upsertBapiCredential(repoName, apiKey, deps) {
546
897
  error: `Cannot store BAPI_API_KEY for ${target}: the provided key was empty.`
547
898
  };
548
899
  }
549
- const primary = await readCredentialStoreJsonIfPresent(primaryPath, deps);
550
- let base;
551
- let migratedFallback = false;
552
- if (primary.state === "error") {
553
- return { ok: false, path: primaryPath, target, kind: primary.kind, error: primary.error };
554
- }
555
- if (primary.state === "present") {
556
- base = { ...primary.value };
557
- } else {
558
- const seeded = await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);
559
- base = seeded.base;
560
- migratedFallback = seeded.migratedFallback;
561
- }
562
- const existingEntry = base[target];
563
- const hadKey = !!existingEntry && typeof existingEntry.BAPI_API_KEY === "string" && existingEntry.BAPI_API_KEY.length > 0;
564
- const action = hadKey ? "updated" : "created";
565
- const nextEntry = { ...existingEntry ?? {}, BAPI_API_KEY: trimmedKey };
566
- const next = { ...base, [target]: nextEntry };
567
- const dir = path6.dirname(primaryPath);
568
- const suffix = (deps.tempSuffix ?? defaultTempSuffix)();
569
- const tempPath = path6.join(dir, `${path6.basename(primaryPath)}.${suffix}.tmp`);
570
- const json = formatCredentialStoreJson(next);
571
- const isPosix = deps.platform !== "win32";
572
- try {
573
- await deps.mkdir(dir, { recursive: true });
574
- const writeOptions = isPosix ? { encoding: "utf-8", mode: 384 } : { encoding: "utf-8" };
575
- await deps.writeFile(tempPath, json, writeOptions);
576
- if (isPosix) {
577
- await deps.chmod(tempPath, 384);
578
- }
579
- await deps.rename(tempPath, primaryPath);
580
- } catch {
581
- if (deps.unlink) {
900
+ return withCredentialStoreLock(
901
+ deps,
902
+ async () => {
903
+ const primary = await readCredentialStoreJsonIfPresent(primaryPath, deps);
904
+ let base;
905
+ let migratedFallback = false;
906
+ if (primary.state === "error") {
907
+ return { ok: false, path: primaryPath, target, kind: primary.kind, error: primary.error };
908
+ }
909
+ if (primary.state === "present") {
910
+ base = { ...primary.value };
911
+ } else {
912
+ const seeded = await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);
913
+ base = seeded.base;
914
+ migratedFallback = seeded.migratedFallback;
915
+ }
916
+ const existingEntry = base[target];
917
+ const hadKey = !!existingEntry && typeof existingEntry.BAPI_API_KEY === "string" && existingEntry.BAPI_API_KEY.length > 0;
918
+ const action = hadKey ? "updated" : "created";
919
+ const nextEntry = { ...existingEntry ?? {}, BAPI_API_KEY: trimmedKey };
920
+ const next = { ...base, [target]: nextEntry };
921
+ const dir = path6.dirname(primaryPath);
922
+ const suffix = (deps.tempSuffix ?? defaultTempSuffix)();
923
+ const tempPath = path6.join(dir, `${path6.basename(primaryPath)}.${suffix}.tmp`);
924
+ const json = formatCredentialStoreJson(next);
925
+ const isPosix = deps.platform !== "win32";
582
926
  try {
583
- await deps.unlink(tempPath);
927
+ await deps.mkdir(dir, { recursive: true });
928
+ const writeOptions = isPosix ? { encoding: "utf-8", mode: 384 } : { encoding: "utf-8" };
929
+ await deps.writeFile(tempPath, json, writeOptions);
930
+ if (isPosix) {
931
+ await deps.chmod(tempPath, 384);
932
+ }
933
+ await deps.rename(tempPath, primaryPath);
584
934
  } catch {
935
+ if (deps.unlink) {
936
+ try {
937
+ await deps.unlink(tempPath);
938
+ } catch {
939
+ }
940
+ }
941
+ return {
942
+ ok: false,
943
+ path: primaryPath,
944
+ target,
945
+ kind: "write-error",
946
+ error: `Failed to write credentials file at ${primaryPath}.`
947
+ };
585
948
  }
586
- }
587
- return {
588
- ok: false,
589
- path: primaryPath,
590
- target,
591
- kind: "write-error",
592
- error: `Failed to write credentials file at ${primaryPath}.`
593
- };
594
- }
595
- return { ok: true, path: primaryPath, target, action, migratedFallback };
949
+ return { ok: true, path: primaryPath, target, action, migratedFallback };
950
+ },
951
+ (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error })
952
+ );
596
953
  }
597
- var tempSuffixCounter;
954
+ var tempSuffixCounter, LOCK_POLL_INTERVAL_MS, LOCK_TIMEOUT_MS, BOOTSTRAP_PENDING_TARGET_PREFIX, BOOTSTRAP_PENDING_SECRET_FIELD, BOOTSTRAP_PENDING_FINGERPRINT_FIELD;
598
955
  var init_credential_store = __esm({
599
956
  "src/credential-store.ts"() {
600
957
  "use strict";
601
958
  tempSuffixCounter = 0;
959
+ LOCK_POLL_INTERVAL_MS = 50;
960
+ LOCK_TIMEOUT_MS = 5e3;
961
+ BOOTSTRAP_PENDING_TARGET_PREFIX = "bootstrap-pending:";
962
+ BOOTSTRAP_PENDING_SECRET_FIELD = "BAPI_API_KEY";
963
+ BOOTSTRAP_PENDING_FINGERPRINT_FIELD = "BOOTSTRAP_INVITE_FINGERPRINT";
602
964
  }
603
965
  });
604
966
 
@@ -3766,6 +4128,7 @@ __export(bridge_api_client_exports, {
3766
4128
  buildConductorVcsUrl: () => buildConductorVcsUrl,
3767
4129
  buildEpicDispatchKey: () => buildEpicDispatchKey,
3768
4130
  claimEpicSupervisionLease: () => claimEpicSupervisionLease,
4131
+ createEpicRun: () => createEpicRun,
3769
4132
  createEpicTicketStatus: () => createEpicTicketStatus,
3770
4133
  deletePullRequestBranch: () => deletePullRequestBranch,
3771
4134
  extractSanitizedErrorDiagnostics: () => extractSanitizedErrorDiagnostics,
@@ -3802,7 +4165,7 @@ async function resolveConductorBridgeApiAccess(deps = {}) {
3802
4165
  const platform = deps.platform ?? process.platform;
3803
4166
  const readFileImpl = deps.readFile ?? ((p) => readFile4(p, "utf-8"));
3804
4167
  const statImpl = deps.stat ?? ((p) => stat2(p));
3805
- const repoName = await resolveStartTicketsRepoName({ env, cwd, readFile: readFileImpl });
4168
+ const repoName = deps.repoName?.trim() || await resolveStartTicketsRepoName({ env, cwd, readFile: readFileImpl });
3806
4169
  if (!repoName) {
3807
4170
  return {
3808
4171
  ok: false,
@@ -4010,8 +4373,8 @@ async function fetchPrReviewStatus(access2, prNumber, fetchImpl = globalThis.fet
4010
4373
  }
4011
4374
  function buildConductorVcsUrl(baseUrl, apiPath) {
4012
4375
  const trimmed = baseUrl.replace(/\/+$/, "");
4013
- const path33 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
4014
- return new URL(`${trimmed}${path33}`).toString();
4376
+ const path34 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
4377
+ return new URL(`${trimmed}${path34}`).toString();
4015
4378
  }
4016
4379
  function conductorPostHeaders(access2) {
4017
4380
  return { "X-API-Key": access2.apiKey, "Content-Type": "application/json" };
@@ -4232,6 +4595,31 @@ async function fetchActiveEpicRuns(access2, fetchImpl = globalThis.fetch) {
4232
4595
  }
4233
4596
  return [];
4234
4597
  }
4598
+ async function createEpicRun(access2, request, fetchImpl = globalThis.fetch) {
4599
+ requireNonEmptyString(request.epicKey);
4600
+ const body = {
4601
+ repo_name: access2.repoName,
4602
+ epic_key: request.epicKey,
4603
+ status: request.status ?? "planning",
4604
+ current_plan_version: request.currentPlanVersion ?? 0
4605
+ };
4606
+ if (request.policyJson !== void 0) body.policy_json = request.policyJson;
4607
+ if (request.budgetWallClockSeconds !== void 0) {
4608
+ body.budget_wall_clock_seconds = request.budgetWallClockSeconds;
4609
+ }
4610
+ if (request.budgetCostCents !== void 0) {
4611
+ body.budget_cost_cents = request.budgetCostCents;
4612
+ }
4613
+ const url = buildConductorJiraUrl(access2.baseUrl, `${EPIC_RUNS_API_PREFIX}/runs`);
4614
+ const parsed = await fetchConductorJsonPostWithTimeout(
4615
+ url,
4616
+ conductorPostHeaders(access2),
4617
+ JSON.stringify(body),
4618
+ CONDUCTOR_FETCH_TIMEOUT_MS,
4619
+ fetchImpl
4620
+ );
4621
+ return parsed;
4622
+ }
4235
4623
  async function updateEpicRunStatus(access2, request, fetchImpl = globalThis.fetch) {
4236
4624
  requireNonEmptyString(request.epicKey);
4237
4625
  const url = buildConductorJiraUrl(access2.baseUrl, epicRunApiPath(request.epicKey));
@@ -4392,8 +4780,8 @@ async function transitionEpicDispatch(access2, request, fetchImpl = globalThis.f
4392
4780
  if (request.nextStatus === "run_spawned") {
4393
4781
  requireNonEmptyString(request.runId);
4394
4782
  }
4395
- const path33 = epicDispatchTransitionApiPath(request.dispatchKey, request.nextStatus);
4396
- const url = buildConductorJiraUrl(access2.baseUrl, path33);
4783
+ const path34 = epicDispatchTransitionApiPath(request.dispatchKey, request.nextStatus);
4784
+ const url = buildConductorJiraUrl(access2.baseUrl, path34);
4397
4785
  const body = request.nextStatus === "run_spawned" ? JSON.stringify({ repo_name: access2.repoName, run_id: request.runId }) : JSON.stringify({ repo_name: access2.repoName });
4398
4786
  const parsed = await fetchConductorJsonPostWithTimeout(
4399
4787
  url,
@@ -4454,6 +4842,10 @@ async function approveEpicPlan(access2, request, fetchImpl = globalThis.fetch) {
4454
4842
  return parsed;
4455
4843
  } catch (error) {
4456
4844
  if (error instanceof ConductorBridgeApiError && error.status === 409) {
4845
+ const preview = error.bodyPreview ?? "";
4846
+ if (/multiple active runs/i.test(preview)) {
4847
+ return { ok: false, kind: "conflict", reason: "multiple_active_runs" };
4848
+ }
4457
4849
  return { ok: false, kind: "conflict", reason: "superseded" };
4458
4850
  }
4459
4851
  throw error;
@@ -4629,6 +5021,18 @@ var init_bridge_api_client = __esm({
4629
5021
  }
4630
5022
  });
4631
5023
 
5024
+ // src/pr-base-contract.ts
5025
+ function buildPrBaseContractLaunchInstruction() {
5026
+ return 'PR base contract: when you open the pull request for this ticket you MUST run gh pr create --base "$BAPI_BASE_BRANCH" so the PR targets the run base branch. Do not infer the base from the current branch ancestry or from the repository default branch. If a pull request for this branch already exists, verify that its base equals $BAPI_BASE_BRANCH and report the mismatch rather than retargeting the PR or rebuilding the branch yourself.';
5027
+ }
5028
+ var PR_BASE_BRANCH_ENV_VAR;
5029
+ var init_pr_base_contract = __esm({
5030
+ "src/pr-base-contract.ts"() {
5031
+ "use strict";
5032
+ PR_BASE_BRANCH_ENV_VAR = "BAPI_BASE_BRANCH";
5033
+ }
5034
+ });
5035
+
4632
5036
  // src/worktree-core.ts
4633
5037
  import path13 from "path";
4634
5038
  function resolveBranchForTicket(key, overrides) {
@@ -4702,6 +5106,39 @@ async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint) {
4702
5106
  reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree \u2014 delete it (git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`
4703
5107
  };
4704
5108
  }
5109
+ async function hardResetWorktree(deps, worktreePath, ref) {
5110
+ const resetArgs = ["reset", "--hard", ref];
5111
+ const reset = await deps.runCommand("git", resetArgs, { cwd: worktreePath });
5112
+ if (!commandSucceeded(reset)) {
5113
+ const reason = (reset.stderr || reset.stdout || "").trim();
5114
+ return `git ${resetArgs.join(" ")} failed${reason ? `: ${reason}` : ""}`;
5115
+ }
5116
+ return null;
5117
+ }
5118
+ async function verifyWorktreeHead(deps, worktreePath, expected) {
5119
+ const headRes = await deps.runCommand(
5120
+ "git",
5121
+ ["rev-parse", "--verify", "HEAD^{commit}"],
5122
+ { cwd: worktreePath }
5123
+ );
5124
+ if (!commandSucceeded(headRes)) {
5125
+ return "failed to resolve worktree HEAD after creation (git rev-parse HEAD^{commit} failed).";
5126
+ }
5127
+ const expectedRes = await deps.runCommand(
5128
+ "git",
5129
+ ["rev-parse", "--verify", `${expected}^{commit}`],
5130
+ { cwd: worktreePath }
5131
+ );
5132
+ if (!commandSucceeded(expectedRes)) {
5133
+ return "failed to resolve the expected base commit after creation (git rev-parse --verify failed).";
5134
+ }
5135
+ const head = headRes.stdout.trim();
5136
+ const want = expectedRes.stdout.trim();
5137
+ if (head !== want) {
5138
+ return `worktree head ${head.slice(0, 12)} does not match the pinned base ${want.slice(0, 12)}; Worktrunk seeded the worktree from an unexpected start point. Refusing to hand a mis-seeded worktree to a worker.`;
5139
+ }
5140
+ return null;
5141
+ }
4705
5142
  async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseStartPoint = "main", guardStaleWorktree = false, behavior = {}) {
4706
5143
  const branch = resolveBranchForTicket(key, branchOverrides);
4707
5144
  try {
@@ -4730,16 +5167,21 @@ async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBina
4730
5167
  }
4731
5168
  const worktreePath = extractWorktreePath(result.stdout, deps.cwd, deps.platform);
4732
5169
  if (exists && behavior.freshenFromOrigin) {
4733
- const resetArgs = ["reset", "--hard", behavior.freshenFromOrigin];
4734
- const reset = await deps.runCommand("git", resetArgs, { cwd: worktreePath });
4735
- if (!commandSucceeded(reset)) {
4736
- const reason = (reset.stderr || reset.stdout || "").trim();
4737
- return {
4738
- key,
4739
- branch,
4740
- status: "create-failed",
4741
- error: `git ${resetArgs.join(" ")} failed${reason ? `: ${reason}` : ""}`
4742
- };
5170
+ const resetError = await hardResetWorktree(deps, worktreePath, behavior.freshenFromOrigin);
5171
+ if (resetError) {
5172
+ return { key, branch, status: "create-failed", error: resetError };
5173
+ }
5174
+ }
5175
+ if (exists && behavior.alignExistingBranchTo) {
5176
+ const resetError = await hardResetWorktree(deps, worktreePath, behavior.alignExistingBranchTo);
5177
+ if (resetError) {
5178
+ return { key, branch, status: "create-failed", error: resetError };
5179
+ }
5180
+ }
5181
+ if (behavior.verifyHeadMatches) {
5182
+ const verifyError = await verifyWorktreeHead(deps, worktreePath, behavior.verifyHeadMatches);
5183
+ if (verifyError) {
5184
+ return { key, branch, status: "create-failed", error: verifyError };
4743
5185
  }
4744
5186
  }
4745
5187
  return { key, branch, status: "created", path: worktreePath };
@@ -4755,11 +5197,88 @@ var init_worktree_core = __esm({
4755
5197
  }
4756
5198
  });
4757
5199
 
5200
+ // src/base-ref.ts
5201
+ import path14 from "path";
5202
+ function validateBranchName(branch) {
5203
+ if (branch.trim().length === 0) return "branch name must not be empty.";
5204
+ if (branch.length > 255) return "branch name must be 255 characters or fewer.";
5205
+ if (branch.startsWith("-")) return "branch name must not start with '-'.";
5206
+ if (branch.includes("..")) return "branch name must not contain '..'.";
5207
+ if (branch.endsWith(".lock")) return "branch name must not end with '.lock'.";
5208
+ for (let i = 0; i < branch.length; i++) {
5209
+ const code = branch.charCodeAt(i);
5210
+ if (code <= 31 || code === 127) {
5211
+ return "branch name must not contain control characters.";
5212
+ }
5213
+ }
5214
+ return null;
5215
+ }
5216
+ function normalizeRepoKey(cwd) {
5217
+ return path14.resolve(cwd);
5218
+ }
5219
+ async function withRepoFetchLock(repoKey, fn) {
5220
+ const previous = repoFetchLocks.get(repoKey) ?? Promise.resolve();
5221
+ let releaseCurrent;
5222
+ const current = new Promise((resolve2) => {
5223
+ releaseCurrent = resolve2;
5224
+ });
5225
+ const chained = previous.then(() => current);
5226
+ repoFetchLocks.set(repoKey, chained);
5227
+ await previous.catch(() => {
5228
+ });
5229
+ try {
5230
+ return await fn();
5231
+ } finally {
5232
+ releaseCurrent();
5233
+ if (repoFetchLocks.get(repoKey) === chained) {
5234
+ repoFetchLocks.delete(repoKey);
5235
+ }
5236
+ }
5237
+ }
5238
+ async function fetchAndResolveBaseSha(deps, baseBranch) {
5239
+ const validationError = validateBranchName(baseBranch);
5240
+ if (validationError) {
5241
+ return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
5242
+ }
5243
+ const repoKey = normalizeRepoKey(deps.cwd);
5244
+ return withRepoFetchLock(repoKey, async () => {
5245
+ const fetch2 = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
5246
+ cwd: deps.cwd
5247
+ });
5248
+ if (!commandSucceeded(fetch2)) {
5249
+ return {
5250
+ ok: false,
5251
+ error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`
5252
+ };
5253
+ }
5254
+ const resolve2 = await deps.runCommand(
5255
+ "git",
5256
+ ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`],
5257
+ { cwd: deps.cwd }
5258
+ );
5259
+ if (!commandSucceeded(resolve2)) {
5260
+ return {
5261
+ ok: false,
5262
+ error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`
5263
+ };
5264
+ }
5265
+ return { ok: true, base_sha: resolve2.stdout.trim() };
5266
+ });
5267
+ }
5268
+ var repoFetchLocks;
5269
+ var init_base_ref = __esm({
5270
+ "src/base-ref.ts"() {
5271
+ "use strict";
5272
+ init_start_tickets_prereqs();
5273
+ repoFetchLocks = /* @__PURE__ */ new Map();
5274
+ }
5275
+ });
5276
+
4758
5277
  // src/start-tickets.ts
4759
5278
  import { execFile } from "child_process";
4760
5279
  import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3, mkdtemp, stat as stat3, readdir as readdir2, rm } from "fs/promises";
4761
5280
  import os4 from "node:os";
4762
- import path14 from "path";
5281
+ import path15 from "path";
4763
5282
  import { existsSync as existsSync2 } from "node:fs";
4764
5283
  function appendSummaryRowWarning(row, warning) {
4765
5284
  return { ...row, warnings: [...row.warnings ?? [], warning] };
@@ -4771,6 +5290,8 @@ function getStartTicketsUsage() {
4771
5290
  "",
4772
5291
  "Flags:",
4773
5292
  " --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
5293
+ " --workflow implement|review-and-implement Slash command each spawned worktree runs (default: implement). review-and-implement runs /review-ticket then, after a per-ticket halt gate, /implement-ticket in the same session; --auto applies to the selected workflow.",
5294
+ " --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
4774
5295
  " --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
4775
5296
  " --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use",
4776
5297
  " --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)",
@@ -4816,11 +5337,13 @@ function parseStartTicketsArgs(argv) {
4816
5337
  let agentName = DEFAULT_AGENT_NAME;
4817
5338
  let baseBranch = "main";
4818
5339
  let conductorEnabled = false;
5340
+ let workflow = "implement";
5341
+ let reviewRoundsRaw;
4819
5342
  const branchEntries = [];
4820
5343
  const keys = [];
4821
5344
  for (let i = 0; i < argv.length; i++) {
4822
5345
  const arg = argv[i];
4823
- const takeValue2 = () => {
5346
+ const takeValue3 = () => {
4824
5347
  if (i + 1 >= argv.length) return void 0;
4825
5348
  i += 1;
4826
5349
  return argv[i];
@@ -4830,7 +5353,7 @@ function parseStartTicketsArgs(argv) {
4830
5353
  if (arg.startsWith("--agent=")) {
4831
5354
  value = arg.slice("--agent=".length);
4832
5355
  } else {
4833
- value = takeValue2();
5356
+ value = takeValue3();
4834
5357
  if (value === void 0) {
4835
5358
  return { status: "error", message: "--agent requires a value (an agent name)." };
4836
5359
  }
@@ -4844,12 +5367,53 @@ function parseStartTicketsArgs(argv) {
4844
5367
  agentName = value;
4845
5368
  continue;
4846
5369
  }
5370
+ if (arg === "--workflow" || arg.startsWith("--workflow=")) {
5371
+ let value;
5372
+ if (arg.startsWith("--workflow=")) {
5373
+ value = arg.slice("--workflow=".length);
5374
+ } else {
5375
+ value = takeValue3();
5376
+ if (value === void 0) {
5377
+ return {
5378
+ status: "error",
5379
+ message: "--workflow requires a value (allowed values: implement, review-and-implement)."
5380
+ };
5381
+ }
5382
+ }
5383
+ if (value !== "implement" && value !== "review-and-implement") {
5384
+ return {
5385
+ status: "error",
5386
+ message: `Invalid --workflow value: '${value}' (allowed values: implement, review-and-implement).`
5387
+ };
5388
+ }
5389
+ workflow = value;
5390
+ continue;
5391
+ }
5392
+ if (arg === "--rounds" || arg.startsWith("--rounds=")) {
5393
+ let value;
5394
+ if (arg.startsWith("--rounds=")) {
5395
+ value = arg.slice("--rounds=".length);
5396
+ } else {
5397
+ value = takeValue3();
5398
+ if (value === void 0) {
5399
+ return { status: "error", message: "--rounds requires a value (allowed values: 1, 2)." };
5400
+ }
5401
+ }
5402
+ if (value !== "1" && value !== "2") {
5403
+ return {
5404
+ status: "error",
5405
+ message: `Invalid --rounds value: '${value}' (allowed values: 1, 2).`
5406
+ };
5407
+ }
5408
+ reviewRoundsRaw = value;
5409
+ continue;
5410
+ }
4847
5411
  if (arg === "--terminal" || arg.startsWith("--terminal=")) {
4848
5412
  let value;
4849
5413
  if (arg.startsWith("--terminal=")) {
4850
5414
  value = arg.slice("--terminal=".length);
4851
5415
  } else {
4852
- value = takeValue2();
5416
+ value = takeValue3();
4853
5417
  if (value === void 0) {
4854
5418
  return { status: "error", message: "--terminal requires a value (terminal or iterm)." };
4855
5419
  }
@@ -4867,7 +5431,7 @@ function parseStartTicketsArgs(argv) {
4867
5431
  if (arg.startsWith("--max-parallel=")) {
4868
5432
  maxParallelRaw = arg.slice("--max-parallel=".length);
4869
5433
  } else {
4870
- const value = takeValue2();
5434
+ const value = takeValue3();
4871
5435
  if (value === void 0) {
4872
5436
  return { status: "error", message: "--max-parallel requires a positive integer value." };
4873
5437
  }
@@ -4880,7 +5444,7 @@ function parseStartTicketsArgs(argv) {
4880
5444
  if (arg.startsWith("--branch=")) {
4881
5445
  value = arg.slice("--branch=".length);
4882
5446
  } else {
4883
- value = takeValue2();
5447
+ value = takeValue3();
4884
5448
  if (value === void 0) {
4885
5449
  return { status: "error", message: "--branch requires a KEY=BRANCH value." };
4886
5450
  }
@@ -4897,7 +5461,7 @@ function parseStartTicketsArgs(argv) {
4897
5461
  if (next === void 0 || next.startsWith("-")) {
4898
5462
  return { status: "error", message: "--base-branch requires a value (a branch name)." };
4899
5463
  }
4900
- value = takeValue2();
5464
+ value = takeValue3();
4901
5465
  }
4902
5466
  const trimmed = (value ?? "").trim();
4903
5467
  const error = validateBranchName(trimmed);
@@ -4989,22 +5553,33 @@ function parseStartTicketsArgs(argv) {
4989
5553
  }
4990
5554
  branchOverrides[overrideKey] = branchName;
4991
5555
  }
5556
+ let reviewRounds;
5557
+ if (reviewRoundsRaw !== void 0) {
5558
+ if (workflow !== "review-and-implement") {
5559
+ return {
5560
+ status: "error",
5561
+ message: "--rounds is only valid with --workflow review-and-implement."
5562
+ };
5563
+ }
5564
+ reviewRounds = reviewRoundsRaw === "1" ? 1 : 2;
5565
+ }
4992
5566
  return {
4993
5567
  status: "ok",
4994
- options: { keys, terminal, dryRun, autoApprove, refreshMain, maxParallel, branchOverrides, agentName, baseBranch, conductorEnabled }
4995
- };
4996
- }
4997
- function validateBranchName(branch) {
4998
- if (branch.trim().length === 0) return "branch name must not be empty.";
4999
- if (branch.length > 255) return "branch name must be 255 characters or fewer.";
5000
- if (branch.startsWith("-")) return "branch name must not start with '-'.";
5001
- for (let i = 0; i < branch.length; i++) {
5002
- const code = branch.charCodeAt(i);
5003
- if (code <= 31 || code === 127) {
5004
- return "branch name must not contain control characters.";
5568
+ options: {
5569
+ keys,
5570
+ terminal,
5571
+ dryRun,
5572
+ autoApprove,
5573
+ refreshMain,
5574
+ maxParallel,
5575
+ branchOverrides,
5576
+ agentName,
5577
+ baseBranch,
5578
+ conductorEnabled,
5579
+ workflow,
5580
+ reviewRounds
5005
5581
  }
5006
- }
5007
- return null;
5582
+ };
5008
5583
  }
5009
5584
  function detectTerminal(explicit, env) {
5010
5585
  if (explicit) return explicit;
@@ -5024,21 +5599,27 @@ function getDefaultSpawnTerminalTabForPlatform(platform) {
5024
5599
  return spawnUnsupportedPlatformTerminalTab;
5025
5600
  }
5026
5601
  }
5027
- function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null, resumeMode = false) {
5602
+ function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
5028
5603
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
5029
5604
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
5030
5605
  }
5031
5606
  const platform = deps.platform;
5607
+ const prBaseBranch = conductorEnabled ? baseBranch : null;
5032
5608
  return {
5033
5609
  ok: true,
5034
5610
  config: {
5035
5611
  platform,
5036
5612
  worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
5037
5613
  // Inject the resolved repo identity so the spawned worktree session never
5038
- // falls back to the basename-derived repo name (the 403 root cause).
5039
- buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(
5040
- buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode),
5041
- repoName,
5614
+ // falls back to the basename-derived repo name (the 403 root cause), and
5615
+ // (BAPI-586) the run base so the conductor worker opens its PR against it.
5616
+ buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(
5617
+ prependRepoNameEnvAssignment(
5618
+ buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch),
5619
+ repoName,
5620
+ platform
5621
+ ),
5622
+ prBaseBranch,
5042
5623
  platform
5043
5624
  ),
5044
5625
  spawnTerminalTab: deps.spawnTerminalTab
@@ -5052,6 +5633,13 @@ function prependRepoNameEnvAssignment(command, repoName, platform = "darwin") {
5052
5633
  }
5053
5634
  return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
5054
5635
  }
5636
+ function prependBaseBranchEnvAssignment(command, baseBranch, platform = "darwin") {
5637
+ if (!baseBranch) return command;
5638
+ if (platform === "win32") {
5639
+ return `$env:${PR_BASE_BRANCH_ENV_VAR} = ${powershellSquote(baseBranch)}; ${command}`;
5640
+ }
5641
+ return `export ${PR_BASE_BRANCH_ENV_VAR}='${shSquoteInner(baseBranch)}' && ${command}`;
5642
+ }
5055
5643
  function shSquoteInner(value) {
5056
5644
  return value.replace(/'/g, "'\\''");
5057
5645
  }
@@ -5204,33 +5792,6 @@ async function refreshBaseBranch(deps, options) {
5204
5792
  }
5205
5793
  return { ok: true };
5206
5794
  }
5207
- async function fetchAndResolveBaseSha(deps, baseBranch) {
5208
- const validationError = validateBranchName(baseBranch);
5209
- if (validationError) {
5210
- return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
5211
- }
5212
- const fetch2 = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
5213
- cwd: deps.cwd
5214
- });
5215
- if (!commandSucceeded(fetch2)) {
5216
- return {
5217
- ok: false,
5218
- error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`
5219
- };
5220
- }
5221
- const resolve2 = await deps.runCommand(
5222
- "git",
5223
- ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`],
5224
- { cwd: deps.cwd }
5225
- );
5226
- if (!commandSucceeded(resolve2)) {
5227
- return {
5228
- ok: false,
5229
- error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`
5230
- };
5231
- }
5232
- return { ok: true, base_sha: resolve2.stdout.trim() };
5233
- }
5234
5795
  async function runWithConcurrency(items, limit, worker) {
5235
5796
  const results = new Array(items.length);
5236
5797
  const effectiveLimit = Math.max(1, Math.floor(limit));
@@ -5252,6 +5813,8 @@ async function runWithConcurrency(items, limit, worker) {
5252
5813
  return results;
5253
5814
  }
5254
5815
  async function createWorktrees(deps, options, worktrunkBinary, baseStartPoint = options.baseBranch) {
5816
+ const exactBase = options.guardStaleWorktree === true && options.nonMutatingBase === true;
5817
+ const behavior = exactBase ? { alignExistingBranchTo: baseStartPoint, verifyHeadMatches: baseStartPoint } : {};
5255
5818
  return runWithConcurrency(
5256
5819
  options.keys,
5257
5820
  options.maxParallel,
@@ -5261,7 +5824,8 @@ async function createWorktrees(deps, options, worktrunkBinary, baseStartPoint =
5261
5824
  options.branchOverrides,
5262
5825
  worktrunkBinary,
5263
5826
  baseStartPoint,
5264
- options.guardStaleWorktree === true
5827
+ options.guardStaleWorktree === true,
5828
+ behavior
5265
5829
  )
5266
5830
  );
5267
5831
  }
@@ -5303,9 +5867,22 @@ function buildResumeModeRemediationFinalizeInstruction() {
5303
5867
  return "Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket (a merge conflict, a CI failure, or requested review changes). First rebase against the current base branch and resolve the merge conflicts. A clean textual merge can still break behavior, so inspect for semantic conflicts even when there are no textual conflict markers. Before you push or mark the ticket complete, run the full test suite for the project (the full unit suite, the same gate enforced by the advisory pre-push hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) and do not rely on targeted subsets as your only verification. Push and mark the ticket complete only after the full suite is green. If you cannot make the full suite pass, report the ticket blocked and escalate rather than pushing a green-looking but broken merge.";
5304
5868
  }
5305
5869
  function buildAgentPrompt(key, opts = {}) {
5306
- const command = `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
5870
+ const workflow = opts.workflow ?? "implement";
5871
+ const head = workflow === "review-and-implement" ? "/review-and-implement" : "/implement-ticket";
5872
+ let command = `${head} ${key}${opts.autoApprove ? " --auto" : ""}`;
5873
+ if (workflow === "review-and-implement") {
5874
+ if (opts.reviewRounds !== void 0) {
5875
+ command += ` --rounds=${opts.reviewRounds}`;
5876
+ }
5877
+ if (opts.baseBranch !== void 0 && opts.baseBranch !== "main") {
5878
+ command += ` --base-branch='${shSquoteInner(opts.baseBranch)}'`;
5879
+ }
5880
+ }
5307
5881
  const parts = [command];
5308
- if (opts.conductorEnabled) parts.push(buildConductorMessageRelayLaunchInstruction());
5882
+ if (opts.conductorEnabled) {
5883
+ parts.push(buildConductorMessageRelayLaunchInstruction());
5884
+ parts.push(buildPrBaseContractLaunchInstruction());
5885
+ }
5309
5886
  if (opts.resumeMode) parts.push(buildResumeModeRemediationFinalizeInstruction());
5310
5887
  return parts.join(" ");
5311
5888
  }
@@ -5330,27 +5907,27 @@ function buildAgentInvocation(agent, prompt, quote, modelAlias) {
5330
5907
  }
5331
5908
  }
5332
5909
  }
5333
- function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
5910
+ function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
5334
5911
  const invocation = buildAgentInvocation(
5335
5912
  agent,
5336
- buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }),
5913
+ buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }),
5337
5914
  (p) => `'${shSquoteInner(p)}'`,
5338
5915
  modelAlias
5339
5916
  );
5340
5917
  return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
5341
5918
  }
5342
- function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
5919
+ function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
5343
5920
  const invocation = buildAgentInvocation(
5344
5921
  agent,
5345
- buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }),
5922
+ buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }),
5346
5923
  powershellSquote,
5347
5924
  modelAlias
5348
5925
  );
5349
5926
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
5350
5927
  }
5351
- function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
5352
- if (platform === "win32") return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
5353
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
5928
+ function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
5929
+ if (platform === "win32") return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
5930
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
5354
5931
  }
5355
5932
  function buildGenericAgentShellCommand(agent, prompt, cwd, platform = "darwin", modelAlias) {
5356
5933
  if (platform === "win32") {
@@ -5559,7 +6136,7 @@ function buildLaunchScriptRunnerCommand(platform, scriptPath) {
5559
6136
  }
5560
6137
  async function pruneStaleLaunchScripts(deps = defaultPruneStaleLaunchScriptsDeps) {
5561
6138
  try {
5562
- const parent = path14.join(os4.tmpdir(), "bridge-start-tickets");
6139
+ const parent = path15.join(os4.tmpdir(), "bridge-start-tickets");
5563
6140
  let entries;
5564
6141
  try {
5565
6142
  entries = await deps.readdir(parent);
@@ -5569,7 +6146,7 @@ async function pruneStaleLaunchScripts(deps = defaultPruneStaleLaunchScriptsDeps
5569
6146
  const cutoff = deps.now() - STALE_LAUNCH_SCRIPT_MAX_AGE_MS;
5570
6147
  for (const entry of entries) {
5571
6148
  if (!entry.startsWith("w-")) continue;
5572
- const full = path14.join(parent, entry);
6149
+ const full = path15.join(parent, entry);
5573
6150
  try {
5574
6151
  const info = await deps.stat(full);
5575
6152
  if (info.mtimeMs < cutoff) {
@@ -5627,22 +6204,24 @@ function buildDryRunResults(keys, overrides) {
5627
6204
  status: "dry-run"
5628
6205
  }));
5629
6206
  }
5630
- function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null) {
6207
+ function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
5631
6208
  return {
5632
6209
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
5633
6210
  // The builder accepts an optional resolved modelAlias; the dry-run caller
5634
6211
  // now passes the previewed tier's alias so `--model` shows in the preview.
5635
6212
  // The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix
5636
- // so the dry-run preview matches the real spawn command exactly.
6213
+ // so the dry-run preview matches the real spawn command exactly. Reuses the
6214
+ // same buildAgentShellCommand/buildAgentPrompt path as a real spawn — dry-run
6215
+ // is never special-cased — so the previewed workflow prompt is exact.
5637
6216
  buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(
5638
- buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled),
6217
+ buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch),
5639
6218
  repoName,
5640
6219
  platform
5641
6220
  )
5642
6221
  };
5643
6222
  }
5644
6223
  function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation) {
5645
- const api = platform === "win32" ? path14.win32 : path14.posix;
6224
+ const api = platform === "win32" ? path15.win32 : path15.posix;
5646
6225
  const mcpJson = api.join(worktreePath, ".mcp.json");
5647
6226
  const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
5648
6227
  const invocation = mcpServerInvocation ?? {
@@ -5661,14 +6240,17 @@ function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platfo
5661
6240
  `DRY-RUN: ${shim}`
5662
6241
  ];
5663
6242
  }
5664
- function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation) {
6243
+ function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds) {
5665
6244
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(
5666
6245
  agent,
5667
6246
  platform,
5668
6247
  env,
5669
6248
  autoApprove,
5670
6249
  conductorEnabled,
5671
- repoName
6250
+ repoName,
6251
+ workflow,
6252
+ reviewRounds,
6253
+ baseBranch
5672
6254
  );
5673
6255
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
5674
6256
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
@@ -6348,7 +6930,12 @@ async function orchestrateStartTickets(deps, options, overrides = {}) {
6348
6930
  options.conductorEnabled ?? false,
6349
6931
  resolvedRepoName,
6350
6932
  // BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
6351
- options.resumeMode ?? false
6933
+ options.resumeMode ?? false,
6934
+ options.workflow,
6935
+ options.reviewRounds,
6936
+ // BAPI-586: the effective run base (already carries any epic.base_branch
6937
+ // override applied above) so conductor workers get BAPI_BASE_BRANCH.
6938
+ options.baseBranch
6352
6939
  );
6353
6940
  if (!platformConfig.ok) return { ok: false, error: platformConfig.error };
6354
6941
  let effectiveBaseStartPoint = options.baseBranch;
@@ -6489,7 +7076,9 @@ async function runStartTicketsCli(argv, overrides = {}) {
6489
7076
  modelAlias,
6490
7077
  options.conductorEnabled ?? false,
6491
7078
  dryRunRepoName,
6492
- dryRunMcpInvocation
7079
+ dryRunMcpInvocation,
7080
+ options.workflow,
7081
+ options.reviewRounds
6493
7082
  )) {
6494
7083
  log(line);
6495
7084
  }
@@ -6555,7 +7144,9 @@ var init_start_tickets = __esm({
6555
7144
  init_agent_registry();
6556
7145
  init_start_tickets_conductor();
6557
7146
  init_bridge_api_client();
7147
+ init_pr_base_contract();
6558
7148
  init_worktree_core();
7149
+ init_base_ref();
6559
7150
  TICKET_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
6560
7151
  DEFAULT_MAX_PARALLEL = 3;
6561
7152
  DEFAULT_TMUX_SESSION_PREFIX = "bridge-start-tickets";
@@ -6572,11 +7163,11 @@ var init_start_tickets = __esm({
6572
7163
  key,
6573
7164
  content
6574
7165
  }) => {
6575
- const parent = path14.join(os4.tmpdir(), "bridge-start-tickets");
7166
+ const parent = path15.join(os4.tmpdir(), "bridge-start-tickets");
6576
7167
  await mkdir3(parent, { recursive: true });
6577
- const dir = await mkdtemp(path14.join(parent, "w-"));
7168
+ const dir = await mkdtemp(path15.join(parent, "w-"));
6578
7169
  const ext = platform === "win32" ? "ps1" : "sh";
6579
- const file = path14.join(dir, `launch-${sanitizeKeyForLaunchScript(key)}.${ext}`);
7170
+ const file = path15.join(dir, `launch-${sanitizeKeyForLaunchScript(key)}.${ext}`);
6580
7171
  await writeFile3(file, content, { mode: 384 });
6581
7172
  return file;
6582
7173
  };
@@ -6742,7 +7333,7 @@ function parseReviewTicketsArgs(argv) {
6742
7333
  const rawReviewEntries = [];
6743
7334
  for (let i = 0; i < argv.length; i++) {
6744
7335
  const arg = argv[i];
6745
- const takeValue2 = () => {
7336
+ const takeValue3 = () => {
6746
7337
  if (i + 1 >= argv.length) return void 0;
6747
7338
  i += 1;
6748
7339
  return argv[i];
@@ -6768,7 +7359,7 @@ function parseReviewTicketsArgs(argv) {
6768
7359
  if (next === void 0 || next.startsWith("-")) {
6769
7360
  return { status: "error", message: "--base-branch requires a value (a branch name)." };
6770
7361
  }
6771
- value = takeValue2();
7362
+ value = takeValue3();
6772
7363
  }
6773
7364
  const trimmed = (value ?? "").trim();
6774
7365
  const error = validateBranchName(trimmed);
@@ -6783,7 +7374,7 @@ function parseReviewTicketsArgs(argv) {
6783
7374
  if (arg.startsWith("--rounds=")) {
6784
7375
  value = arg.slice("--rounds=".length);
6785
7376
  } else {
6786
- value = takeValue2();
7377
+ value = takeValue3();
6787
7378
  if (value === void 0) {
6788
7379
  return { status: "error", message: "--rounds requires a value (1 or 2)." };
6789
7380
  }
@@ -6801,7 +7392,7 @@ function parseReviewTicketsArgs(argv) {
6801
7392
  if (arg.startsWith("--max-parallel=")) {
6802
7393
  maxParallelRaw = arg.slice("--max-parallel=".length);
6803
7394
  } else {
6804
- const value = takeValue2();
7395
+ const value = takeValue3();
6805
7396
  if (value === void 0) {
6806
7397
  return { status: "error", message: "--max-parallel requires a positive integer value." };
6807
7398
  }
@@ -6814,7 +7405,7 @@ function parseReviewTicketsArgs(argv) {
6814
7405
  if (arg.startsWith("--agent=")) {
6815
7406
  value = arg.slice("--agent=".length);
6816
7407
  } else {
6817
- value = takeValue2();
7408
+ value = takeValue3();
6818
7409
  if (value === void 0) {
6819
7410
  return { status: "error", message: "--agent requires a value (an agent name)." };
6820
7411
  }
@@ -6833,7 +7424,7 @@ function parseReviewTicketsArgs(argv) {
6833
7424
  if (arg.startsWith("--model=")) {
6834
7425
  value = arg.slice("--model=".length);
6835
7426
  } else {
6836
- value = takeValue2();
7427
+ value = takeValue3();
6837
7428
  if (value === void 0) {
6838
7429
  return { status: "error", message: "--model requires a value (a model alias)." };
6839
7430
  }
@@ -6852,7 +7443,7 @@ function parseReviewTicketsArgs(argv) {
6852
7443
  if (arg.startsWith("--review=")) {
6853
7444
  value = arg.slice("--review=".length);
6854
7445
  } else {
6855
- value = takeValue2();
7446
+ value = takeValue3();
6856
7447
  if (value === void 0) {
6857
7448
  return { status: "error", message: "--review requires a value (KEY=auto,rounds=1)." };
6858
7449
  }
@@ -7265,9 +7856,9 @@ var init_review_tickets = __esm({
7265
7856
  });
7266
7857
 
7267
7858
  // src/scheduler-backends/types.ts
7268
- import path17 from "node:path";
7859
+ import path18 from "node:path";
7269
7860
  function pathApiForPlatform2(platform) {
7270
- return platform === "win32" ? path17.win32 : path17.posix;
7861
+ return platform === "win32" ? path18.win32 : path18.posix;
7271
7862
  }
7272
7863
  var init_types = __esm({
7273
7864
  "src/scheduler-backends/types.ts"() {
@@ -8233,7 +8824,7 @@ var init_schedule_store = __esm({
8233
8824
 
8234
8825
  // src/command-catalog.ts
8235
8826
  import { promises as nodeFs } from "node:fs";
8236
- import path18 from "node:path";
8827
+ import path19 from "node:path";
8237
8828
  import { z } from "zod";
8238
8829
  function createDefaultCommandCatalogFsDeps() {
8239
8830
  return {
@@ -8313,11 +8904,11 @@ function parseFrontmatterText(text) {
8313
8904
  return { ok: true, frontmatter: { schedulable, interactive, argumentSchema } };
8314
8905
  }
8315
8906
  function commandsDirForRepo(repoPath, platform) {
8316
- const pathApi = platform === "win32" ? path18.win32 : path18.posix;
8907
+ const pathApi = platform === "win32" ? path19.win32 : path19.posix;
8317
8908
  return pathApi.join(repoPath, ".claude", "commands");
8318
8909
  }
8319
8910
  async function discoverCommandCatalog(repoPath, platform, fsDeps = createDefaultCommandCatalogFsDeps()) {
8320
- const pathApi = platform === "win32" ? path18.win32 : path18.posix;
8911
+ const pathApi = platform === "win32" ? path19.win32 : path19.posix;
8321
8912
  const dir = commandsDirForRepo(repoPath, platform);
8322
8913
  let entries;
8323
8914
  try {
@@ -11011,6 +11602,35 @@ var init_local_merge = __esm({
11011
11602
  }
11012
11603
  });
11013
11604
 
11605
+ // src/conductor/plan.ts
11606
+ function canonicalizePlanDAG(plan) {
11607
+ const nodes = plan.nodes.map((node) => ({
11608
+ ...node,
11609
+ ticket_key: node.ticket_key.trim(),
11610
+ depends_on: [...node.depends_on].map((k) => k.trim()).sort(),
11611
+ ...node.touched_files ? { touched_files: [...node.touched_files].sort() } : {}
11612
+ })).sort((a, b) => a.ticket_key.localeCompare(b.ticket_key));
11613
+ const edges = [...plan.edges].map((e) => ({
11614
+ from: e.from.trim(),
11615
+ to: e.to.trim(),
11616
+ ...e.kind ? { kind: e.kind } : {},
11617
+ ...e.overlap_files ? { overlap_files: [...e.overlap_files].sort() } : {}
11618
+ })).sort((a, b) => {
11619
+ const cmp = a.from.localeCompare(b.from);
11620
+ return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
11621
+ });
11622
+ return { plan_version: plan.plan_version, nodes, edges };
11623
+ }
11624
+ function hashPlan(plan) {
11625
+ return stableJsonHash(canonicalizePlanDAG(plan));
11626
+ }
11627
+ var init_plan = __esm({
11628
+ "src/conductor/plan.ts"() {
11629
+ "use strict";
11630
+ init_git_ci_types();
11631
+ }
11632
+ });
11633
+
11014
11634
  // src/conductor/done-gate.ts
11015
11635
  function isPlainObject3(value) {
11016
11636
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -11417,7 +12037,7 @@ var init_done_gate = __esm({
11417
12037
  });
11418
12038
 
11419
12039
  // src/conductor/producer-ledger.ts
11420
- import { createHash as createHash3 } from "node:crypto";
12040
+ import { createHash as createHash4 } from "node:crypto";
11421
12041
  function makeProducerDedupeKey(dimensions) {
11422
12042
  const canonical = {};
11423
12043
  for (const [key, value] of Object.entries(dimensions)) {
@@ -11426,7 +12046,7 @@ function makeProducerDedupeKey(dimensions) {
11426
12046
  return stableJsonHash(canonical);
11427
12047
  }
11428
12048
  function makeStableProducerEventId(dedupeKey) {
11429
- const h = createHash3("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
12049
+ const h = createHash4("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
11430
12050
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
11431
12051
  }
11432
12052
  function isDuplicateConstraintError2(error) {
@@ -11775,6 +12395,9 @@ function discoverPrWithGhCli(options = {}, deps = {}) {
11775
12395
  if (typeof record.headRefName === "string" && record.headRefName.trim().length > 0) {
11776
12396
  discovered.head_ref = record.headRefName.trim();
11777
12397
  }
12398
+ if (typeof record.baseRefName === "string" && record.baseRefName.trim().length > 0) {
12399
+ discovered.base_ref = record.baseRefName.trim();
12400
+ }
11778
12401
  if (typeof record.url === "string" && record.url.trim().length > 0) {
11779
12402
  discovered.url = record.url.trim();
11780
12403
  }
@@ -11789,6 +12412,7 @@ function makeBinding(repo, prNumber, headSha, extra = {}) {
11789
12412
  };
11790
12413
  if (extra.url !== void 0) binding.url = extra.url;
11791
12414
  if (extra.head_ref !== void 0) binding.head_ref = extra.head_ref;
12415
+ if (extra.base_ref !== void 0) binding.base_ref = extra.base_ref;
11792
12416
  return binding;
11793
12417
  }
11794
12418
  function resolvePrHeadBinding(input = {}, deps = {}) {
@@ -11831,7 +12455,11 @@ function resolvePrHeadBinding(input = {}, deps = {}) {
11831
12455
  }
11832
12456
  return {
11833
12457
  ok: true,
11834
- binding: makeBinding(repo, prNumber, localSha, { url: pr.url, head_ref: pr.head_ref })
12458
+ binding: makeBinding(repo, prNumber, localSha, {
12459
+ url: pr.url,
12460
+ head_ref: pr.head_ref,
12461
+ base_ref: pr.base_ref
12462
+ })
11835
12463
  };
11836
12464
  }
11837
12465
  var GH_COMMAND_TIMEOUT_MS, GH_PR_VIEW_ARGS;
@@ -11847,7 +12475,8 @@ var init_pr_discovery = __esm({
11847
12475
  "view",
11848
12476
  "--json",
11849
12477
  // BAPI-494: mergeability fields added to the SAME one-shot call — no new gh process.
11850
- "number,headRefOid,headRefName,url,state,mergeable,mergeStateStatus"
12478
+ // BAPI-586: baseRefName added to the same call so a wrong-base PR is detectable.
12479
+ "number,headRefOid,headRefName,baseRefName,url,state,mergeable,mergeStateStatus"
11851
12480
  ];
11852
12481
  }
11853
12482
  });
@@ -11927,7 +12556,7 @@ function buildGateMetEventInput(binding, evaluation, runId = null, workerId = nu
11927
12556
  function defaultSleep2(ms) {
11928
12557
  return new Promise((resolve2) => setTimeout(resolve2, ms));
11929
12558
  }
11930
- async function observeWithResolved(binding, access2, gateConfig, deps) {
12559
+ async function observeWithResolved(binding, access2, gateConfig, deps, expectedBaseBranch) {
11931
12560
  const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEvent;
11932
12561
  const emitIfNew = deps.emitIfNew ?? ((input, dimensions) => emitConductorEventIfNew(input, dimensions, {
11933
12562
  emitEvent: emitConductorEventFn
@@ -11959,6 +12588,16 @@ async function observeWithResolved(binding, access2, gateConfig, deps) {
11959
12588
  head_sha: binding.head_sha
11960
12589
  });
11961
12590
  result.pr_opened_emitted = prDecision.emitted;
12591
+ const expectedBase = typeof expectedBaseBranch === "string" ? expectedBaseBranch.trim() : "";
12592
+ if (expectedBase) {
12593
+ const observedBase = typeof binding.base_ref === "string" ? binding.base_ref.trim() : "";
12594
+ if (observedBase !== expectedBase) {
12595
+ const actual = observedBase.length > 0 ? observedBase : "(unresolved)";
12596
+ result.gate_met = false;
12597
+ result.reason = `pr-base-mismatch: PR #${binding.pr_number} targets base '${actual}' but the run base is '${expectedBase}'. Rebuild the branch from fresh origin/${expectedBase} and cherry-pick only this ticket's commits; do not retarget the PR base in the GitHub UI.`;
12598
+ return result;
12599
+ }
12600
+ }
11962
12601
  let rawPoll;
11963
12602
  try {
11964
12603
  rawPoll = await pollCi(access2, binding.head_sha);
@@ -12433,7 +13072,7 @@ var init_supervisor_config = __esm({
12433
13072
  });
12434
13073
 
12435
13074
  // src/conductor/supervisor-ledger.ts
12436
- import { createHash as createHash4 } from "node:crypto";
13075
+ import { createHash as createHash5 } from "node:crypto";
12437
13076
  function normalizeDimension(value) {
12438
13077
  return (value ?? "").trim().toLowerCase();
12439
13078
  }
@@ -12448,7 +13087,7 @@ function makeSupervisorIdempotencyKey(meta) {
12448
13087
  return parts.join("|");
12449
13088
  }
12450
13089
  function makeSupervisorAssessmentEventId(idempotencyKey) {
12451
- const h = createHash4("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");
13090
+ const h = createHash5("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");
12452
13091
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
12453
13092
  }
12454
13093
  function isDuplicateConstraintError3(error) {
@@ -13263,12 +13902,12 @@ var init_event_accessors = __esm({
13263
13902
  });
13264
13903
 
13265
13904
  // src/conductor/merge-ledger.ts
13266
- import { createHash as createHash5 } from "node:crypto";
13905
+ import { createHash as createHash6 } from "node:crypto";
13267
13906
  function extractMergeActionIdentityFromGateEvent(event) {
13268
13907
  return getMergeIdentity(event);
13269
13908
  }
13270
13909
  function makeMergeEventId(eventType, actionKey) {
13271
- const h = createHash5("sha256").update(`${eventType}:${actionKey}`).digest("hex");
13910
+ const h = createHash6("sha256").update(`${eventType}:${actionKey}`).digest("hex");
13272
13911
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
13273
13912
  }
13274
13913
  async function lookupMergeEventByActionKey(eventType, actionKey, deps) {
@@ -13751,9 +14390,9 @@ var init_supervisor_runtime = __esm({
13751
14390
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13752
14391
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13753
14392
  import { z as z15 } from "zod";
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";
13755
- import path32 from "path";
13756
- import os14 from "os";
14393
+ 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, open as open2 } from "fs/promises";
14394
+ import path33 from "path";
14395
+ import os15 from "os";
13757
14396
  import { fileURLToPath as fileURLToPath3 } from "url";
13758
14397
 
13759
14398
  // src/pipelines.generated.ts
@@ -14450,7 +15089,7 @@ var INSTRUCTIONS = {
14450
15089
  "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',
14451
15090
  "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',
14452
15091
  "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',
14453
- "execute-plan.md": 'Execute the AI-generated implementation plan for ticket {ticket_key}.\n\n---\n\n## Step 1 \u2014 Retrieve the Full Plan\n\n1. Call the `get_plan` tool for `{ticket_key}` to retrieve the full generated plan as a flat markdown step list.\n2. Derive the total number of implementation steps from the returned plan.\n3. Announce: **"Plan contains N steps."**\n\nThe local file at `{docs_dir}/plans/{ticket_key}-plan.md` is a saved copy of the same plan and may be used as a reference if needed.\n\n## Step 2 \u2014 Execute Each Step Sequentially\n\nFor each step in the plan:\n\n1. **Announce** before starting: **"Step X of N: <step title from plan>"**\n2. **Execute** the step, making code changes as directed.\n3. **Confirm** after completing: **"Step X complete \u2014 <brief summary of what was done>."**\n\n### Rules\n\n- Execute steps in strict sequential order. Do not skip, reorder, or combine steps.\n- Run any tests or checks specified in the plan\'s review steps.\n- Do NOT run `git commit` or `git push` \u2014 leave all changes uncommitted for developer review.\n- If a step is ambiguous or blocked, note the issue clearly (what is ambiguous and why) and continue with the next step.\n\n## Step 3 \u2014 Final Audit\n\nAfter all steps are executed:\n\n1. Review the full plan (already retrieved in Step 1, or the local saved copy at `{docs_dir}/plans/{ticket_key}-plan.md`) to re-enumerate every step.\n2. For any step you are unsure you fully addressed, compare it against the work completed.\n3. List any steps that were skipped or only partially completed, with reasons.\n4. Announce: **"Audit complete \u2014 N of N steps fully addressed."** (or note discrepancies).\n\n## Return\n\nConfirm "Audit complete \u2014 N of N steps fully addressed." (or list discrepancies \u2014 which steps were skipped/partial and why).\n',
15092
+ "execute-plan.md": 'Execute the AI-generated implementation plan for ticket {ticket_key}.\n\n---\n\n## Step 1 \u2014 Retrieve the Full Plan\n\n1. Call the `get_plan` tool for `{ticket_key}` to retrieve the full generated plan as a flat markdown step list.\n2. Derive the total number of implementation steps from the returned plan.\n3. Announce: **"Plan contains N steps."**\n\nThe local file at `{docs_dir}/plans/{ticket_key}-plan.md` is a saved copy of the same plan and may be used as a reference if needed.\n\n## Step 2 \u2014 Execute Each Step Sequentially\n\nFor each step in the plan:\n\n1. **Announce** before starting: **"Step X of N: <step title from plan>"**\n2. **Execute** the step, making code changes as directed.\n3. **Confirm** after completing: **"Step X complete \u2014 <brief summary of what was done>."**\n\n### Rules\n\n- Execute steps in strict sequential order. Do not skip, reorder, or combine steps.\n- Run any tests or checks specified in the plan\'s review steps.\n- Do NOT run `git commit` or `git push` \u2014 leave all changes uncommitted for developer review.\n- If a step is ambiguous or blocked, note the issue clearly (what is ambiguous and why) and continue with the next step.\n- If the current step leaves a requirement ambiguous or blocked by missing ticket detail, call the `get_ticket` tool with `ticket_number` set to `{ticket_key}` to fetch the live Jira ticket details. Use only the returned fields relevant to the unresolved requirement, then continue the affected step. Do not call `get_ticket` unconditionally or as part of initial plan retrieval \u2014 `get_plan` in Step 1 remains the sole unconditional context retrieval.\n\n## Step 3 \u2014 Final Audit\n\nAfter all steps are executed:\n\n1. Review the full plan (already retrieved in Step 1, or the local saved copy at `{docs_dir}/plans/{ticket_key}-plan.md`) to re-enumerate every step.\n2. For any step you are unsure you fully addressed, compare it against the work completed.\n3. List any steps that were skipped or only partially completed, with reasons.\n4. Announce: **"Audit complete \u2014 N of N steps fully addressed."** (or note discrepancies).\n\n## Return\n\nConfirm "Audit complete \u2014 N of N steps fully addressed." (or list discrepancies \u2014 which steps were skipped/partial and why).\n',
14454
15093
  "execute-research.md": "Execute the research plan and produce a consolidated research pack.\n\n## Inputs\n\n- Research plan: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-plan.json`.\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n\n## Instructions\n\n1. Read `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-plan.json`. Execute only the tools listed in `selected_tools`. Do not invoke any tool that is not in that list.\n\n2. For each selected tool:\n - **codebase_search**: search the local working tree using the listed `codebase_search_topics`. Capture file paths, function names, and short excerpts as evidence.\n - **web_search**: run narrow, targeted searches for each item in `web_search_topics`. Capture the source URL and a short summary for each result.\n - **deep_research**: run the deep research query exactly once with the planned `deep_research_query`. Capture the consolidated answer plus the cited URLs.\n\n3. Tool failures must be recorded, not silently dropped:\n - If a tool returns an error, missing-credential message, or empty result, record the failure under `per_tool_failures` in the research pack and continue with the remaining tools.\n - A partial research pack is preferable to no research pack. Do not halt the pipeline because one tool failed.\n\n4. Write two artifacts to the run directory:\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` \u2014 a human-readable consolidated brief. It must include these sections:\n - **Evidence table** \u2014 a structured list of evidence rows: claim, source (file path / URL), and tool that produced it. Render as a bulleted list, not a markdown table (BAPI-320 hygiene).\n - **Codebase references** \u2014 file paths and function names worth citing in the ticket.\n - **External references** \u2014 only present when web/deep search ran; URL + short summary per item.\n - **Unresolved unknowns** \u2014 questions the research could not answer.\n - **Per-tool failures** \u2014 any tool that failed, with the failure reason.\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.json` \u2014 machine-readable counterpart with the same evidence rows, references, unresolved unknowns, and per-tool failures arrays.\n\n5. Mark research failures as warnings in `research-pack.json` so downstream steps can branch on them: include `partial: true` when any selected tool failed.\n\n## Return\n\nConfirm `research-pack.md` and `research-pack.json` were written, and list any tools that failed.\n",
14455
15094
  "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',
14456
15095
  "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",
@@ -14472,7 +15111,7 @@ var INSTRUCTIONS = {
14472
15111
  "request-prd.md": "# request_prd\n\nStart (or refresh) asynchronous generation of a **Product Requirements Document\n(PRD)** for a Jira ticket.\n\nA PRD is the most product/stakeholder-facing document in the design-document\nfamily. It frames product intent \u2014 the problem, goals, non-goals, target users,\nsuccess metrics, product requirements, scope, and risks \u2014 rather than the\ndetailed functional flows and acceptance behavior an FSD covers, or the\narchitecture/implementation guidance a TDD covers.\n\n## Async request/retrieve pattern\n\n`request_prd` only **starts** generation; it does not return the PRD directly\nunless you set `wait_for_result`. PRD generation typically takes **2\u20134 minutes**.\n\n1. Call `request_prd` with the `ticket_number`.\n2. Wait for processing to complete (2\u20134 minutes).\n3. Call `get_prd` with the same `ticket_number` to retrieve the result.\n\nSet `wait_for_result: true` to block and return the PRD content directly instead\nof polling separately.\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| `wait_for_result` | boolean | `false` | When `true`, block and poll until the PRD is ready, then return it directly. |\n| `save_locally` | boolean | `true` | When `wait_for_result` is `true`, save the PRD to `BAPI_DOCS_DIR/prd/{ticket}-prd-plan.md`. |\n| `second_opinion` | string | \u2014 | Provider routing override for **this** generation request (e.g. `anthropic`, `openai`, `gemini`). This is **not** the standalone `second_opinion` tool \u2014 it only changes which provider produces this request's artifact, and takes precedence over `provider`. |\n| `provider` | string | \u2014 | Pure provider switch without second-opinion semantics. If both `provider` and `second_opinion` are set, `second_opinion` wins. |\n\n## Return\n\n- `202` when the request is accepted (async dispatch).\n- `404` if the ticket does not exist in Jira.\n- `403` if the API key is unauthorized.\n",
14473
15112
  "research-decision.md": 'Decide which research tools to run for this idea, biased toward cheap local research first.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` (must already exist from the preflight step).\n\n## Instructions\n\n1. Read `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`. This is the source of truth for `idea`, `readiness`, `scope`, and `run_id`. If the file does not exist, halt locally \u2014 the preflight step did not complete.\n\n2. Decide which research tools should run for this idea, in roughly this priority order:\n - **Local codebase research first.** Inspect the working tree (search, grep, file reads) for prior art, related modules, and existing tests. Prefer this for anything that touches code you already own.\n - **Narrow web search second.** Use targeted web search for short factual lookups: a specific library API, a known external standard, a public spec.\n - **Deep research only when justified.** Deep research is expensive and slow; it must be earned by one of the rubric items below.\n\n3. Deep-research allowance rubric. Deep research is only allowed when at least one of these is true:\n - **blast radius**: the change spans many systems or has high reversibility cost (e.g., schema migrations, auth, billing, public APIs).\n - **unfamiliar external domain**: the idea depends on a third-party domain or specification the repository has no prior coverage of.\n - **compliance/security uncertainty**: there is real compliance or security uncertainty (SOC2, PII, secret handling, access control).\n - **cheaper research failed**: a cheaper round (local + narrow web search) already happened in this run and left blocking unknowns.\n - **explicit user request**: the user explicitly asked for deep research.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-plan.json`. Required fields:\n - `selected_tools` \u2014 array of tool identifiers to run, drawn from at least `["codebase_search", "web_search", "deep_research"]`. Empty array is allowed when no research is needed.\n - `rationale` \u2014 short string explaining the choice in terms of the rubric above.\n - `deep_research_query` \u2014 string. Required when `deep_research` is in `selected_tools`, otherwise empty string.\n - `web_search_topics` \u2014 array of strings; may be empty.\n - `codebase_search_topics` \u2014 array of strings; may be empty.\n - `expected_unknowns` \u2014 array of strings describing what the research is expected to resolve.\n\n5. Do not invoke any research tool from this step \u2014 that happens in `execute-research.md`. This step only writes the plan.\n\n## Return\n\nConfirm `research-plan.json` was written, list `selected_tools`, and quote the rationale.\n',
14474
15113
  "screen-and-resolve.md": 'Apply project standards and the minimum-evidence gate before drafting.\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` (may be partial or absent).\n- Duplicate assessment: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json`.\n- Project standards: the response from the earlier `get_project_standards` step in the same pipeline run. Treat the standards as unavailable when that response was an error envelope, a 404, or missing.\n\n## Instructions\n\n1. Produce three artifacts in the run directory:\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/open-questions.md`\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`\n\n2. Standards checklist:\n - When `get_project_standards` returned a usable result, derive the checklist items from those standards. Each item is a `{requirement, satisfied, evidence}` triple where `evidence` either cites the research pack or notes "deferred to draft".\n - When standards are unavailable (404, error envelope, missing), generate a fallback baseline checklist covering at least: requirements clarity, acceptance criteria presence, testability, security/PII consideration, and rollback/observability when scope warrants it. Mark each as `satisfied: false` with `evidence: "fallback baseline \u2014 no project standards available"`.\n\n3. Open questions:\n - List every unresolved unknown that blocks drafting. Pull from the research pack\'s `unresolved_unknowns` and from your own reading of `idea`.\n - Each question gets its own bullet (no markdown tables, no `- [ ]` checkboxes \u2014 BAPI-320 hygiene).\n - If the question has a defensible best-guess answer, write it in `resolved-uncertainties.md` instead, with explicit assumption language ("Assuming X because Y...").\n\n4. Resolved uncertainties:\n - Mirror open questions that have defensible best-guess answers. Each entry must include `assumption`, `basis` (research-pack reference or codebase reference), and `confidence` ("low", "medium", "high").\n\n5. Minimum-evidence gate. Halt locally if ALL of the following are true:\n - The research pack contains no codebase references for the idea.\n - The standards checklist has zero items (even the fallback baseline is missing).\n - `resolved-uncertainties.md` records no explicit assumptions.\n When the gate fires, do not continue to drafting. Report the halt and direct the user to either run a smaller idea, run `--allow-duplicate` semantics for replays, or supply more context manually.\n\n## Return\n\nConfirm the three artifacts were written and whether the minimum-evidence gate fired.\n',
14475
- "store-and-approve-epic-plan.md": 'Store the approved epic plan DAG in the backend and approve it.\n\nThis step runs after the user has approved the decomposition in the\n`decompose-epic` step. It reads the machine-readable sidecar written by that\nstep and wires it into the backend durable store.\n\n## Variables\n\n- `{epic_key}` \u2014 Jira epic key (e.g. BAPI-405)\n- `{epic_slug}` \u2014 lowercase-hyphen slug derived from the epic key\n- `{docs_dir}` \u2014 base docs directory (e.g. `docs/tmp`)\n\n## Step 1 \u2014 Read the DAG sidecar\n\nRead the structured JSON sidecar from\n`{docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json`.\n\nThe DAG must be serialized from this sidecar \u2014 **never by re-parsing the\nmarkdown** (`epic-plan.md`). If the sidecar is missing or unparseable, warn\nthe user with:\n\n> "The structured DAG sidecar (`epic-plan.dag.json`) is missing or invalid.\n> The plan cannot be stored automatically. To recover, you can reconstruct the\n> DAG manually by parsing the Jira dependency links for each sub-task\n> (deterministic Jira-link DAG builder \u2014 documented fallback, not built here)."\n\nThen stop this step with a warning (do not raise an error that aborts the\nentire pipeline).\n\n## Step 2 \u2014 Create the epic run\n\nCall `mcp__bridge-api__create_epic_run` (or equivalent) to create the epic run\nrecord for `{epic_key}`. If the run already exists (HTTP 409), read the\nexisting run ID and proceed with that run \u2014 do not fail.\n\nAlternatively, if a `create_epic_run` MCP tool is not available, make a direct\nAPI call to `POST /jira/epic-runs/runs` with `{ repo_name, epic_key, status: "planning" }`.\n\n## Step 3 \u2014 Compute the plan hash and store the plan\n\nUse the `storeEpicPlan` conductor client method (or equivalent) to POST the\nplan blob:\n\n- `plan_version`: the integer `plan_version` field from the DAG sidecar (must be \u2265 1).\n- `plan_blob`: the full parsed DAG object from the sidecar.\n- `plan_hash`: computed by `hashPlan(dag)` from `mcp_server/src/conductor/plan.ts`.\n If the hash cannot be computed locally, pass the SHA-256 hex of the\n canonical JSON string (keys sorted, no extra whitespace) as a fallback.\n\nIf the store call returns HTTP 409 with a hash mismatch, warn the user (substituting the actual version number from the sidecar):\n\n> "Plan version N is already stored with a different hash.\n> Increment `plan_version` in the sidecar and retry."\n\nThen stop this step.\n\n## Step 4 \u2014 Approve the plan\n\nCall `approveEpicPlan` for `plan_version` from the sidecar. On success, the\nbackend transitions the run to `active` status.\n\nIf HTTP 409 is returned (superseded), warn the user:\n\n> "A later plan version is already approved \u2014 approval skipped."\n\n## Return\n\nReport:\n- The epic run ID.\n- The stored `plan_version`.\n- The `plan_hash` returned by the approve call.\n- Whether the run transitioned to `active`.\n\nExample: "Plan v1 stored and approved for epic run `<epic_run_id>`. Run is now active."\n',
15114
+ "store-and-approve-epic-plan.md": 'Store the approved epic plan DAG in the backend and approve it.\n\nThis step runs after the user has approved the decomposition in the\n`decompose-epic` step. It reads the machine-readable sidecar written by that\nstep and wires it into the backend durable store.\n\nThe whole step is a single deterministic command \u2014 `setup-epic`. Do **not**\nhand-roll the HTTP calls, and do **not** compute the plan hash yourself.\n\n## Variables\n\n- `{epic_key}` \u2014 Jira epic key (e.g. BAPI-405)\n- `{epic_slug}` \u2014 lowercase-hyphen slug derived from the epic key\n- `{docs_dir}` \u2014 base docs directory (e.g. `docs/tmp`)\n\n## Step 1 \u2014 Confirm the DAG sidecar exists\n\nThe sidecar written by `decompose-epic` lives at\n`{docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json`.\n\nIf it is missing, warn the user with:\n\n> "The structured DAG sidecar (`epic-plan.dag.json`) is missing or invalid.\n> The plan cannot be stored automatically. To recover, you can reconstruct the\n> DAG manually by parsing the Jira dependency links for each sub-task\n> (deterministic Jira-link DAG builder \u2014 documented fallback, not built here)."\n\nThen stop this step with a warning (do not raise an error that aborts the\nentire pipeline).\n\nThe DAG must always come from this sidecar \u2014 **never from re-parsing the\nmarkdown** (`epic-plan.md`).\n\n## Step 2 \u2014 Run `setup-epic`\n\n```bash\nnpx -y @bridge_gpt/mcp-server setup-epic \\\n --epic-key {epic_key} \\\n --plan-file {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json \\\n --json\n```\n\nThis creates the epic run, stores the plan blob, and approves it \u2014 in the one\norder that is safe. It is idempotent: re-running it on an epic that already has\na live run reuses that run rather than creating a second one.\n\n**Never work around a failure by POSTing to `/jira/epic-runs/runs` directly.**\nCreating a second active run for an epic wedges it permanently \u2014 every later\nplan call fails with "Multiple active runs" \u2014 and double-charges billing.\n\nPass `--dry-run` first if you want to validate the plan and preview the calls\nwithout changing anything.\n\n## Step 3 \u2014 Interpret the result\n\n`setup-epic` exits `0` on success and prints a JSON object with `epic_run_id`,\n`plan_version`, `plan_hash`, `status`, and any `warnings`.\n\nOn a non-zero exit, relay its error message verbatim \u2014 it is already actionable.\nThe three you are most likely to see:\n\n- **"already stored with a DIFFERENT hash"** \u2014 the plan blob changed after being\n stored. Increment `plan_version` in the sidecar and re-run.\n- **"MULTIPLE active runs"** \u2014 the epic is wedged. It must be repaired by\n abandoning the duplicate run before anything else can proceed.\n- **"no touched_files"** (a warning, not an error) \u2014 the plan cannot be protected\n by file-overlap serialization. Mention it, but do not stop.\n\nA `plan_hash` that differs from the locally computed hash is **not** an error:\nthe server re-hashes after applying file-overlap serialization, and the server\'s\nhash is authoritative.\n\n## Return\n\nReport:\n- The epic run ID.\n- The stored `plan_version`.\n- The `plan_hash` returned by `setup-epic`.\n- Whether the run reached `active`.\n- Any warnings.\n\nExample: "Plan v1 stored and approved for epic run `<epic_run_id>`. Run is now\nactive \u2014 the server-side reconciler will pick it up within ~30s."\n',
14476
15115
  "update-ticket-rewrite.md": "Rewrite the Jira ticket description for {ticket_key} using the generated clarifying questions and critique documents.\n\n1. Fetch the current ticket description using the `get_ticket` tool with ticket_number `{ticket_key}`.\n2. Read the clarifying questions from the local file saved by the previous step (check `{docs_dir}/clarifying-questions/` for `{ticket_key}-clarifying-questions.md`). For each best-guess answer, verify it against the codebase using file search and code grep. Accept verified answers, correct inaccurate ones with evidence, and let ambiguous ones stand.\n3. Read the critique from the local file saved by the previous step (check `{docs_dir}/ticket-critiques/` for `{ticket_key}-ticket-quality-critique.md`). Address all Requested Changes. Apply Points to Consider selectively \u2014 accept genuine improvements, skip stylistic preferences.\n4. Write the rewritten ticket in standard markdown format (not Jira wiki markup). Preserve the Summary, Requirements, and Acceptance Criteria structure.\n5. Save the output to `{docs_dir}/tickets/{ticket_key}.md`. Output only the clean rewritten ticket \u2014 no meta-commentary.\n\n## Return\n\nConfirm the rewritten ticket was saved to `{docs_dir}/tickets/{ticket_key}.md` and briefly note which clarifying-question answers were corrected against the codebase and which critique Requested Changes were addressed.\n",
14477
15116
  "upload-and-track.md": 'Step-10 umbrella upload instruction. Idempotently create the Jira ticket(s) for this run, attach the full draft(s), and call `track_ticket`.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Draft metadata: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json`.\n- For epic runs, this instruction is also responsible for producing or refreshing `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json` before any Jira mutation, by following `decompose-epic-candidate.md` (hard cap `{max_children}`).\n- Pipeline variable `auto_approve_external` controls whether the external-mutation pause is skipped (for this run, `auto_approve_external` = `{auto_approve_external}`). Treat the literal string `"true"` as skip; any other value (including `"false"`, missing, or empty) means pause and ask.\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`, `create_ticket`, `attachment` (operations: `upload`, `list`), `update_ticket_description`, `track_ticket`, and `add_comment`, and to execute the shared `gather-and-attach-materials.md` instruction, as directed below \u2014 performing orchestrator-directed tool calls is not "re-orchestrating".\n\n1. Read `run-manifest.json` and `draft-metadata.json`. Branch internally based on the manifest\'s `scope`:\n - `task` or `spike` \u2192 follow the **Single-ticket path** below.\n - `epic_candidate` \u2192 follow the **Epic path** below.\n The orchestrator does not support conditional steps; this branching lives in agent logic.\n\n2. External approval gate, applied before any mutating MCP tool call:\n - If `auto_approve_external` is `"false"` (or any non-`"true"` value), summarize the exact planned Jira mutations \u2014 list every `create_ticket`, `attachment` (operation: `"upload"`), and `track_ticket` call with its key arguments \u2014 and ask the user for explicit confirmation in this agent task before proceeding.\n - If `auto_approve_external` is `"true"`, proceed without the confirmation pause.\n\n3. **Single-ticket path** (`scope` is `task` or `spike`):\n 1. Idempotency lookup. Call `get_tickets` with its `labels` parameter set to both the per-run label `<idempotency_label>` and the stable `bapi-idea-hash-{idea_hash}` label from `draft-metadata.json` (comma-separated). If a match is found by either label, reuse that ticket key and skip `create_ticket`.\n 2. If no match was found, call `create_ticket` with `summary`, `slim_description` as the description, `issue_type`, and `labels` exactly as written in the metadata. Capture the returned `ticket_key`.\n 3. Upload the full markdown draft via `attachment` (operation: `"upload"`) using `attachment_path`.\n 4. **Gather and attach referenced materials.** Execute the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the resolved ticket key, `draft_file_path` = `attachment_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value. It attaches reachable local text materials and records external/auth-gated and binary/image materials per its own warn-not-halt rules. Any attach failure it reports is recorded (via `update_ticket_description`) as `partial_success` and never halts this step.\n 5. Call `track_ticket` with the resolved ticket key so Bridge API picks the new ticket up.\n 6. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/upload-state.json` describing the final state.\n\n4. **Epic path** (`scope` is `epic_candidate`):\n 1. If `decomposition-plan.json` does not yet exist for this run, follow `decompose-epic-candidate.md` first to produce it (hard cap `{max_children}`).\n 2. Draft any surviving children that lack a draft on disk by calling `jira-ticket-writer` per child with the `draft_path` from the decomposition plan. After drafting, extend `draft-metadata.json` so `children[]` mirrors the final list from the decomposition plan.\n 3. Parent first. Look up the Epic parent by `bapi-idea-to-ticket-{run_id}-parent` via `get_tickets`. If found, reuse that key; otherwise call `create_ticket` with the parent\'s summary, slim description, issue type `Epic`, and parent labels. Attach the Epic draft via `attachment` (operation: `"upload"`) using `parent.attachment_path`. Then **gather and attach the Epic parent\'s referenced materials** by executing the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the Epic key, `draft_file_path` = `parent.attachment_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value. Then call `track_ticket` for the Epic key.\n 4. Children next. For each child in order:\n - Look up by the child\'s `idempotency_label`. If found, reuse that key.\n - Otherwise call `create_ticket(parent_key=<epic_key>)` with the child\'s `summary`, `slim_description`, `issue_type`, and `labels`. The `parent_key` is required so Jira\'s modern parent linkage is set.\n - Upload the child draft via `attachment` (operation: `"upload"`) using `draft_path`.\n - **Gather and attach this child\'s referenced materials** by executing the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the child key, `draft_file_path` = `draft_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value.\n - Call `track_ticket` for the child key.\n 5. After every parent or child mutation, write partial progress to `{docs_dir}/idea-to-ticket/{slug}-{run_id}/upload-state.json` so a later resume can pick up exactly where the run stopped.\n 6. **Recommended implementation order comment.** Once the Epic parent and all surviving children exist (real keys known), post a single comment on the Epic via `add_comment` with `ticket_number` set to the Epic key. The comment carries (a) a short System Goals / Non-Functional Requirements summary from `goals-and-nfrs.md`, and (b) the **Recommended Implementation Order** \u2014 the children in order, each referenced by its real Jira key, derived from the `depends_on` / `recommended_after` / `order_rationale` fields in `decomposition-plan.json`. State that this is recommended sequencing only \u2014 do **not** create Jira dependency links and do **not** attach a separate markdown doc. Skip this only if the run reused a pre-existing comment for the same run (idempotency); do not post duplicate order comments on resume.\n\n5. Required child label set whenever any child is created: `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and `bapi-idea-to-ticket-{run_id}-child-<N>` (1-based index from the decomposition plan).\n\n6. Partial-failure recovery rules:\n - If `create_ticket` succeeds but `attachment` (operation: `"upload"`) fails, record the outcome as `partial_success` in `upload-state.json` and continue with the next planned mutation; do not retry inside this step.\n - If the Epic parent is created successfully but one or more children fail, preserve the parent key and any completed child keys in `upload-state.json` before raising the failure.\n - On resume of any prior run, search by every relevant idempotency label first (`bapi-idea-to-ticket-{run_id}` for single tickets, `bapi-idea-to-ticket-{run_id}-parent`, and each `bapi-idea-to-ticket-{run_id}-child-<N>`) before considering any `create_ticket` call. Idempotency labels are how this pipeline avoids creating duplicate tickets across retries.\n\n## Return\n\nConfirm the run\'s final upload outcome: attachment results, `track_ticket` outcome, and any `partial_success` rows recorded in `upload-state.json`.\n\nThen, as the FINAL content of your reply, emit a fenced ```json block holding the authoritative payload for this run \u2014 and nothing else. The chain reads ONLY this final fenced JSON block to pick its review / start-tickets targets, so it must contain exactly the keys from `upload-state.json` and never any key you merely looked up during duplicate detection. Duplicate-detection / looked-up keys must not appear in this authoritative payload unless they are the final created/reused ticket for this run.\n\nThere are exactly two authoritative final payload shapes:\n\n- **Single-ticket path** (`scope` is `task` or `spike`): emit strictly `created_ticket_keys` containing **exactly one** implementable ticket key. `created_ticket_keys` is only for the single-ticket `task`/`spike` path and must contain exactly one implementable ticket key:\n\n ```json\n {"created_ticket_keys": ["BAPI-331"]}\n ```\n\n- **Epic path** (`scope` is `epic_candidate`): emit the Epic parent key separately as `epic_parent_key`, and the implementable children as `child_ticket_keys`:\n\n ```json\n {"epic_parent_key": "BAPI-400", "child_ticket_keys": ["BAPI-401", "BAPI-402"]}\n ```\n\n `child_ticket_keys` contains **only** implementable child Task/Spike ticket keys, listed in final decomposition order. `child_ticket_keys` must **never** include the Epic parent key.\n',
14478
15117
  "upload-epic-hierarchy.md": 'Standalone Epic upload protocol. Use as the detailed reference for the Epic path triggered from `upload-and-track.md`.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` with `scope == "epic_candidate"`.\n- Draft metadata: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json` with a populated `parent` and `children`.\n- Decomposition plan: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json`.\n- Pipeline variable `auto_approve_external` governs the external-mutation pause as in `upload-and-track.md` (for this run, `auto_approve_external` = `{auto_approve_external}`).\n\n## Instructions\n\n1. Parent idempotency lookup. Search Jira via `get_tickets` for issues carrying the label `bapi-idea-to-ticket-{run_id}-parent`. If a match exists, reuse that ticket key as the Epic parent and skip `create_ticket` for the parent. Otherwise call `create_ticket` with the parent\'s summary, slim description, `issue_type = "Epic"`, and labels including `ai-generated`, `idea-to-ticket`, and `bapi-idea-to-ticket-{run_id}-parent`. After creation or reuse, upload the Epic draft via `attachment` (operation: `"upload"`) and call `track_ticket`.\n\n2. Capture the resolved Epic key into a local variable `epic_key`. Every subsequent child mutation must reference this exact key.\n\n3. Per-child idempotency lookup. For each child in `decomposition-plan.json` (in order), search Jira by the child\'s `idempotency_label` (`bapi-idea-to-ticket-{run_id}-child-<N>`). If a match exists, reuse that key and skip `create_ticket` for that child. Otherwise call `create_ticket(parent_key=<epic_key>)` with:\n - `summary` \u2014 child summary.\n - `slim_description` \u2014 child slim description.\n - `issue_type` \u2014 typically `Task` (or `Spike` when the child is primarily discovery).\n - `labels` \u2014 `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and the child\'s own `bapi-idea-to-ticket-{run_id}-child-<N>` label.\n The `parent_key` argument is REQUIRED for every child `create_ticket` call so Jira sets the modern parent relationship; never omit it.\n\n4. After each child is created or reused, upload its draft via `attachment` (operation: `"upload"`) using the child\'s `draft_path`, then call `track_ticket` for that child key, then append the child outcome to `upload-state.json` in the run directory.\n\n5. On partial failure (e.g., parent succeeded, third child failed), preserve `epic_key` plus every completed child key in `upload-state.json`. The next run of this protocol must rediscover those keys via the idempotency-label lookups in steps 1 and 3 before considering any new `create_ticket` call.\n\n## Return\n\nConfirm the Epic key, the number of children created vs reused vs failed, and the path of the updated `upload-state.json`.\n',
@@ -14483,7 +15122,7 @@ var INSTRUCTIONS = {
14483
15122
  init_version_generated();
14484
15123
 
14485
15124
  // src/readme.generated.ts
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';
15125
+ 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\n**Were you sent a bootstrap invite?** Then you don\'t need an API key or the web UI\nat all \u2014 run the command your operator gave you:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --invite\n```\n\nThat one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap\ninvite token with **echo suppressed**, and sends it only in the request body. It\ncreates your project and mints your own admin API key in a single command.\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 \u2014 **`--invite` is the one exception** (below). The key is\n **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\n#### Bootstrap-invite onboarding (`--invite`)\n\nWith a **bootstrap invite** there is no pre-existing key and no web UI: this is the\none mode where `install-bridge` **creates** the project and its first admin key\ninstead of consuming one. Run `install-bridge --invite` and it:\n\n1. **Prompts for the bootstrap invite token** with echo suppressed (the default \u2014\n see below).\n2. **Generates your `key_secret`** (32 CSPRNG bytes) and **fsyncs it locally**\n *before* contacting the server. If that write fails the run aborts and the\n invite is **not** spent.\n3. **Redeems the invite** \u2014 `POST /setup/bootstrap` with the token, repo name, and\n `key_secret` in the **body** \u2014 which creates the project and mints your admin\n key. (This replaces the connectivity ping: there is no key to ping with yet.)\n4. **Verifies the newly-minted key**, then writes the per-host MCP config.\n5. **Promotes the credential** to `bapi:<repo>` and **opens the agent session** \u2014\n the same Steps 3\u20135 as the normal flow.\n\nBecause the locally-saved `key_secret` is the only proof that can replay a\nredemption, a re-run after a network failure is safe: it re-sends the *same* secret\nand gets back the *same* project and key. If the repo name you chose is already\ntaken (names are globally unique) the server rolls back \u2014 your invite is untouched \u2014\nand the CLI asks for a different name and retries with the same token.\n\n**The delivered one-liner is secret-free, by design.** "A copy/paste one-liner" and\n"the token never touches shell history" are contradictory, so the token is *not* in\nthe command: the CLI asks for it, and it travels only in the request body.\n`--invite <token>`, `--invite=<token>`, and `BAPI_INVITE` still work for\n**scripting only** \u2014 and both forms **expose the token to your shell history and to\nthe process list**. Prefer the prompt.\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 With `--invite` it also never calls the exchange endpoint and never generates or\n stores a secret.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config, or in the\n credential store, without 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- `--invite [token]` \u2014 redeem a bootstrap invite (mutually exclusive with\n `--api-key`). Omit the value to get the hidden prompt.\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 \xB7 `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs (default `implement`, byte-identical to today; `review-and-implement` runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session) \xB7 `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement`.\n\n**2b. Review and Start**\n- **What it does:** Spawns one worktree per ticket, each running review then (after a per-ticket human proceed/halt gate) implementation \u2014 the chained `review \u2192 gate \u2192 implement` composition.\n- **When it\'s useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/full-automation`, which only accepts an idea).\n- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).\n- **Flags:** `--auto` a single chain-level flag that auto-approves both the review and the implementation phase of every spawned session \xB7 `--rounds=1|2` forwarded to the review phase \xB7 `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.\n- Under the hood, this is a thin shim over `start-tickets --workflow review-and-implement <KEYS>` \u2014 the lower-level launcher seam documented above; the halt-gate decision logic lives in the spawned `/review-and-implement` session, never in this command or the CLI.\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`. For existing ticket keys, `/review-and-start` (see [Usage Documentation \u2192 Review and Start](#tier-1--regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\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| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\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### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\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 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\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';
14487
15126
 
14488
15127
  // src/update-check.ts
14489
15128
  init_version_generated();
@@ -14819,7 +15458,7 @@ var COMMANDS = {
14819
15458
  "bridge-research.md": 'Run multi-source, fact-checked web research via Bridge API and save a cited report 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\nParse `$ARGUMENTS`:\n- The required `query` is the full text of `$ARGUMENTS` after removing any recognized flags.\n- An optional `--ticket <KEY>` flag captures a Jira ticket key (e.g., `BAPI-123`) to associate the research with a specific ticket. If `--ticket` appears, treat the immediately following token as the ticket key and remove both from the query.\n- If `$ARGUMENTS` is empty, or the query (after flag removal) is blank, stop immediately and display:\n\n```\nUsage: /bridge-research <question> [--ticket PROJ-123]\nExample: /bridge-research "Best practices for rate limiting in FastAPI?"\n```\n\n## Step 2 \u2014 Resolve Docs Directory\n\nCall `get_docs_dir` (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 \u2014 Run Research\n\nCall `request_deep_research` with:\n- `query`: the parsed question\n- `wait_for_result`: `true`\n- `ticket_number`: the value from `--ticket` if provided; omit this parameter entirely if not present\n\nThis step polls until the research completes (up to 15 minutes) and returns the full cited report directly. The tool appends a literal `Saved to <path>` line to its result \u2014 extract that line and store the path as `saved_path`.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nResearch failed: <error message from the tool>\n```\n\n## Step 4 \u2014 Confirm\n\nDisplay a confirmation message:\n\n```\nResearch complete.\nSaved to: {saved_path}\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Bridge Research Report\n\n- **Query**: <query>\n- **Status**: Completed\n- **Local File**: {saved_path}\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n',
14820
15459
  "check-ci.md": '# Check CI: $ARGUMENTS\n\n$ARGUMENTS\n\n> **Warning**: Keep this file behaviorally in sync with `mcp_server/instructions/monitor-ci-checks.md` to prevent drift (BAPI-462).\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), discovers CI checks for the current commit, polls their status, and applies confidence-gated code corrections for failures. It is designed to run after `/create-pr` completes.\n\nIf any critical stage fails (Stage 0), stop immediately and report which stage failed and why. Non-critical stages (Stage 1, Stage 2, and Stage 3) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to monitor and respond to CI checks 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: /check-ci <ticket_key> (e.g., /check-ci 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 commit SHA**: Run `git rev-parse HEAD` in the terminal. Store the result as `commit_sha`.\n\n4. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `current_branch`.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Resolve CI Checks\n\n1. **Initial resolution**: Call the `resolve_ci_checks` MCP tool immediately with `commit_ref` set to `commit_sha`. Do NOT wait before calling \u2014 the cache state is unknown until the first call returns.\n\n2. **Handle the response**:\n - If the response contains `source: "cached"`: The checks were already resolved. Skip the wait and proceed to evaluate the check list.\n - If the response contains `source: "new"` and the check list is empty: CI checks have not registered yet. Run `sleep 45` in the terminal to wait for checks to appear, then call `resolve_ci_checks` again with `commit_ref` set to `commit_sha` and `force_rerun` set to `true`.\n - If the response contains `source: "resolved"` on the first call: Present the resolved checks to the user for approval before proceeding.\n\n3. **Evaluate the check list**:\n - If the response contains `available: false`: Warn that CI check resolution is not available and skip to Stage 3.\n - If the check list is empty or all checks have `detail_level: "none"`: Warn that no actionable CI checks were found and skip to Stage 3.\n - Otherwise: Store the resolved checks and proceed to Stage 2.\n\n4. **Required-check source**: Each resolved check carries a `required` field (from GitHub Branch Protection, or an LLM classification fallback \u2014 the same signal `poll_ci_checks` echoes back per check in Stage 2). Treat `required: false` as non-required (e.g. `pip-audit`); treat a missing field or `required: true` as required. This per-check field is the tool-provided proxy for the Conductor done-gate\'s authoritative required-checks set (`mcp_server/src/conductor/done-gate.ts`) \u2014 do not invent your own required/non-required classification in prose. The done-gate\'s own `required_checks` list is not directly queryable by a worker; `wait_for_done_gate` (Stage 2) is the authoritative backstop that evaluates it server-side.\n\nThis stage is **non-critical** \u2014 warn on failure or empty results, skip to Stage 3.\n\n## Stage 2 \u2014 Poll CI Checks + Correction Loop\n\nInitialize `retry_count = 0` and `max_retries = 2`.\n\n**Conductor steerability**: If launched under the Conductor (the `BAPI_CONDUCTOR_RUN_ID` and `BAPI_CONDUCTOR_WORKER_ID` env identifiers are present), call the `check_messages` MCP tool once per poll cycle.\n- Returned messages are **advisory supervisor guidance**, are acknowledged by the call (not redelivered), and are advisory context for the next fix batch only.\n- Fold concrete fix hints, "skip this flaky check," or "stop and wait" directions into how you handle failures.\n- **Guardrails**: Guidance is strictly advisory and never mutates the session. The deterministic confidence-gating, single-batch fix, single commit/push, and `detail_level` rules remain authoritative. Guidance never overrides the deterministic rules and never causes a fix you are not confident in.\n- **Fail-open**: If `check_messages` errors with an identity-unavailable message (e.g. "Conductor worker identity is unavailable"), you were not launched under the Conductor. Stop calling it for the rest of the run and proceed normally.\n\n1. **Polling loop**: Poll CI check status. In each cycle, call `poll_ci_checks` with `commit_ref` set to `commit_sha`, then (if applicable) perform the Conductor-gated `check_messages` call, and finally run `sleep 30` in the terminal. Continue polling until `all_complete` is `true` or 10 minutes have elapsed (approximately 20 poll cycles).\n\n2. **On poll completion \u2014 required-subset evaluation**: Partition the polled checks into `required` (checks with `required: true` or a missing `required` field) and `non_required` (checks with `required: false`, e.g. `pip-audit`). Compute `required_green` = every required check is complete and green. Do **not** gate on the aggregate `all_passed` flag \u2014 a red non-required check must never block progression.\n - Non-required failures are reported in the Stage 3 breakdown but are **never blocking**: they do not gate progression, do not consume `retry_count`, and are not sent through the fix loop in item 3.\n - If `required_green` is `false` (a required check is still red), proceed to item 3 to attempt fixes for the failing **required** checks only.\n - If `required_green` is `true`:\n - **Review verdict gating**: if `claude-review` is one of the required checks, its GitHub check reaching a non-pending/"success" state means only that the review action *ran* \u2014 this is **transport completion, not approval**. Fetch the PR\'s comments (e.g. `gh pr view --json comments`) and look for the sticky comment\'s machine-readable verdict line. The review counts as approved only when that comment contains `claude-review-verdict: approved` on its own line **and** the accompanying `Reviewed-SHA:` line matches the current `commit_sha` \u2014 a verdict posted against an older head does not count.\n - **Missing or stale verdict** (no `claude-review-verdict:` line at all, or a `Reviewed-SHA:` that does not match the current `commit_sha`): do not treat the review as approved. Report it in Stage 3 and do **not** call `wait_for_done_gate` this cycle \u2014 a missing or stale verdict is not approval.\n - **`claude-review-verdict: changes_requested`** (the reviewer rejected the current head): do **not** call `wait_for_done_gate` for the rejected old head. When you are confident you can address the review, remediate it in-session \u2014 mirroring the confidence-gated commit+push already specified for CI-failure fixes \u2014 instead of merely reporting it:\n - Read the review findings from the sticky comment and any inline review comments.\n - Apply confidence gating: remediate only when you are confident you can address ALL findings; otherwise report in Stage 3 without committing.\n - Address all review findings across all affected files in a single batch. Do not fix one at a time.\n - After applying all fixes, perform a single `git commit` and `git push` so `origin/feature/<KEY>` advances to the new head and a fresh CI/review runs against it.\n - Increment `retry_count`. If `retry_count` exceeds `max_retries`: if launched under the Conductor, call `check_messages` one final time; if the supervisor sent explicit "continue" guidance with a concrete hint, apply it for exactly **one additional batch** (the only way the ceiling is raised); otherwise stop the correction loop and proceed to Stage 3 with a warning.\n - Otherwise, update `commit_sha` to the new HEAD (`git rev-parse HEAD`) and restart the polling loop against the new head.\n - **Conductor done-gate**: once the required subset is green and (if `claude-review` is required) the verdict token confirms approval for the current head, and if launched under the Conductor (the `BAPI_CONDUCTOR_RUN_ID`/`BAPI_CONDUCTOR_WORKER_ID` identifiers are present), call the `wait_for_done_gate` MCP tool **once** from inside your worktree before proceeding (no arguments are required \u2014 it self-resolves the PR number and head commit SHA from the worktree, fetches the review snapshot, evaluates the composite done-gate against the Conductor\'s authoritative `required_checks` config, and emits a `gate.met` event correlated to your run/worker so the supervisor can fold and merge). This tool does **not** merge or mutate the repo. It applies its own bounded fast-path poll (a short internal cap, not a multi-minute wait) \u2014 if it times out or does not observe `gate_met`, **exit cleanly** to Stage 3 without treating the timeout as a failure: the Conductor\'s own reconciliation pass is the correctness backstop for reaching `gate.met`, not this call. Fail-open: if the tool errors with an identity-unavailable message, you were not launched under the Conductor \u2014 skip it. Then proceed to Stage 3.\n\n3. **On required-subset failures detected**: Examine each failed **required** check\'s `detail_level` (non-required failures such as `pip-audit` are never processed here \u2014 they were already reported and skipped in item 2, and never consume a retry):\n\n - **`detail_level: "full"`** \u2014 Apply confidence gating:\n - Read the full `failure_details` payload for all failed checks.\n - If you are confident you can fix the errors, address ALL failures across all affected files in a single batch. Do not fix one at a time.\n - After applying all fixes, perform a single `git commit` and `git push`.\n - Increment `retry_count`.\n - If `retry_count` exceeds `max_retries`: if launched under the Conductor, call `check_messages` one final time. If the supervisor sent explicit "continue" guidance with a concrete hint, apply it for exactly **one additional batch** (this is the only way the ceiling is raised). Otherwise, stop the correction loop and proceed to Stage 3 with a warning.\n - Otherwise, update `commit_sha` to the new HEAD (`git rev-parse HEAD`) and restart the polling loop.\n - **`detail_level: "url_only"`** \u2014 Report the check name and URL to the user. Do not attempt fixes. Do not consume a retry.\n - **`detail_level: "none"`** \u2014 Report the check name only. Do not attempt fixes. Do not consume a retry.\n\n4. **Handle `unknown_checks`**: If the poll response contains `unknown_checks`, call `resolve_ci_checks` with `commit_ref` set to `commit_sha` and `force_rerun` set to `true` at most ONCE. If `unknown_checks` persist on the next poll, warn the user that the CI check configuration is unresolvable and skip to Stage 3.\n\n5. **Timeout**: If 10 minutes elapse without `all_complete` becoming `true`, warn that polling timed out and proceed to Stage 3.\n\nThis stage is **non-critical** \u2014 warn on failure or timeout, continue to Stage 3 regardless.\n\n## Stage 3 \u2014 Summary Report\n\nDisplay a structured completion report. The report **must** be presented as markdown, using exactly the structure below:\n\n```\n## CI Check Report\n\n**Ticket**: <ticket_key>\n**Branch**: <current_branch>\n**Commit SHA**: <commit_sha>\n**Status**: <Passed / Failed / Timed Out / Not Available>\n\n**Per-check breakdown**:\n| Check Name | Status | Required | Detail Level |\n|------------|--------|----------|--------------|\n| <name> | <pass/fail> | <yes/no> | <full/url_only/none> |\n\n**Review verdict**: <approved / changes_requested / not yet posted / N/A \u2014 claude-review not required>\n**Fixes attempted**: <retry_count> of <max_retries>\n**Supervisor guidance applied**: <yes/no>\n\n**Warnings**:\n<If any non-critical stages had warnings (Stage 1: resolution unavailable,\nStage 2: timeout, unfixable required-check failures, unknown_checks,\nnon-required check failures reported for visibility),\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 3 confirming the CI check status, including per-check breakdown, fixes attempted, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0), display which stage failed and the error details.\n',
14821
15460
  "clarify-ticket.md": 'Generate clarifying questions for a Jira ticket and save them locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n`$ARGUMENTS` is a required Jira ticket key (e.g., `PROJ-123`). This command generates clarifying questions for the ticket and saves them locally.\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 ticket key format**: Confirm the ticket key matches the Jira key pattern `[A-Z][A-Z0-9]+-\\d+`. If it does not match, stop immediately and report: "Invalid ticket key format. Expected a Jira key like PROJ-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 Clarifying Questions\n\nCall the `request_clarifying_questions` 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: "Clarifying questions generation failed." Include the error details.\n\n## Final Report\n\nOn successful completion, display:\n\n> **Ticket Key**: {ticket_key}\n>\n> **Local File Path**: {docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md\n>\n> **Status**: The clarifying questions document has been saved locally. No changes were pushed to Jira.\n>\n> To incorporate these findings into the Jira ticket description, run: `/update-ticket {ticket_key}`\n',
14822
- "code-ticket.md": "# Code Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), downloads the AI-generated implementation plan and clarifying questions via MCP tools, then executes the plan step by step directly in the main conversation so all progress is visible.\n\nIf any critical stage fails (Stage 0, Stage 1, or Stage 3), stop immediately and report which stage failed and why. Non-critical stages (Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to implement a Jira ticket using an AI-generated plan. 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: /code-ticket <ticket_key> (e.g., /code-ticket 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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Download Implementation Plan\n\nCall the `get_plan` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `true`\n\nInspect the response for errors. If the response text contains `NOT_FOUND` or `404` or indicates the plan was not found, stop immediately and display:\n\n```\nNo implementation plan found for <ticket_key>. Run `/plan-ticket <ticket_key>` first to generate one,\nor use the `request_plan_generation` MCP tool with `wait_for_result: true`.\n```\n\nOn success, read and internalize the full plan content. This is the plan you will execute in Stage 3.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Download Clarifying Questions\n\nCall the `get_clarifying_questions` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `false`\n\nIf the response contains `NOT_FOUND` or `404` or indicates no clarifying questions were found, log a warning note:\n\n```\nWarning: No clarifying questions found for <ticket_key>. Proceeding without supplementary context.\n```\n\nDo **NOT** stop the pipeline. Clarifying questions are supplementary context, not a hard prerequisite for implementation.\n\nOn success, internalize the clarifying questions content. Reference these for additional context where relevant to implementation steps \u2014 the answers provide supplementary guidance on requirements and technical decisions.\n\nThis stage is **non-critical** \u2014 warn on failure, continue to Stage 3 regardless.\n\n## Stage 3 \u2014 Execute Implementation Plan\n\nExecute the implementation plan step by step, directly in this conversation. Work inline so the user can see all progress and approve tool calls.\n\nFollow these rules:\n\n1. **Execute the plan in order.** Do not skip any steps, especially review steps involving test execution, lint checks, and architectural verification.\n2. **Make code changes** as directed by each step in the plan.\n3. **Run tests and checks** as specified in the plan's review steps.\n4. **Do NOT run `git commit` or `git push`.** Leave all changes uncommitted for developer review.\n5. **If a step is ambiguous or blocked**, note the issue clearly and continue with the next step rather than halting entirely.\n6. **Reference clarifying questions** (if retrieved in Stage 2) when they provide relevant context for a given step.\n\nThis stage is **critical** \u2014 if a blocking error prevents further progress, stop and report the failure.\n\n## Stage 4 \u2014 Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Implementation Complete\n\n**Ticket**: <ticket_key>\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 non-critical stages had warnings (Stage 2: no clarifying questions),\nlist them here so the developer has full visibility. If no warnings, omit this section.>\n```\n\n## Final Report\n\nOn success, display the structured report from Stage 4 confirming that implementation of the ticket is complete.\n\nOn failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.\n",
15461
+ "code-ticket.md": "# Code Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), downloads the AI-generated implementation plan and clarifying questions via MCP tools, then executes the plan step by step directly in the main conversation so all progress is visible.\n\nIf any critical stage fails (Stage 0, Stage 1, or Stage 3), stop immediately and report which stage failed and why. Non-critical stages (Stage 2) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to implement a Jira ticket using an AI-generated plan. 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: /code-ticket <ticket_key> (e.g., /code-ticket 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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Download Implementation Plan\n\nCall the `get_plan` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `true`\n\nInspect the response for errors. If the response text contains `NOT_FOUND` or `404` or indicates the plan was not found, stop immediately and display:\n\n```\nNo implementation plan found for <ticket_key>. Run `/plan-ticket <ticket_key>` first to generate one,\nor use the `request_plan_generation` MCP tool with `wait_for_result: true`.\n```\n\nOn success, read and internalize the full plan content. This is the plan you will execute in Stage 3.\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Download Clarifying Questions\n\nCall the `get_clarifying_questions` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `save_locally`: `false`\n\nIf the response contains `NOT_FOUND` or `404` or indicates no clarifying questions were found, log a warning note:\n\n```\nWarning: No clarifying questions found for <ticket_key>. Proceeding without supplementary context.\n```\n\nDo **NOT** stop the pipeline. Clarifying questions are supplementary context, not a hard prerequisite for implementation.\n\nOn success, internalize the clarifying questions content. Reference these for additional context where relevant to implementation steps \u2014 the answers provide supplementary guidance on requirements and technical decisions.\n\nThis stage is **non-critical** \u2014 warn on failure, continue to Stage 3 regardless.\n\n## Stage 3 \u2014 Execute Implementation Plan\n\nExecute the implementation plan step by step, directly in this conversation. Work inline so the user can see all progress and approve tool calls.\n\nFollow these rules:\n\n1. **Execute the plan in order.** Do not skip any steps, especially review steps involving test execution, lint checks, and architectural verification.\n2. **Make code changes** as directed by each step in the plan.\n3. **Run tests and checks** as specified in the plan's review steps.\n4. **Do NOT run `git commit` or `git push`.** Leave all changes uncommitted for developer review.\n5. **If a step is ambiguous or blocked**, note the issue clearly and continue with the next step rather than halting entirely.\n6. **Reference clarifying questions** (if retrieved in Stage 2) when they provide relevant context for a given step.\n7. **If a specific plan step or requirement remains ambiguous** after consulting the plan and any retrieved clarifying questions, call the `get_ticket` MCP tool with `ticket_number` set to the parsed `ticket_key` from Stage 0 to fetch the live Jira ticket details. Use only the fields relevant to resolving that ambiguity, then continue with the affected step. Do not call `get_ticket` unconditionally or as a prerequisite \u2014 only when a step's requirement is genuinely unclear.\n\nThis stage is **critical** \u2014 if a blocking error prevents further progress, stop and report the failure.\n\n## Stage 4 \u2014 Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Implementation Complete\n\n**Ticket**: <ticket_key>\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 non-critical stages had warnings (Stage 2: no clarifying questions),\nlist them here so the developer has full visibility. If no warnings, omit this section.>\n```\n\n## Final Report\n\nOn success, display the structured report from Stage 4 confirming that implementation of the ticket is complete.\n\nOn failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.\n",
14823
15462
  "commit-ticket.md": '# Commit Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes a Jira ticket key (e.g., `BAPI-150`), verifies the current git branch matches the ticket, identifies implementation files by cross-referencing git changes against the saved implementation plan, and commits and pushes the work. It is designed to run after `/code-ticket` completes.\n\nIf any critical stage fails (Stage 0, Stage 1, or Stage 3), stop immediately and report which stage failed and why. Non-critical stages (Stage 2, Stage 4, Stage 5, and Stage 6) should log a warning but not stop the pipeline.\n\n---\n\n# Instructions\n\nYou are executing a 7-stage pipeline to commit and push implementation work 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: /commit-ticket <ticket_key> (e.g., /commit-ticket BAPI-150)\n ```\n\n2. **Resolve docs directory**: Call the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n3. **Verify uncommitted changes exist**: Run `git status --porcelain` in the terminal. If the output is empty (no modified, added, or untracked files), stop immediately and display:\n\n ```\n No uncommitted changes found. Nothing to commit for <ticket_key>.\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Branch Verification and Creation\n\n1. **Get current branch**: Run `git branch --show-current` in the terminal. Store the result as `current_branch`.\n\n2. **Check branch match**: Determine if `current_branch` contains the `ticket_key` (case-insensitive comparison). For example, if the ticket key is `BAPI-150`, branch `feature/BAPI-150-add-caching` matches, as does `feature/BAPI-150` or `bugfix/bapi-150-fix`.\n\n3. **If the branch matches**: Log a confirmation message and proceed:\n\n ```\n Branch \'<current_branch>\' matches ticket <ticket_key>. Proceeding.\n ```\n\n4. **If the branch does NOT match**: Create a new branch from the current HEAD in the format `feature/<ticket_key>` (e.g., `feature/BAPI-150`). Run `git checkout -b feature/<ticket_key>` in the terminal. If the branch creation fails (e.g., branch already exists), try `git checkout feature/<ticket_key>` instead. If both fail, stop immediately and display:\n\n ```\n Failed to create or switch to branch \'feature/<ticket_key>\'.\n Please resolve the branch situation manually and re-run.\n ```\n\n On success, log:\n\n ```\n Created and switched to new branch \'feature/<ticket_key>\'.\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 \u2014 Identify and Stage Implementation Files\n\n1. **Collect git changes**: Run `git status --porcelain` in the terminal. Parse the output to build two lists:\n - `modified_files`: files with status `M`, `MM`, `AM`, or `A` (modified or staged)\n - `untracked_files`: files with status `??` (new untracked files)\n\n Combine into a single list `all_changed_files`.\n\n2. **Load the implementation plan**: Look for the implementation plan file at `{docs_dir}/plans/{ticket_key}-plan.md`. Read the file.\n\n - **If the plan file exists**: Extract file paths mentioned in the plan. Look for patterns like backtick-quoted paths (e.g., `src/python/foo.py`), file references in step descriptions, and any explicit file listings. Build a list `plan_files` of all file paths referenced in the plan.\n\n - **If the plan file does NOT exist**: Log a warning:\n\n ```\n Warning: No implementation plan found at {docs_dir}/plans/{ticket_key}-plan.md.\n Cannot cross-reference changes against plan. Will present all changed files for review.\n ```\n\n Set `plan_files` to an empty list.\n\n3. **Classify changed files**: For each file in `all_changed_files`, classify it into one of three categories:\n\n - **Plan-matched**: The file path appears in `plan_files` (exact match or the plan references a parent directory). These are high-confidence implementation files.\n - **Likely related**: The file is not explicitly in the plan but is a test file for a plan-matched file, a migration file, an `__init__.py` in a directory with plan-matched files, or otherwise clearly related to the implementation (e.g., `requirements.txt` if the plan mentions adding a dependency).\n - **Ambiguous**: The file does not appear related to the plan. These may be pre-existing uncommitted changes.\n\n4. **Present file list for user confirmation**: Display the classified file list to the user:\n\n ```\n ## Files to Commit for <ticket_key>\n\n ### Plan-matched files (high confidence):\n - path/to/file1.py\n - path/to/file2.py\n\n ### Likely related files:\n - tests/pytest/routes/test_file1.py\n - db/alembic/versions/xxxx_migration.py\n\n ### Ambiguous files (not referenced in plan):\n - some/other/file.py\n\n Shall I proceed with committing all listed files?\n If you want to exclude any files, please specify which ones to remove.\n ```\n\n If `plan_files` is empty (plan not found), display all files under a single "All changed files" heading instead.\n\n5. **Wait for user confirmation**: The user may:\n - Approve all files (proceed)\n - Specify files to exclude (remove those from the commit list)\n - Cancel entirely (stop the pipeline)\n\n If the user cancels, stop immediately and display:\n\n ```\n Commit cancelled by user. No files were staged or committed.\n ```\n\n6. **Stage the approved files**: Run `git add <file1> <file2> ...` in the terminal, listing only the approved files explicitly. Do NOT use `git add -A` or `git add .`.\n\nThis stage is **non-critical** if the plan file is not found (warn and continue with all files). It is **critical** if the user cancels or if `git add` fails \u2014 stop immediately on those failures.\n\n## Stage 3 \u2014 Commit and Push\n\n1. **Generate commit message**: Based on the staged files and the implementation plan (if available), generate a concise commit message. The message must:\n - Start with a brief summary line (under 72 characters) that references the ticket key\n - Format: `<ticket_key>: <brief description of changes>`\n - Example: `BAPI-150: Add rate limiting to LLM client`\n - If the plan was available, derive the description from the plan\'s title or objective\n - If the plan was not available, summarize based on the file names and `git diff --staged` output\n\n2. **Commit**: Run `git commit -m "<message>"` in the terminal. If the commit fails due to a pre-commit hook, report the hook output and stop:\n\n ```\n Commit failed due to pre-commit hook. Hook output:\n <hook output>\n\n Please fix the issues and re-run /commit-ticket <ticket_key>.\n ```\n\n3. **Push to remote**: Run `git push -u origin <current_branch>` in the terminal. The `-u` flag sets up upstream tracking. If the push fails, stop immediately and display:\n\n ```\n Push failed. Error:\n <error output>\n\n The commit was created locally. You can push manually with:\n git push -u origin <current_branch>\n ```\n\nThis stage is **critical** \u2014 stop immediately on failure.\n\n## Stage 4 \u2014 Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Commit Complete\n\n**Ticket**: <ticket_key>\n**Branch**: <current_branch>\n**Commit**: <commit_hash> (from `git rev-parse --short HEAD`)\n**Files committed**: <count> files\n**Remote**: Pushed to origin/<current_branch>\n\n**Committed files**:\n- path/to/file1.py\n- path/to/file2.py\n- ...\n\n**Warnings**:\n<If any non-critical warnings occurred (Stage 2: plan not found),\nlist them here. If no warnings, omit this section.>\n```\n\nThis stage is **non-critical** \u2014 display the report regardless.\n\n## Stage 5 \u2014 Jira Status Transition\n\nThis stage attempts to transition the Jira ticket to the appropriate post-PR status.\n\n1. **Resolve target status**: Call the `resolve_target_status` MCP tool with `ticket_number` set to the `ticket_key`. This returns the cached or LLM-resolved target status for the project.\n\n2. **Attempt transition**: If `resolve_target_status` returned a non-null `target_status`, call the `update_jira_status` MCP tool with `ticket_number` set to the `ticket_key` and `target_status` set to the resolved value. If the ticket is already in the target status, this is a no-op.\n\n3. **On success**: Display `"Ticket status updated: <from_status> -> <to_status>"`.\n\n4. **On failure or not applicable**: Display a warning but do not stop the pipeline:\n - If `resolve_target_status` returned null: `"Ticket status transition skipped: no target status configured for this project"`\n - If `update_jira_status` failed: `"Ticket status transition skipped: <error message>"`\n\nThis stage is **non-critical** \u2014 log a warning on failure but do not stop the pipeline.\n\n## Stage 6 \u2014 Smoke Test Validation Comment\n\nThis stage reviews the implementation against the ticket requirements and posts a comment if manual validation is needed.\n\n1. **Fetch ticket description**: Call the `get_ticket` MCP tool with `ticket_number` set to the `ticket_key` to retrieve the current ticket requirements.\n\n2. **Review implementation**: Compare the implementation (from the plan loaded in Stage 2 and the files committed in Stage 3) against the ticket requirements. Identify any behavior or requirements that could NOT be validated through the automated tests written during implementation or through code review alone. Consider the limitations of any tests that were written: what functionality or behavior could not be validated by those tests? Examples include: requirements involving visual UI rendering or layout checks, third-party system integrations where mock tests are insufficient, or non-deterministic behaviors.\n\n3. **If untestable requirements exist**: Compose a structured comment describing specific manual validation steps stakeholders should perform. Then call the `add_comment` MCP tool with `ticket_number` set to the `ticket_key` and the comment text. Display: `"Smoke test validation comment posted to <ticket_key>"`.\n\n4. **If no untestable requirements exist**: Skip silently. Display: `"No untestable requirements identified \u2014 skipping smoke test comment"`.\n\nThis stage is **non-critical** \u2014 log a warning on failure but do not stop the pipeline.\n\n## Final Report\n\nOn success, display the structured report from Stage 4 confirming that the commit and push are complete, including the branch name, commit hash, file list, and any warnings from earlier stages.\n\nOn failure at any critical stage (Stage 0, Stage 1, or Stage 3), display which stage failed and the error details.\n',
14824
15463
  "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',
14825
15464
  "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',
@@ -14836,12 +15475,14 @@ var COMMANDS = {
14836
15475
  "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',
14837
15476
  "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',
14838
15477
  "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",
15478
+ "review-and-implement.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":"baseBranch","flag":"--base-branch","type":"string"}]}\n---\n\n# Review and Implement: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command runs inside a single already-created worktree (typically spawned by `/start-tickets --workflow review-and-implement`, or its front door `/review-and-start`): it reviews the ticket via `/review-ticket`, pauses at a per-ticket human proceed/halt gate, and only then implements it via `/implement-ticket`. It creates no worktrees, refreshes no base branch, and does not monitor sibling sessions \u2014 those responsibilities stay in the launcher (`start-tickets`) and the parent front-door command.\n\n---\n\n# Instructions\n\n## Argument Parsing\n\nParse `$ARGUMENTS`:\n\n1. **Ticket key**: exactly one required token matching the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`). If zero tokens match, or more than one token matches, stop immediately and display:\n ```\n Invalid ticket key. Expected exactly one key, e.g. PROJ-123.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n2. **`--auto`**: an optional position-independent flag. When present, sets chain-level `auto_approve` to `true`. This single flag applies to BOTH the review phase and the implementation phase below \u2014 there is no separate review-auto or implementation-auto state.\n3. **`--rounds`**: an optional position-independent `--rounds <n>` / `--rounds=<n>` argument, `1` or `2`. Normalize either form to `--rounds=<n>`. Reject any other value:\n ```\n Invalid --rounds value. Expected: --rounds=1 or --rounds=2.\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n When omitted, forward no `--rounds` to `/review-ticket` (the backend\'s difficulty-adaptive review policy decides the shape).\n4. **`--base-branch`**: an optional position-independent `--base-branch <branch>` / `--base-branch=<branch>` argument. Validate it using the same rules `/start-tickets` Stage 0 uses: after trimming, non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`). Reject a malformed value:\n ```\n Invalid --base-branch value: <reason>\n Usage: /review-and-implement <ticket_key> [--auto] [--rounds=1|2] [--base-branch=BRANCH]\n ```\n\n## Phase 1 \u2014 Review (inline)\n\nInvoke `/review-ticket <ticket_key>` **inline, in this same session**, forwarding:\n- `--auto` when chain-level `auto_approve` is `true`.\n- The normalized `--rounds=<n>` when `--rounds` was supplied.\n- `--base-branch=<branch>` when it was supplied.\n\n`/review-ticket` grounds its codebase evaluation against a **fresh `git archive` of `origin/<base>`**, materialized into an isolated temp directory via `materialize_fresh_base` (see `docs/BAPI-474-ground-review-against-fresh-base.md`) \u2014 NOT against this worktree\'s own working tree. Review therefore behaves identically whether run standalone or, as here, inside a worktree that `start-tickets` already created; the worktree\'s isolation exists for the implementation phase below, not for review\'s codebase grounding.\n\nIf the review pipeline itself fails (any step reports `Status: Failed at step N`), halt this ticket immediately \u2014 in **both** auto and non-auto mode \u2014 before Phase 2 or Phase 3 run. Report:\n```\nReview failed for <ticket_key> at step N. Implementation was not started.\n```\nDo not attempt any cleanup; the worktree/branch remains available for manual inspection or cleanup.\n\n## Phase 2 \u2014 Halt Gate\n\n`/review-ticket` produces **decisions that rewrite the ticket** \u2014 it does NOT emit a binary approve/decline verdict token. Do not parse, infer, or synthesize an approve/decline signal from its output; no such token exists to parse.\n\n- **When chain-level `auto_approve` is `true`** and Phase 1 completed successfully: skip this gate entirely and proceed straight to Phase 3.\n- **Otherwise** (non-auto, and Phase 1 completed successfully): after the ticket has been rewritten, ask the user exactly:\n ```\n Proceed to implementation for <ticket_key>? (y/N)\n ```\n Treat an empty response, any negative response (`n`, `no`, or similar), or any ambiguous/unrecognized response as **halt** \u2014 do not guess intent. On halt, report:\n ```\n Halted before implementation for <ticket_key> (declined). The worktree/branch remains available for manual cleanup.\n ```\n and stop. Do not perform Phase 3 or any cleanup.\n- On an explicit affirmative response (`y` or `yes`), proceed to Phase 3.\n\n## Phase 3 \u2014 Implement (inline)\n\nInvoke `/implement-ticket <ticket_key>` **inline, in this same session**, appending `--auto` when chain-level `auto_approve` is `true` (and omitting it on the manual-confirmation path). This is the same single chain-level `--auto` from Phase 1 \u2014 there is no separate implementation-only auto flag.\n\n## Scope\n\nThis command composes `/review-ticket` and `/implement-ticket`; it owns none of their internals and must not:\n- create, re-cut, or switch a Worktrunk worktree,\n- refresh or fetch the launcher\'s base branch,\n- monitor or coordinate with sibling `/review-and-implement` sessions spawned for other tickets,\n- orchestrate the parent `start-tickets` / `/review-and-start` session.\n\nA halt (review failure or a declined gate) affects **this ticket only** \u2014 sibling worktrees spawned for other keys are unaffected. Review and implementation share the single difficulty-derived model tier selected when this session was spawned by `start-tickets`; this command does not re-resolve or override model routing.\n',
15479
+ "review-and-start.md": '---\nschedulable: 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":"agent","flag":"--agent","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"}]}\n---\n\n# Review and Start: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248` or `BAPI-248 BAPI-250` \u2014 both flow through the identical ordered `keys: string[]` path) and spawns one refreshed, self-contained Worktrunk worktree per ticket, each running `/review-and-implement <KEY>` \u2014 review, then a per-ticket human proceed/halt gate, then implementation. It is the **recommended front door** for chaining "review these tickets, then implement the ones that pass" starting from *existing ticket keys* (the entry point `/full-automation` lacks, since its server orchestrator only accepts an idea).\n\nIt is a thin shim over the packaged `start-tickets` CLI, invoked with `--workflow review-and-implement`: this command performs the same connectivity check and branch enrichment as `/start-tickets`, then hands off to the identical worktree/base-refresh/model-routing/credential/cross-platform-spawn/`doctor` engine \u2014 reused verbatim. The review\u2192implement halt-gate decision logic lives entirely in the spawned `/review-and-implement` session, never here or in the CLI. Using `start-tickets --workflow review-and-implement` directly (documented in `commands/src/start-tickets.md`) remains available as the lower-level launcher seam this command drives.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that resolves connectivity and per-ticket branch names, then spawns N parallel Worktrunk worktrees via the packaged CLI\'s `--workflow review-and-implement` seam. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys and pass-through flags:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). A single key and multiple keys flow through the identical ordered `keys: string[]` path \u2014 there is no separate single-ticket code path. If zero keys are found, stop immediately and display:\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /review-and-start [flags] <KEY> [KEY ...] (e.g., /review-and-start BAPI-248 BAPI-250)\n ```\n - **`--auto`**: a single chain-level flag. When present, it is forwarded to `start-tickets --auto`, which threads it into BOTH the review and implementation phases of every spawned `/review-and-implement <KEY>` session.\n - **`--rounds`**: normalize `--rounds 1`/`--rounds=1` and `--rounds 2`/`--rounds=2` to `--rounds=<n>`; reject any other value. This value is forwarded to the review phase of every spawned session.\n - **Selected agent**: validate `--agent <name>` / `--agent=<name>` against `claude`/`cursor-agent` using the same rule as `/start-tickets`; default `claude`.\n - **User-supplied base branch**: validate `--base-branch <branch>` / `--base-branch=<branch>` using the same non-empty/\u2264255-character/no-leading-dash/no-control-character rules as `/start-tickets` Stage 0. A user-supplied value takes precedence over Stage 2a\'s `config_field` resolution below.\n - **`--max-parallel N`**: a positive integer; the CLI\'s own default (3) applies when omitted.\n - **`--dry-run`**: a boolean toggle.\n - Reject malformed input before proceeding: an unsupported flag, a ticket key not matching `[A-Z]+-[0-9]+`, an unsupported `--agent`/`--rounds`/`--base-branch` value \u2014 stop and report the malformed argument.\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 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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged `start-tickets` CLI (driven with `--workflow review-and-implement` in Stage 3) owns all platform pre-flight checks, refreshed worktree creation, secret-free credential provisioning, difficulty\u2192model-tier routing, and cross-platform terminal spawning \u2014 identical to `/start-tickets`. This command does not duplicate or re-verify any of that; see `commands/src/start-tickets.md` Stage 1 for the full prerequisite/provisioning description it drives. This command does not expose `--conductor`, `--terminal`, or `--no-refresh-main`/`--no-refresh-base` \u2014 it stays scoped to the front-door flags listed above. Proceed to Stage 2.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve Base Branch\n\nIdentical to `/start-tickets` Stage 2a: if the user supplied `--base-branch`, use it and skip the `config_field` lookup entirely. Otherwise call `config_field` (`operation: "get"`, `field_name: "base_branch"`) and treat a non-empty string `value` as the configured base branch; treat `null`, an empty/whitespace-only string, an HTTP `400` response, or any other lookup failure as "unset" and omit `--base-branch` from Stage 3 (the CLI then defaults to `main`). When forwarding a resolved value into the Stage 3 Bash invocation, apply the same mandatory single-quote escaping rule as `/start-tickets` (`\'` \u2192 `\'\\\'\'`, then wrap the whole value in single quotes) before interpolating it into the command string \u2014 never expand it unquoted.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nIdentical to `/start-tickets` Stage 2b: for each ticket key without a user-provided `--branch` override, call `get_ticket` (`ticket_number` set to the key, `save_locally: false`), slugify its `summary` (lowercase, collapse runs of `[^a-z0-9]+` to a single `-`, trim leading/trailing dashes, truncate to at most 40 characters preferring a dash boundary), and build `feature/<KEY>-<slug>`. On any per-key failure (404, network error, empty slug), emit a warning and let the CLI apply its default `feature/<KEY>` for that key only; never stop the pipeline. Credentials never reach the Bash-spawned CLI \u2014 this enrichment stays in the command, exactly as in `/start-tickets`.\n\nThis stage is **non-critical** \u2014 warnings are acceptable; the pipeline continues with the fallback branch for any key that fails enrichment.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke exactly **one** CLI invocation covering every requested key \u2014 never a per-key loop:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` forwards `--auto` (only if supplied), the normalized `--rounds=<n>` (only if supplied), `--agent <name>` (only if supplied), `--max-parallel N` (only if supplied), and `--dry-run` (only if supplied) verbatim.\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` only when a value was resolved in Stage 2a; otherwise omitted entirely.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2b.\n- `<ticket-keys>` is the original ordered list of keys parsed in Stage 0.\n\nCredentials are never placed in this shell command \u2014 the same secret-free provisioning path `/start-tickets` uses applies here unchanged.\n\nExample, single key, hands-off:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=1 --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step BAPI-248\n```\n\nExample, multiple keys, manual per-ticket gate (no `--auto`):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step --branch BAPI-250=feature/BAPI-250-deep-research-durability BAPI-248 BAPI-250\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket: `KEY branch=BRANCH status=STATUS`, optional trailing `path=PATH`) and reformat it as a markdown table, identical in shape to `/start-tickets` Stage 4 (`Ticket | Branch | Status`; statuses `dry-run`, `spawned`, `create-failed`, `spawn-failed`).\n\nThis report describes **worktree/spawn status only**. State explicitly in the report:\n- Each successfully spawned session independently continues through `/review-ticket`, its own per-ticket halt gate, and `/implement-ticket` \u2014 this parent session does not observe or report that later outcome. Never claim or imply that review or implementation has completed.\n- When `--dry-run` was passed, state explicitly that no worktrees were created and no tabs/sessions were opened.\n- As a fixed trade-off of this design: the difficulty-derived model tier selected at spawn time serves the **entire** chained review-and-implement session (both phases share one model), and a non-auto (including a non-auto scheduled) run pauses at each ticket\'s own gate rather than proceeding hands-off.\n\nIf the CLI reported any `create-failed`/`spawn-failed` statuses, or Stage 2b emitted enrichment warnings, list them under a `Warnings:` heading. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the underlying runbook this command builds on, and `commands/src/review-and-implement.md` for the per-ticket review\u2192gate\u2192implement composition each spawned session runs.\n',
14839
15480
  "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
15481
  "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',
14841
15482
  "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',
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',
15483
+ "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/runbooks/self-install-smoke-test.md`, `docs/claude/runbooks/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',
14843
15484
  "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',
14844
- "start-tickets.md": '---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"agent","flag":"--agent","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"}]}\n---\n\n# Start Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248 BAPI-250`) and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `start-tickets`, which creates a Worktrunk worktree for each key and opens one tab/session per worktree running the **selected agent** \u2014 Claude Code (`claude`) by default, or Cursor Agent (`cursor-agent`) via `--agent` \u2014 in a macOS Terminal/iTerm tab, a Windows Terminal tab (or PowerShell fallback window), or a detached Linux tmux session, chosen automatically by platform. It replaces Parts 2\u20135 of `docs/claude/parallel-worktrees.md` with a single command.\n\nBecause the orchestration ships inside the `@bridge_gpt/mcp-server` npm package (not a repo-local script), this command works for every consumer \u2014 including projects that installed the package via `--init`.\n\nStage 0 and Stage 1 are critical (stop on failure). Stage 2 is non-critical (per-ticket enrichment failures fall back to the default branch and continue). Stage 3 is critical (propagate the packaged CLI\'s exit code).\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that spawns N parallel Worktrunk worktrees and selected-agent sessions (Claude Code by default) via the packaged CLI. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys, pass-through flags, and branch overrides:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). If zero keys are found, stop immediately and display:\n\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /start-tickets [flags] <KEY> [KEY ...] (e.g., /start-tickets BAPI-248 BAPI-250)\n ```\n\n - **Pass-through flags**: collect any of `--agent <name>` (and the equals form `--agent=<name>`), `--terminal terminal|iterm`, `--dry-run`, `--auto`, `--no-refresh-main`, `--base-branch <branch>` (and the equals form `--base-branch=<branch>`), and `--max-parallel N` that the user supplied. These are forwarded verbatim to the CLI in Stage 3. `--auto` makes each spawned agent run `/implement-ticket <KEY> --auto` (hands-off implementation); omit it to keep the implementation agents interactive.\n - **Selected agent**: track a `selected_agent` variable that defaults to `claude`. If the user passed `--agent <name>` / `--agent=<name>`, validate the value against the supported agents `claude` and `cursor-agent`, set `selected_agent` to it, and reject any other (malformed/unsupported) `--agent` value before proceeding. The agent is not auto-detected from the host editor \u2014 the user selects it explicitly (default `claude`).\n - **User-supplied base branch**: track a `user_supplied_base_branch` boolean that defaults to `false`. If the user passed `--base-branch <branch>` or `--base-branch=<branch>`, set the boolean to `true` and capture the value. A user-supplied `--base-branch` value **takes precedence** over any value resolved from Bridge API config in Stage 2. Validate the user-supplied value before proceeding: after trimming surrounding whitespace it must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`); reject any malformed value with a clear error.\n - **User branch overrides**: collect any user-supplied repeatable `--branch KEY=BRANCH` flags. A user-provided override always takes precedence over Stage 2 enrichment for that key.\n - Reject malformed input before proceeding: if a token looks like a flag but is not one of the supported flags, or a ticket key does not match `[A-Z]+-[0-9]+`, or a `--branch` value is not `KEY=BRANCH`, or `--agent` names an agent other than `claude`/`cursor-agent`, or `--base-branch` fails the validation rules above, stop and report the malformed argument.\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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged CLI runs its own per-platform pre-flight checks and then fetches `origin` and fast-forwards the local **configured base branch** (the value resolved in Stage 2 below, or `main` when none is configured) from `origin/<base>` so the new worktrees are based on an up-to-date base. The historical flag `--no-refresh-main` still controls this behavior \u2014 the flag name is preserved for backward compatibility, but it now skips refresh of whatever base branch resolves (default `main`). The required commands depend on the OS:\n\n- **macOS**: `wt`, `git`, `osascript`.\n- **Windows**: `git-wt`, `git`, Git for Windows / Git Bash (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash), and Windows Terminal **or** PowerShell.\n- **Linux**: `wt`, `git`, `tmux`.\n\nOn **Windows** the Worktrunk binary is `git-wt` (its winget alias), which is a different tool from Windows Terminal\'s `wt.exe`: the CLI uses `git-wt` to **create worktrees** and `wt.exe` to **open a tab**, and never conflates the two. On **Linux** the CLI opens one detached `tmux` session per ticket (a window is added if that ticket\'s session already exists); attach later with `tmux attach -t <session>`. An unsupported OS (not macOS/Windows/Linux) fails fast with a clear "unsupported platform" message.\n\nThis stage simply notes that the CLI will fail fast if any prerequisite is missing or if local `main` has diverged from `origin/main` \u2014 you do not need to verify anything separately here, and you must not run any pre-flight commands yourself. When the CLI\'s pre-flight fails it now hints the user to run the read-only diagnostics command `npx -y @bridge_gpt/mcp-server doctor`, which reports found/missing for every prerequisite on the current OS \u2014 the pre-flight set plus `uv` plus the selected agent\'s command \u2014 and prints the manual install command for each missing one. `doctor` is strictly read-only and never installs anything; never run install commands automatically on the user\'s behalf. The CLI does not call any Bridge API tools; all credential-bearing work (branch enrichment in Stage 2) stays in this command. Proceed to Stage 2.\n\nThe packaged CLI also performs **secret-free Bridge API MCP provisioning** inside each created worktree: synchronously after the worktree is created and **before the agent tab/session is opened**, it writes both `.mcp.json` (Claude Code) and `.cursor/mcp.json` (Cursor) pointing at the `mcp-invoke` shim. These registrations are **secret-free** \u2014 they contain no `env` block and no API key, because the shim resolves credentials at runtime. If a spawned agent (or difficulty\u2192model routing) reports missing Bridge API credentials, fix it by rerunning `/install-bridge` (its final stage persists the routing credential), by running `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate a key that lives only in `.mcp.json` / `.cursor/mcp.json`, or by adding a `bapi:<repo>` entry to the user-scoped credentials file (`~/.config/bridge/credentials.json`) \u2014 never by putting `BAPI_API_KEY` into the worktree `.mcp.json` or `.cursor/mcp.json` (that env is invisible to the Bash-spawned CLI).\n\nThis stage is **critical** in the sense that the CLI will abort if its pre-flight fails; you will see the error in Stage 3\'s output and must surface it.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve configured `base_branch`\n\nThe CLI must be told which branch to cut new worktrees from. Resolution order:\n\n1. If `user_supplied_base_branch` from Stage 0 is `true`, **skip the config-field lookup entirely** and use the user-supplied value. The user\'s explicit `--base-branch` always wins; never call `config_field` for `base_branch` in that case.\n2. Otherwise, call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch` (do not pass any other parameters; the tool resolves the repository from the MCP server\'s configured `BAPI_REPO_NAME`).\n3. Parse the response. Treat the result as the **configured base branch** only when the response is a JSON object whose `value` field is a non-empty string after trimming surrounding whitespace.\n4. Treat **all** of the following as "unset" \u2014 emit a single-line warning like `Warning: base_branch is unset; CLI will default to main` and **omit** the `--base-branch` flag entirely from the Stage 3 command (the CLI\'s own default is `main`):\n - `value` is `null`.\n - `value` is an empty string or a whitespace-only string.\n - The endpoint returns HTTP `400` (invalid field \u2014 happens before the registry includes `base_branch`).\n - The tool returns a network error, timeout, or non-JSON parse failure.\n - Any other lookup failure.\n5. When the configured value is usable, capture it in a `resolved_base_branch` variable. **Do not** stop the pipeline on a lookup failure; fall through to the CLI default.\n\nWhen forwarding `resolved_base_branch` into the Bash invocation in Stage 3, **shell-escape it safely**: replace every literal single quote `\'` in the value with the four-character sequence `\'\\\'\'`, then wrap the entire resulting string in single quotes (so the final argument looks like `\'<escaped-value>\'`). This is the standard POSIX single-quote escaping rule and is **mandatory** because `base_branch` is admin-configurable data that gets interpolated into a Bash command string; any unescaped single quote would otherwise break out of the surrounding quotes. Pass `--base-branch \'<escaped-value>\'` to the CLI as a single argv element \u2014 never expand the value unquoted into the command line.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nBranch enrichment happens here, in the command, **before** invoking the CLI \u2014 the `get_ticket` MCP tool runs inside the MCP server process, which holds the Bridge API credentials the shell-spawned CLI does not have. For each parsed ticket key that does **not** already have a user-provided `--branch` override:\n\n1. Call the `get_ticket` MCP tool with `ticket_number` set to the key and `save_locally` set to `false`.\n2. From the response, extract the `summary` field. Slugify it: lowercase the string, replace every run of non-alphanumeric characters (`[^a-z0-9]+`) with a single dash `-`, trim leading and trailing dashes, and truncate to at most `40` characters (cutting at a dash boundary if possible).\n3. The enriched branch name is `feature/<KEY>-<slug>`. Example: `BAPI-248` with summary `"Add PR rating pre-evaluation step"` becomes `feature/BAPI-248-add-pr-rating-pre-evaluation-step` (trimmed at 40 chars).\n4. If the `get_ticket` call fails for a particular key (404, network error, missing summary) or produces an empty slug, emit a single-line warning like `Warning: could not enrich BAPI-248, falling back to feature/BAPI-248` and let the CLI apply its default `feature/<KEY>` for that key only. Do NOT stop the pipeline.\n5. Build a list of `--branch <KEY>=<BRANCH>` arguments \u2014 one entry per key whose enrichment succeeded \u2014 and merge it with any user-provided overrides from Stage 0. **Do not** call `get_ticket` for keys that already have a user-provided override; those overrides win.\n\nThis stage is **non-critical** \u2014 warnings are acceptable, the pipeline continues with the fallback default for any key that fails. Do not call the Bridge API from the CLI itself; the CLI never has credentials.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke the packaged CLI. Build the command line as:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` are the supported flags collected in Stage 0 (`--agent`, `--terminal`, `--dry-run`, `--auto`, `--no-refresh-main`, `--max-parallel`), forwarded verbatim. Forward `--agent <name>` only if the user supplied it; otherwise omit it and the CLI defaults to `claude`. Forward `--auto` only if the user supplied it.\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` (single-quoted using the Stage 2a escaping rule) **only when** the user supplied `--base-branch` in Stage 0 **or** Stage 2a\'s `config_field` lookup returned a non-empty configured value. When the configured value is unset / lookup fails / user did not supply one, **omit this flag entirely** so the CLI\'s own default (`main`) takes effect.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2 (enrichment results merged with user overrides; omit any key whose enrichment failed and had no user override).\n- `<ticket-keys>` is the original list of ticket keys parsed in Stage 0, space-separated and in the original order.\n\nExample for two tickets after successful enrichment, throttled to 2 concurrent worktrees:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets \\\n --max-parallel 2 \\\n --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step \\\n --branch BAPI-250=feature/BAPI-250-deep-research-durability \\\n BAPI-248 BAPI-250\n```\n\nExample launching Cursor Agent instead of the default Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\nExample cutting worktrees from a non-`main` base (either user-supplied via `--base-branch develop` in Stage 0 or resolved from Bridge API config in Stage 2a):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --base-branch develop BAPI-248\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket in the form `KEY branch=BRANCH status=STATUS`, with an optional trailing `path=PATH`) and reformat it as a markdown table:\n\n```\n| Ticket | Branch | Status |\n|----------|-----------------------------------------------------|----------|\n| BAPI-248 | feature/BAPI-248-add-pr-rating-pre-evaluation-step | spawned |\n| BAPI-250 | feature/BAPI-250-deep-research-durability | spawned |\n```\n\nStatus values are `dry-run`, `spawned`, `create-failed`, and `spawn-failed`. End the report with the worktree-first explanation, rendered for the tracked `selected_agent`. When `selected_agent` is `claude` (the default):\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`claude \'/implement-ticket <KEY>\'` inside its already-created worktree, which launches\nClaude Code with the starter prompt as its first message. Switch to each tab \u2014 or on\nLinux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `cursor-agent`, render the same explanation but with the Cursor handoff \u2014 do **not** claim it launches Claude Code:\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`cursor-agent \'/implement-ticket <KEY>\'` inside its already-created worktree, which\nlaunches Cursor Agent with the starter prompt as its first message. Switch to each\ntab \u2014 or on Linux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nThe `/implement-ticket <KEY>` prompt is identical for both agents; only the launched command differs. When start-tickets was invoked with `--auto`, the spawned prompt is `/implement-ticket <KEY> --auto` (the implementation pipeline runs hands-off, without approval gates).\n\nIf the CLI reported any `create-failed` or `spawn-failed` statuses, or Stage 2 emitted any enrichment warnings, list them under a `Warnings:` heading at the bottom of the report. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the deep-dive runbook and the Worktrunk verification result behind this worktree-first model.\n\n## Difficulty-Based Implementation-Model Routing\n\nBefore launching the interactive agent for each ticket, the packaged CLI selects an\nimplementation **model tier** from the ticket\'s `difficulty` rating (1-10) and injects\nit as a `--model` flag at the agent spawn boundary. This happens entirely inside the\nCLI \u2014 it is **not** part of the server-side `/implement-ticket` recipe, because the\nmodel an interactive agent session uses is fixed at the moment the process is launched.\n\n- **Tier ladder (fixed):** `difficulty 1-2 \u2192 cheap`, `3-5 \u2192 basic`, `6+ \u2192 premium`.\n- **Separation of concerns:** the Python backend returns only the coarse tier\n (`cheap`/`basic`/`premium`) via `GET /jira/tickets/{KEY}/model-tier`; difficulty is\n computed on demand and cached when absent. The TypeScript CLI alone maps a tier to\n the agent-specific model alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`:\n version-suffixed strings validated against `cursor-agent --list-models`).\n- **Per-repo config:**\n - `difficulty_model_routing_enabled` \u2014 boolean, **default ON**. Set to `false` to\n disable routing for a repo (the CLI then omits `--model`).\n - `difficulty_model_tier_overrides` \u2014 a JSON object mapping a tier name to a model\n alias (e.g. `{"premium": "opus"}`), **not** raw CLI arguments. Only `cheap`,\n `basic`, and `premium` keys are accepted; aliases must match `^[A-Za-z0-9._:-]+$`.\n- **Fail-open:** routing never aborts a spawn. Credential, network, config, or\n no-tier routing failures **assume a hard ticket and default to the premium/Opus\n tier** when the selected agent supports a valid premium alias; routing being\n disabled (`difficulty_model_routing_enabled = false`) or an agent that does not\n support `--model` instead omit `--model` so the agent runs on its own default\n model. Each degraded case is surfaced as exactly one secret-free, per-ticket\n routing-diagnostic line, never a hard failure.\n\n### Model routing credential\n\nDifficulty\u2192model routing needs Bridge API credentials, and the shell-spawned\n`start-tickets` CLI is a **different runtime surface** from the MCP server: a\n`BAPI_API_KEY` that lives only in `.mcp.json` / `.cursor/mcp.json` is visible to\nthe MCP server but **not** to the Bash-spawned CLI, so routing silently degrades.\nThe durable source of truth both runtimes can resolve is the user-scoped store\n`~/.config/bridge/credentials.json`, keyed `bapi:<repo>`. If a routing-diagnostic\nline reports the credential is missing (e.g. difficulty resolves as `?`), fix it\nby any one of:\n\n1. Rerun `/install-bridge` \u2014 its final stage now persists the validated routing\n credential into `~/.config/bridge/credentials.json` via the\n `persist_routing_credential` tool.\n2. Migrate a key that lives **only** in `.mcp.json` / `.cursor/mcp.json` into the\n user-scoped store with the consent-gated, one-shot command:\n\n ```\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials\n ```\n\n3. Manually add `BAPI_API_KEY` under the `bapi:<repo>` target in the user-scoped\n store `~/.config/bridge/credentials.json`.\n\nNever put `BAPI_API_KEY` into a worktree `.mcp.json` / `.cursor/mcp.json` as a fix \u2014\nthat env is invisible to the spawned CLI.\n\n## Conductor observability (opt-in via `--conductor`, BAPI-394)\n\nConductor is **opt-in**. By default `start-tickets` spawns the plain\n`cd <worktree> && <agent> \'/implement-ticket <KEY> [--auto]\'` \u2014 no\n`BAPI_CONDUCTOR_*` env, no supervisor window, and no message-relay instruction.\nPass `--conductor` (e.g. `/start-tickets --conductor BAPI-123`) to enable the\nConductor system below.\n\nWith `--conductor`, a run mints a single conductor `run_id` and attributes each\nworker\'s lifecycle events by `worker_id`, ticket key, and worktree path, and a\nsupervisor peer tab is opened. When the selected agent is **Claude Code**, the CLI\ninjects a conductor lifecycle hook into each created worktree\'s\n`.claude/settings.local.json` so the spawned session emits local `run.started` /\n`run.stopped` / `agent.notification` (and, when\n`BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1`, `tool.intent`) events into the local\nconductor ledger. These hooks apply **only** when the selected agent is Claude\nCode; other agents (e.g. `cursor-agent`) still participate in the run-level\n`run.started` event but receive no per-worktree Claude hook. Inspect the ledger\nwith the `conductor` CLI (e.g. `conductor doctor`). Conductor observability is\nbest-effort and never blocks or aborts a spawn.\n\nAlso under `--conductor`, each worker is launched with an explicit instruction to\ncall the `check_messages` MCP tool at checkpoints, so the supervisor can pass it\ntyped guidance mid-run (BAPI-397). Delivery is **cooperative** \u2014 the worker polls\nand acknowledges messages and they are never injected into a running session.\n(Epic-tick dispatch always runs with conductor enabled, independent of this\nuser-facing flag.)\n',
15485
+ "start-tickets.md": '---\nschedulable: true\narguments: {"positionals":[{"name":"ticketKeys","type":"string","required":true,"variadic":true}],"flags":[{"name":"auto","flag":"--auto","type":"boolean"},{"name":"agent","flag":"--agent","type":"string"},{"name":"workflow","flag":"--workflow","type":"string"},{"name":"rounds","flag":"--rounds","type":"string"},{"name":"baseBranch","flag":"--base-branch","type":"string"},{"name":"maxParallel","flag":"--max-parallel","type":"string"},{"name":"dryRun","flag":"--dry-run","type":"boolean"}]}\n---\n\n# Start Tickets: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command takes one or more Jira ticket keys (e.g., `BAPI-248 BAPI-250`) and invokes the packaged `@bridge_gpt/mcp-server` CLI subcommand `start-tickets`, which creates a Worktrunk worktree for each key and opens one tab/session per worktree running the **selected agent** \u2014 Claude Code (`claude`) by default, or Cursor Agent (`cursor-agent`) via `--agent` \u2014 in a macOS Terminal/iTerm tab, a Windows Terminal tab (or PowerShell fallback window), or a detached Linux tmux session, chosen automatically by platform. It replaces Parts 2\u20135 of `docs/claude/parallel-worktrees.md` with a single command.\n\nBecause the orchestration ships inside the `@bridge_gpt/mcp-server` npm package (not a repo-local script), this command works for every consumer \u2014 including projects that installed the package via `--init`.\n\nFor existing ticket keys, `/review-and-start <KEYS>` is the **recommended front door**: it supplies the same connectivity check and branch enrichment as this command, then drives this same packaged CLI with `--workflow review-and-implement` so each worktree reviews the ticket before implementing it. Using `start-tickets --workflow review-and-implement` directly (documented below) remains available as the lower-level launcher seam.\n\nStage 0 and Stage 1 are critical (stop on failure). Stage 2 is non-critical (per-ticket enrichment failures fall back to the default branch and continue). Stage 3 is critical (propagate the packaged CLI\'s exit code).\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline that spawns N parallel Worktrunk worktrees and selected-agent sessions (Claude Code by default) via the packaged CLI. Execute all stages in sequence directly in the main thread.\n\n## Stage 0 \u2014 Argument Parsing and Connectivity Check\n\n1. **Parse `$ARGUMENTS`** into ticket keys, pass-through flags, and branch overrides:\n - **Ticket keys**: every whitespace-separated token matching `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). If zero keys are found, stop immediately and display:\n\n ```\n No ticket keys found in arguments. Expected one or more keys like BAPI-248.\n Usage: /start-tickets [flags] <KEY> [KEY ...] (e.g., /start-tickets BAPI-248 BAPI-250)\n ```\n\n - **Pass-through flags**: collect any of `--agent <name>` (and the equals form `--agent=<name>`), `--terminal terminal|iterm`, `--dry-run`, `--auto`, `--no-refresh-main`, `--base-branch <branch>` (and the equals form `--base-branch=<branch>`), and `--max-parallel N` that the user supplied. These are forwarded verbatim to the CLI in Stage 3. `--auto` makes each spawned agent run the selected workflow\'s slash command with `--auto` (hands-off); omit it to keep the spawned agents interactive.\n - **Selected agent**: track a `selected_agent` variable that defaults to `claude`. If the user passed `--agent <name>` / `--agent=<name>`, validate the value against the supported agents `claude` and `cursor-agent`, set `selected_agent` to it, and reject any other (malformed/unsupported) `--agent` value before proceeding. The agent is not auto-detected from the host editor \u2014 the user selects it explicitly (default `claude`).\n - **Selected workflow**: track a `selected_workflow` variable that defaults to `implement`. If the user passed `--workflow <value>` or `--workflow=<value>`, validate it against the two allowed values `implement` and `review-and-implement`, set `selected_workflow`, and reject any other value with the allowlist in the error. `implement` (the default) preserves today\'s behavior byte-for-byte \u2014 each spawned worktree runs `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]` instead, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` inside the same session. A single chain-level `--auto` applies to the selected workflow as a whole \u2014 under `review-and-implement` it auto-approves both the review and the implementation phase.\n - **Review rounds**: track a `review_rounds` value that defaults to unset. If the user passed `--rounds <n>` or `--rounds=<n>`, normalize it to `--rounds=1` or `--rounds=2` (reject any other value). `--rounds` is **review-only**: reject it (after parsing all flags, so flag order does not matter) if the final `selected_workflow` is not `review-and-implement`.\n - **User-supplied base branch**: track a `user_supplied_base_branch` boolean that defaults to `false`. If the user passed `--base-branch <branch>` or `--base-branch=<branch>`, set the boolean to `true` and capture the value. A user-supplied `--base-branch` value **takes precedence** over any value resolved from Bridge API config in Stage 2. Validate the user-supplied value before proceeding: after trimming surrounding whitespace it must be non-empty, at most 255 characters, must not start with `-`, and must not contain ASCII control characters (`0x00`\u2013`0x1F` or `0x7F`); reject any malformed value with a clear error.\n - **User branch overrides**: collect any user-supplied repeatable `--branch KEY=BRANCH` flags. A user-provided override always takes precedence over Stage 2 enrichment for that key.\n - Reject malformed input before proceeding: if a token looks like a flag but is not one of the supported flags, or a ticket key does not match `[A-Z]+-[0-9]+`, or a `--branch` value is not `KEY=BRANCH`, or `--agent` names an agent other than `claude`/`cursor-agent`, or `--workflow` names anything other than `implement`/`review-and-implement`, or `--rounds` is used outside `review-and-implement` or names anything other than `1`/`2`, or `--base-branch` fails the validation rules above, stop and report the malformed argument.\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\nThis stage is **critical** \u2014 stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Acknowledge CLI Pre-flight\n\nThe packaged CLI runs its own per-platform pre-flight checks and then fetches `origin` and fast-forwards the local **configured base branch** (the value resolved in Stage 2 below, or `main` when none is configured) from `origin/<base>` so the new worktrees are based on an up-to-date base. The historical flag `--no-refresh-main` still controls this behavior \u2014 the flag name is preserved for backward compatibility, but it now skips refresh of whatever base branch resolves (default `main`). The required commands depend on the OS:\n\n- **macOS**: `wt`, `git`, `osascript`.\n- **Windows**: `git-wt`, `git`, Git for Windows / Git Bash (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash), and Windows Terminal **or** PowerShell.\n- **Linux**: `wt`, `git`, `tmux`.\n\nOn **Windows** the Worktrunk binary is `git-wt` (its winget alias), which is a different tool from Windows Terminal\'s `wt.exe`: the CLI uses `git-wt` to **create worktrees** and `wt.exe` to **open a tab**, and never conflates the two. On **Linux** the CLI opens one detached `tmux` session per ticket (a window is added if that ticket\'s session already exists); attach later with `tmux attach -t <session>`. An unsupported OS (not macOS/Windows/Linux) fails fast with a clear "unsupported platform" message.\n\nThis stage simply notes that the CLI will fail fast if any prerequisite is missing or if local `main` has diverged from `origin/main` \u2014 you do not need to verify anything separately here, and you must not run any pre-flight commands yourself. When the CLI\'s pre-flight fails it now hints the user to run the read-only diagnostics command `npx -y @bridge_gpt/mcp-server doctor`, which reports found/missing for every prerequisite on the current OS \u2014 the pre-flight set plus `uv` plus the selected agent\'s command \u2014 and prints the manual install command for each missing one. `doctor` is strictly read-only and never installs anything; never run install commands automatically on the user\'s behalf. The CLI does not call any Bridge API tools; all credential-bearing work (branch enrichment in Stage 2) stays in this command. Proceed to Stage 2.\n\nThe packaged CLI also performs **secret-free Bridge API MCP provisioning** inside each created worktree: synchronously after the worktree is created and **before the agent tab/session is opened**, it writes both `.mcp.json` (Claude Code) and `.cursor/mcp.json` (Cursor) pointing at the `mcp-invoke` shim. These registrations are **secret-free** \u2014 they contain no `env` block and no API key, because the shim resolves credentials at runtime. If a spawned agent (or difficulty\u2192model routing) reports missing Bridge API credentials, fix it by rerunning `/install-bridge` (its final stage persists the routing credential), by running `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate a key that lives only in `.mcp.json` / `.cursor/mcp.json`, or by adding a `bapi:<repo>` entry to the user-scoped credentials file (`~/.config/bridge/credentials.json`) \u2014 never by putting `BAPI_API_KEY` into the worktree `.mcp.json` or `.cursor/mcp.json` (that env is invisible to the Bash-spawned CLI).\n\nThis stage is **critical** in the sense that the CLI will abort if its pre-flight fails; you will see the error in Stage 3\'s output and must surface it.\n\n## Stage 2 \u2014 Resolve Base Branch + Enrich Branch Names (best-effort)\n\n### Stage 2a \u2014 Resolve configured `base_branch`\n\nThe CLI must be told which branch to cut new worktrees from. Resolution order:\n\n1. If `user_supplied_base_branch` from Stage 0 is `true`, **skip the config-field lookup entirely** and use the user-supplied value. The user\'s explicit `--base-branch` always wins; never call `config_field` for `base_branch` in that case.\n2. Otherwise, call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to `base_branch` (do not pass any other parameters; the tool resolves the repository from the MCP server\'s configured `BAPI_REPO_NAME`).\n3. Parse the response. Treat the result as the **configured base branch** only when the response is a JSON object whose `value` field is a non-empty string after trimming surrounding whitespace.\n4. Treat **all** of the following as "unset" \u2014 emit a single-line warning like `Warning: base_branch is unset; CLI will default to main` and **omit** the `--base-branch` flag entirely from the Stage 3 command (the CLI\'s own default is `main`):\n - `value` is `null`.\n - `value` is an empty string or a whitespace-only string.\n - The endpoint returns HTTP `400` (invalid field \u2014 happens before the registry includes `base_branch`).\n - The tool returns a network error, timeout, or non-JSON parse failure.\n - Any other lookup failure.\n5. When the configured value is usable, capture it in a `resolved_base_branch` variable. **Do not** stop the pipeline on a lookup failure; fall through to the CLI default.\n\nWhen forwarding `resolved_base_branch` into the Bash invocation in Stage 3, **shell-escape it safely**: replace every literal single quote `\'` in the value with the four-character sequence `\'\\\'\'`, then wrap the entire resulting string in single quotes (so the final argument looks like `\'<escaped-value>\'`). This is the standard POSIX single-quote escaping rule and is **mandatory** because `base_branch` is admin-configurable data that gets interpolated into a Bash command string; any unescaped single quote would otherwise break out of the surrounding quotes. Pass `--base-branch \'<escaped-value>\'` to the CLI as a single argv element \u2014 never expand the value unquoted into the command line.\n\n### Stage 2b \u2014 Enrich Branch Names\n\nBranch enrichment happens here, in the command, **before** invoking the CLI \u2014 the `get_ticket` MCP tool runs inside the MCP server process, which holds the Bridge API credentials the shell-spawned CLI does not have. For each parsed ticket key that does **not** already have a user-provided `--branch` override:\n\n1. Call the `get_ticket` MCP tool with `ticket_number` set to the key and `save_locally` set to `false`.\n2. From the response, extract the `summary` field. Slugify it: lowercase the string, replace every run of non-alphanumeric characters (`[^a-z0-9]+`) with a single dash `-`, trim leading and trailing dashes, and truncate to at most `40` characters (cutting at a dash boundary if possible).\n3. The enriched branch name is `feature/<KEY>-<slug>`. Example: `BAPI-248` with summary `"Add PR rating pre-evaluation step"` becomes `feature/BAPI-248-add-pr-rating-pre-evaluation-step` (trimmed at 40 chars).\n4. If the `get_ticket` call fails for a particular key (404, network error, missing summary) or produces an empty slug, emit a single-line warning like `Warning: could not enrich BAPI-248, falling back to feature/BAPI-248` and let the CLI apply its default `feature/<KEY>` for that key only. Do NOT stop the pipeline.\n5. Build a list of `--branch <KEY>=<BRANCH>` arguments \u2014 one entry per key whose enrichment succeeded \u2014 and merge it with any user-provided overrides from Stage 0. **Do not** call `get_ticket` for keys that already have a user-provided override; those overrides win.\n\nThis stage is **non-critical** \u2014 warnings are acceptable, the pipeline continues with the fallback default for any key that fails. Do not call the Bridge API from the CLI itself; the CLI never has credentials.\n\n## Stage 3 \u2014 Invoke the Packaged CLI\n\nUse the **Bash tool** to invoke the packaged CLI. Build the command line as:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets <pass-through-flags> <base-branch-flag> <branch-overrides> <ticket-keys>\n```\n\nWhere:\n- `<pass-through-flags>` are the supported flags collected in Stage 0 (`--agent`, `--terminal`, `--dry-run`, `--auto`, `--no-refresh-main`, `--max-parallel`), forwarded verbatim. Forward `--agent <name>` only if the user supplied it; otherwise omit it and the CLI defaults to `claude`. Forward `--auto` only if the user supplied it.\n- Forward `--workflow <selected_workflow>` only when the user explicitly passed `--workflow`; otherwise omit it and the CLI defaults to `implement`. Forward the normalized `--rounds=<n>` from Stage 0 only when the user supplied it (which Stage 0 already guarantees is only possible under `review-and-implement`).\n- `<base-branch-flag>` is `--base-branch \'<escaped-value>\'` (single-quoted using the Stage 2a escaping rule) **only when** the user supplied `--base-branch` in Stage 0 **or** Stage 2a\'s `config_field` lookup returned a non-empty configured value. When the configured value is unset / lookup fails / user did not supply one, **omit this flag entirely** so the CLI\'s own default (`main`) takes effect.\n- `<branch-overrides>` is the list of `--branch KEY=BRANCH` flags assembled in Stage 2 (enrichment results merged with user overrides; omit any key whose enrichment failed and had no user override).\n- `<ticket-keys>` is the original list of ticket keys parsed in Stage 0, space-separated and in the original order.\n\nExample for two tickets after successful enrichment, throttled to 2 concurrent worktrees:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets \\\n --max-parallel 2 \\\n --branch BAPI-248=feature/BAPI-248-add-pr-rating-pre-evaluation-step \\\n --branch BAPI-250=feature/BAPI-250-deep-research-durability \\\n BAPI-248 BAPI-250\n```\n\nExample launching Cursor Agent instead of the default Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\nExample cutting worktrees from a non-`main` base (either user-supplied via `--base-branch develop` in Stage 0 or resolved from Bridge API config in Stage 2a):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --base-branch develop BAPI-248\n```\n\nExample using the lower-level review-and-implement workflow directly (the `/review-and-start` command is the recommended front door for this; this form is documented here as the advanced launcher seam it drives):\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --workflow review-and-implement --auto --rounds=2 BAPI-248\n```\n\nPass through the CLI\'s stdout and stderr to the user verbatim. If the CLI exits non-zero, treat that as a critical failure: report the exit code and the CLI\'s error output, and stop.\n\nThis stage is **critical** \u2014 propagate any non-zero exit from the packaged CLI.\n\n## Stage 4 \u2014 Final Report\n\nOnce the CLI exits 0, parse its `Summary` section (one stable line per ticket in the form `KEY branch=BRANCH status=STATUS`, with an optional trailing `path=PATH`) and reformat it as a markdown table:\n\n```\n| Ticket | Branch | Status |\n|----------|-----------------------------------------------------|----------|\n| BAPI-248 | feature/BAPI-248-add-pr-rating-pre-evaluation-step | spawned |\n| BAPI-250 | feature/BAPI-250-deep-research-durability | spawned |\n```\n\nStatus values are `dry-run`, `spawned`, `create-failed`, and `spawn-failed`. This table (and the report as a whole) describes **worktree/spawn status only** \u2014 it must never claim that review or implementation itself has completed; that work happens later, independently, inside each spawned session.\n\nCompute `spawned_command` from `selected_workflow`: `/implement-ticket <KEY>` when `implement` (the default), or `/review-and-implement <KEY>` when `review-and-implement`. Append `--auto` when the user passed it, and (workflow `review-and-implement` only) append the normalized `--rounds=<n>` when the user supplied `--rounds`. End the report with the worktree-first explanation, rendered for the tracked `selected_agent` and `spawned_command`. When `selected_agent` is `claude` (the default):\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`claude \'<spawned_command>\'` inside its already-created worktree, which launches\nClaude Code with the starter prompt as its first message. Switch to each tab \u2014 or on\nLinux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nWhen `selected_agent` is `cursor-agent`, render the same explanation but with the Cursor handoff \u2014 do **not** claim it launches Claude Code:\n\n```\nThe CLI created/switched each Worktrunk worktree first (throttled by --max-parallel),\nthen opened one tab/session per successful worktree (macOS Terminal/iTerm tab, Windows\nTerminal tab or PowerShell window, or Linux tmux session). Each one runs\n`cursor-agent \'<spawned_command>\'` inside its already-created worktree, which\nlaunches Cursor Agent with the starter prompt as its first message. Switch to each\ntab \u2014 or on Linux run `tmux attach -t <session>` \u2014 to monitor.\n```\n\nThe spawned command is identical for both agents; only the launched agent binary differs. Under `review-and-implement`, each spawned session independently runs `/review-ticket`, pauses at its own per-ticket halt gate (unless chain-level `--auto` was passed), and only then runs `/implement-ticket` \u2014 do not report that review or implementation succeeded from this parent session.\n\nIf the CLI reported any `create-failed` or `spawn-failed` statuses, or Stage 2 emitted any enrichment warnings, list them under a `Warnings:` heading at the bottom of the report. If there were none, omit that section.\n\nSee `docs/claude/parallel-worktrees.md` for the deep-dive runbook and the Worktrunk verification result behind this worktree-first model.\n\n## Difficulty-Based Implementation-Model Routing\n\nBefore launching the interactive agent for each ticket, the packaged CLI selects an\nimplementation **model tier** from the ticket\'s `difficulty` rating (1-10) and injects\nit as a `--model` flag at the agent spawn boundary. This happens entirely inside the\nCLI \u2014 it is **not** part of the server-side `/implement-ticket` recipe, because the\nmodel an interactive agent session uses is fixed at the moment the process is launched.\n\n- **Tier ladder (fixed):** `difficulty 1-2 \u2192 cheap`, `3-5 \u2192 basic`, `6+ \u2192 premium`.\n- **Separation of concerns:** the Python backend returns only the coarse tier\n (`cheap`/`basic`/`premium`) via `GET /jira/tickets/{KEY}/model-tier`; difficulty is\n computed on demand and cached when absent. The TypeScript CLI alone maps a tier to\n the agent-specific model alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`:\n version-suffixed strings validated against `cursor-agent --list-models`).\n- **Per-repo config:**\n - `difficulty_model_routing_enabled` \u2014 boolean, **default ON**. Set to `false` to\n disable routing for a repo (the CLI then omits `--model`).\n - `difficulty_model_tier_overrides` \u2014 a JSON object mapping a tier name to a model\n alias (e.g. `{"premium": "opus"}`), **not** raw CLI arguments. Only `cheap`,\n `basic`, and `premium` keys are accepted; aliases must match `^[A-Za-z0-9._:-]+$`.\n- **Fail-open:** routing never aborts a spawn. Credential, network, config, or\n no-tier routing failures **assume a hard ticket and default to the premium/Opus\n tier** when the selected agent supports a valid premium alias; routing being\n disabled (`difficulty_model_routing_enabled = false`) or an agent that does not\n support `--model` instead omit `--model` so the agent runs on its own default\n model. Each degraded case is surfaced as exactly one secret-free, per-ticket\n routing-diagnostic line, never a hard failure.\n\n### Model routing credential\n\nDifficulty\u2192model routing needs Bridge API credentials, and the shell-spawned\n`start-tickets` CLI is a **different runtime surface** from the MCP server: a\n`BAPI_API_KEY` that lives only in `.mcp.json` / `.cursor/mcp.json` is visible to\nthe MCP server but **not** to the Bash-spawned CLI, so routing silently degrades.\nThe durable source of truth both runtimes can resolve is the user-scoped store\n`~/.config/bridge/credentials.json`, keyed `bapi:<repo>`. If a routing-diagnostic\nline reports the credential is missing (e.g. difficulty resolves as `?`), fix it\nby any one of:\n\n1. Rerun `/install-bridge` \u2014 its final stage now persists the validated routing\n credential into `~/.config/bridge/credentials.json` via the\n `persist_routing_credential` tool.\n2. Migrate a key that lives **only** in `.mcp.json` / `.cursor/mcp.json` into the\n user-scoped store with the consent-gated, one-shot command:\n\n ```\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials\n ```\n\n3. Manually add `BAPI_API_KEY` under the `bapi:<repo>` target in the user-scoped\n store `~/.config/bridge/credentials.json`.\n\nNever put `BAPI_API_KEY` into a worktree `.mcp.json` / `.cursor/mcp.json` as a fix \u2014\nthat env is invisible to the spawned CLI.\n\n## Conductor observability (opt-in via `--conductor`, BAPI-394)\n\nConductor is **opt-in**. By default `start-tickets` spawns the plain\n`cd <worktree> && <agent> \'/implement-ticket <KEY> [--auto]\'` \u2014 no\n`BAPI_CONDUCTOR_*` env, no supervisor window, and no message-relay instruction.\nPass `--conductor` (e.g. `/start-tickets --conductor BAPI-123`) to enable the\nConductor system below.\n\nWith `--conductor`, a run mints a single conductor `run_id` and attributes each\nworker\'s lifecycle events by `worker_id`, ticket key, and worktree path, and a\nsupervisor peer tab is opened. When the selected agent is **Claude Code**, the CLI\ninjects a conductor lifecycle hook into each created worktree\'s\n`.claude/settings.local.json` so the spawned session emits local `run.started` /\n`run.stopped` / `agent.notification` (and, when\n`BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1`, `tool.intent`) events into the local\nconductor ledger. These hooks apply **only** when the selected agent is Claude\nCode; other agents (e.g. `cursor-agent`) still participate in the run-level\n`run.started` event but receive no per-worktree Claude hook. Inspect the ledger\nwith the `conductor` CLI (e.g. `conductor doctor`). Conductor observability is\nbest-effort and never blocks or aborts a spawn.\n\nAlso under `--conductor`, each worker is launched with an explicit instruction to\ncall the `check_messages` MCP tool at checkpoints, so the supervisor can pass it\ntyped guidance mid-run (BAPI-397). Delivery is **cooperative** \u2014 the worker polls\nand acknowledges messages and they are never injected into a running session.\n(Epic-tick dispatch always runs with conductor enabled, independent of this\nuser-facing flag.)\n',
14845
15486
  "teach-bridge.md": 'Update a Bridge API configuration field via a natural-language teaching.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes a natural-language teaching (e.g., "use data-testid selectors in Playwright tests") and updates the appropriate Bridge API configuration field. The teaching is auto-classified to the correct field, merged with existing content as actionable AI instructions, and uploaded after user confirmation.\n\n`$ARGUMENTS` is required \u2014 it is the teaching text. If `$ARGUMENTS` is empty, show:\n\n```\nUsage: /teach-bridge <teaching>\n\nExamples:\n /teach-bridge use data-testid selectors in Playwright tests\n /teach-bridge always validate input DTOs with Pydantic before passing to service layer\n /teach-bridge prefer composition over inheritance for service classes\n```\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n## Stage 0 \u2014 Preflight\n\n1. **Validate arguments**: If `$ARGUMENTS` is empty or contains only whitespace, display the usage instructions above and stop.\n\n2. **Admin check**: Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `role` is `"admin"` OR `source` is `"legacy"`: proceed normally.\n - Otherwise: stop immediately and display:\n ```\n Admin access required. Your API key has role "<role>" (source: <source>).\n Only admin keys and legacy shared keys can update configuration fields.\n Contact your project administrator to request admin access.\n ```\n\nIf this stage fails, stop immediately and report the error. Do not proceed to Stage 1.\n\n## Stage 1 \u2014 Classify\n\n1. **List available fields**: Call the `config_field` MCP tool with `operation` set to `"list"` (no other parameters). This returns all available configuration field names with descriptions.\n\n2. **Evaluate the teaching**: Compare the user\'s teaching (`$ARGUMENTS`) against each field\'s description to determine which field it applies to.\n\n3. **Handle classification outcomes**:\n - **Clear single match**: If one field is clearly the best target, proceed to Stage 2 with that field.\n - **Multiple plausible matches**: If 2-3 fields are equally plausible, present them to the user with their descriptions and ask which one to update. Wait for user input before proceeding.\n - **No confident match**: If you cannot confidently map the teaching to any field, ask the user to elaborate or specify which field they intend. Wait for user input before proceeding.\n\n## Stage 2 \u2014 Merge\n\n1. **Read current value**: Call the `config_field` MCP tool with `operation` set to `"get"` and `field_name` set to the selected field from Stage 1. Capture the current value, description, and examples from the response.\n\n2. **Draft the update**:\n - **If the field is currently null or empty**: Compose initial content from the teaching. Rephrase the user\'s input as imperative, agent-facing instructions (e.g., convert "I want you to use data-testid" to "Always use `data-testid` attributes for Playwright element locators"). Do not use the user\'s exact conversational text.\n - **If the field has existing content**: Merge the teaching into the existing value at the most appropriate location. Rephrase as imperative, agent-facing instructions. Preserve the existing structure and formatting.\n\n3. **Handle contradictions**: If the teaching contradicts existing instructions in the field, present both the existing instruction and the new teaching side-by-side and ask the user which should take precedence. Wait for user input before proceeding.\n\n## Stage 3 \u2014 Confirm and Upload\n\n1. **Show the proposed update**: Display to the user:\n - **Field**: The name of the field being updated\n - **Change summary**: A brief description of what was added or changed\n - **Full proposed value**: The complete new value for the field (not just the diff)\n\n2. **Wait for confirmation**: Ask the user to confirm, request edits, or abort.\n\n3. **On confirmation**: Call the `config_field` MCP tool with:\n - `operation`: `"update"`\n - `field_name`: the selected field name\n - `value`: the full merged value (pass inline, do not use `file_path`)\n\n Display a success message confirming the update.\n\n4. **On rejection**: Ask the user what they\'d like to change. If they provide edits, revise the proposed value and show it again. If they abort, stop without making any changes.\n',
14846
15487
  "upgrade-bridge.md": "# Upgrade Bridge\n\n$ARGUMENTS\n\nUse this command to upgrade (or update) the Bridge API MCP \u2014 the\n`@bridge_gpt/mcp-server` package, also called the bridge-api MCP \u2014 to the latest\npublished version. This is the action behind the ping tool's advice to \"tell\nyour local agent 'upgrade bridge'\".\n\n---\n\n# Instructions\n\nRun the existing packaged upgrade flow. Do not edit files, install anything by\nhand, or invent a new subcommand \u2014 just drive the upgrade CLI and report what it\ndid.\n\n## Step 1 \u2014 Run the upgrade command\n\nFrom the **project root**, run exactly:\n\n```\nnpx -y @bridge_gpt/mcp-server --upgrade\n```\n\nThis upgrades/updates the installed `@bridge_gpt/mcp-server` (the bridge-api MCP)\nand re-scaffolds the slash commands.\n\n## Step 2 \u2014 Report the result\n\n- If the CLI reports a version change, report it in the CLI's\n `oldVersion -> newVersion` form (e.g. `0.1.17 -> 0.1.19`), mirroring the\n `runUpgradeCli` output.\n- If the CLI reports that no upgrade was needed (the installed version is already\n the latest), report `Already up-to-date.` exactly.\n\n## Step 3 \u2014 Handle failures\n\nIf the command fails (non-zero exit or an error in its output), **stop** and\nreport the CLI error verbatim. Do not retry blindly or attempt manual edits to\nwork around it.\n\n## Final Report\n\nReport whether the bridge-api MCP was upgraded (with the\n`oldVersion -> newVersion` transition), was already current (`Already up-to-date.`),\nor failed (with the CLI error).\n"
14847
15488
  };
@@ -15534,11 +16175,11 @@ init_version_generated();
15534
16175
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
15535
16176
  import { spawn } from "child_process";
15536
16177
  import os5 from "os";
15537
- import path16 from "path";
16178
+ import path17 from "path";
15538
16179
 
15539
16180
  // src/install-doctor.ts
15540
16181
  init_credential_store();
15541
- import path15 from "path";
16182
+ import path16 from "path";
15542
16183
  var DEFAULT_BASE_URL = "https://bridgegpt-api.com";
15543
16184
  var MCP_CONFIG_ENV_TARGETS = [
15544
16185
  { relPath: ".mcp.json", topLevelKey: "mcpServers" },
@@ -15556,7 +16197,7 @@ async function resolveInstallDoctorTarget(deps) {
15556
16197
  for (const { relPath, topLevelKey } of MCP_CONFIG_ENV_TARGETS) {
15557
16198
  let raw;
15558
16199
  try {
15559
- raw = await deps.readFile(path15.join(deps.cwd, relPath));
16200
+ raw = await deps.readFile(path16.join(deps.cwd, relPath));
15560
16201
  } catch {
15561
16202
  continue;
15562
16203
  }
@@ -16031,7 +16672,7 @@ function probeNpxNoInstallDefault(spec) {
16031
16672
  async function inspectLauncherCache(deps) {
16032
16673
  const inspections = [];
16033
16674
  for (const { relPath, topLevelKey } of LAUNCHER_CONFIG_TARGETS) {
16034
- const fullPath = path16.join(deps.cwd, relPath);
16675
+ const fullPath = path17.join(deps.cwd, relPath);
16035
16676
  let raw;
16036
16677
  try {
16037
16678
  raw = await deps.readFile(fullPath);
@@ -16208,7 +16849,7 @@ init_credential_store();
16208
16849
  init_third_party_mcp_targets();
16209
16850
  import { spawn as spawn2, execFile as execFile3 } from "child_process";
16210
16851
  import { stat as stat5, readFile as readFile7 } from "fs/promises";
16211
- import path19 from "path";
16852
+ import path20 from "path";
16212
16853
  import os6 from "os";
16213
16854
  function getMcpInvokeUsage() {
16214
16855
  return [
@@ -16283,7 +16924,7 @@ function parseMcpInvokeArgs(argv) {
16283
16924
  if (projectRoot === void 0) {
16284
16925
  return { status: "error", message: "Missing required --project-root flag" };
16285
16926
  }
16286
- if (!path19.isAbsolute(projectRoot)) {
16927
+ if (!path20.isAbsolute(projectRoot)) {
16287
16928
  return { status: "error", message: "--project-root must be an absolute path" };
16288
16929
  }
16289
16930
  return { status: "ok", target: targetValidation.value, projectRoot };
@@ -16832,8 +17473,8 @@ function createDefaultExecutorDeps() {
16832
17473
  mkdir: (dirPath, opts) => mkdir5(dirPath, opts),
16833
17474
  stat: (filePath) => stat6(filePath).then((s) => ({ mode: s.mode })),
16834
17475
  statMtimeMs: (filePath) => stat6(filePath).then((s) => s.mtimeMs).catch(() => null),
16835
- statfs: async (path33) => {
16836
- const s = await statfs(path33);
17476
+ statfs: async (path34) => {
17477
+ const s = await statfs(path34);
16837
17478
  return { bavail: Number(s.bavail), bsize: Number(s.bsize) };
16838
17479
  },
16839
17480
  sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
@@ -17153,6 +17794,7 @@ function buildClaimManifest(report, options, freeSlots) {
17153
17794
  import os9 from "node:os";
17154
17795
 
17155
17796
  // src/executor/env.ts
17797
+ init_pr_base_contract();
17156
17798
  var ALLOWED_ENV_KEYS = [
17157
17799
  "PATH",
17158
17800
  "HOME",
@@ -17186,7 +17828,7 @@ function isExecutorEnvKeyAllowed(key) {
17186
17828
  if (SECRET_NAME_FRAGMENTS.some((fragment) => upper.includes(fragment))) return false;
17187
17829
  return true;
17188
17830
  }
17189
- function buildExecutorWorkerEnv(parentEnv) {
17831
+ function buildExecutorWorkerEnv(parentEnv, effectiveBaseBranch) {
17190
17832
  const env = {};
17191
17833
  for (const key of ALLOWED_ENV_KEYS) {
17192
17834
  if (!isExecutorEnvKeyAllowed(key)) continue;
@@ -17196,6 +17838,9 @@ function buildExecutorWorkerEnv(parentEnv) {
17196
17838
  }
17197
17839
  }
17198
17840
  env.BRIDGE_SKIP_PREPUSH = "1";
17841
+ if (typeof effectiveBaseBranch === "string" && effectiveBaseBranch.length > 0) {
17842
+ env[PR_BASE_BRANCH_ENV_VAR] = effectiveBaseBranch;
17843
+ }
17199
17844
  return env;
17200
17845
  }
17201
17846
 
@@ -17303,6 +17948,7 @@ var MissingVerdictArtifact = "MissingVerdictArtifact";
17303
17948
  var WorktreeLostBeforePush = "WorktreeLostBeforePush";
17304
17949
  var BranchMismatch = "BranchMismatch";
17305
17950
  var WorkerFinalizationMissingRemoteBranchAndPr = "WorkerFinalizationMissingRemoteBranchAndPr";
17951
+ var WorkerFinalizationPrBaseMismatch = "WorkerFinalizationPrBaseMismatch";
17306
17952
  var ERROR_MESSAGE_MAX_CHARS = 300;
17307
17953
  var ExecutorNamedError = class extends Error {
17308
17954
  errorKind;
@@ -17632,9 +18278,9 @@ async function provisionExecutorDenyLayer(worktreePath, options, deps) {
17632
18278
  init_worktree_core();
17633
18279
 
17634
18280
  // src/executor/worktree-inspection.ts
17635
- import path20 from "node:path";
18281
+ import path21 from "node:path";
17636
18282
  function pathApiForExecutorPlatform(platform) {
17637
- return platform === "win32" ? path20.win32 : path20.posix;
18283
+ return platform === "win32" ? path21.win32 : path21.posix;
17638
18284
  }
17639
18285
  function parseGitWorktreePorcelain(porcelain) {
17640
18286
  const entries = [];
@@ -17997,7 +18643,7 @@ ${userPrompt}`;
17997
18643
  }
17998
18644
 
17999
18645
  // src/executor/results.ts
18000
- import path21 from "node:path";
18646
+ import path22 from "node:path";
18001
18647
  var SMOKE_EVIDENCE_MAX_BYTES = 8e3;
18002
18648
  var ARTIFACT_MAX_BYTES = 32e3;
18003
18649
  var SUMMARY_MAX_CHARS = 500;
@@ -18043,7 +18689,7 @@ function buildGenericSuccessResult(input) {
18043
18689
  }
18044
18690
  async function readCompletionArtifacts(worktreePath, deps) {
18045
18691
  if (!worktreePath) return void 0;
18046
- const critiquePath = path21.join(worktreePath, ".conductor", "critique.md");
18692
+ const critiquePath = path22.join(worktreePath, ".conductor", "critique.md");
18047
18693
  let content;
18048
18694
  try {
18049
18695
  content = await deps.readFile(critiquePath);
@@ -18336,6 +18982,30 @@ function isImplementationStyleJobType(jobType) {
18336
18982
  return isSpawnJobType(jobType) && jobType !== "spec_review";
18337
18983
  }
18338
18984
 
18985
+ // src/executor/base-branch.ts
18986
+ init_base_ref();
18987
+ function resolveExecutorJobBaseBranch(job, fallbackBaseBranch) {
18988
+ const payload = job.payload;
18989
+ const raw = payload && typeof payload === "object" ? payload.base_branch : void 0;
18990
+ if (raw === void 0) {
18991
+ return { ok: true, baseBranch: fallbackBaseBranch };
18992
+ }
18993
+ if (typeof raw !== "string") {
18994
+ return {
18995
+ ok: false,
18996
+ error: "job payload base_branch is present but is not a string branch name."
18997
+ };
18998
+ }
18999
+ const validationError = validateBranchName(raw);
19000
+ if (validationError) {
19001
+ return {
19002
+ ok: false,
19003
+ error: `job payload base_branch is not a valid branch name: ${validationError}`
19004
+ };
19005
+ }
19006
+ return { ok: true, baseBranch: raw };
19007
+ }
19008
+
18339
19009
  // src/executor/worker-finalization.ts
18340
19010
  var DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS = 3;
18341
19011
  var DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS = 500;
@@ -18391,6 +19061,39 @@ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, b
18391
19061
  }
18392
19062
  return remoteSha;
18393
19063
  }
19064
+ async function resolvePrBaseRef(runCommand, worktreePath, branch) {
19065
+ const args = ["pr", "view"];
19066
+ if (branch) args.push(branch);
19067
+ args.push("--json", "number,baseRefName");
19068
+ let result;
19069
+ try {
19070
+ result = await runCommand("gh", args, { cwd: worktreePath });
19071
+ } catch {
19072
+ return { found: false, baseRef: null };
19073
+ }
19074
+ if (result.exitCode !== 0) return { found: false, baseRef: null };
19075
+ let parsed;
19076
+ try {
19077
+ parsed = JSON.parse(result.stdout);
19078
+ } catch {
19079
+ return { found: false, baseRef: null };
19080
+ }
19081
+ if (!parsed || typeof parsed !== "object") return { found: false, baseRef: null };
19082
+ const obj = parsed;
19083
+ const prNumber = typeof obj.number === "number" ? obj.number : void 0;
19084
+ const rawBase = obj.baseRefName;
19085
+ const baseRef = typeof rawBase === "string" && rawBase.trim().length > 0 ? rawBase.trim() : null;
19086
+ return { found: true, prNumber, baseRef };
19087
+ }
19088
+ function prBaseMismatchFailure(job, prNumber, expectedBase, actualBase) {
19089
+ const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
19090
+ const prLabel = typeof prNumber === "number" ? `PR #${prNumber}` : "its PR";
19091
+ return {
19092
+ error_kind: WorkerFinalizationPrBaseMismatch,
19093
+ error_message: `${label} opened ${prLabel} against base '${actualBase}', but the run base is '${expectedBase}'. A PR that does not target '${expectedBase}' never triggers code review and would strand the ticket. Recovery: rebuild the feature branch from a fresh origin/${expectedBase} and cherry-pick only this ticket's own commits, then force-push. Do NOT merely retarget the PR base in the GitHub UI \u2014 a branch built on a squash-merged dependency goes CONFLICTING when retargeted.`,
19094
+ classification: "crashed"
19095
+ };
19096
+ }
18394
19097
  function missingBranchAndPrFailure(job, detail) {
18395
19098
  const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
18396
19099
  return {
@@ -18404,6 +19107,25 @@ async function validateWorkerFinalization(input) {
18404
19107
  if (!isImplementationStyleJobType(job.job_type)) {
18405
19108
  return { ok: true };
18406
19109
  }
19110
+ const expectedBase = typeof input.expectedBaseBranch === "string" ? input.expectedBaseBranch.trim() : "";
19111
+ if (expectedBase) {
19112
+ const branchForPr = typeof branch === "string" ? branch.trim() : "";
19113
+ const prBase = await resolvePrBaseRef(runCommand, worktreePath, branchForPr);
19114
+ if (prBase.found) {
19115
+ if (prBase.baseRef === null) {
19116
+ return {
19117
+ ok: false,
19118
+ failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, "(unresolved)")
19119
+ };
19120
+ }
19121
+ if (prBase.baseRef !== expectedBase) {
19122
+ return {
19123
+ ok: false,
19124
+ failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, prBase.baseRef)
19125
+ };
19126
+ }
19127
+ }
19128
+ }
18407
19129
  if (extractPrUrl(result)) {
18408
19130
  return { ok: true };
18409
19131
  }
@@ -18463,6 +19185,7 @@ async function validateWorkerFinalization(input) {
18463
19185
  }
18464
19186
 
18465
19187
  // src/executor/worktree.ts
19188
+ init_base_ref();
18466
19189
  init_start_tickets_prereqs();
18467
19190
  init_worktree_core();
18468
19191
  function resolveExecutorBranch(job) {
@@ -18515,13 +19238,22 @@ async function ensureExecutorWorktree(job, options, deps, policy = {}) {
18515
19238
  }
18516
19239
  return { ok: false, error: row2.error ?? `worktree creation failed for branch '${branch}'` };
18517
19240
  }
19241
+ const resolvedBase = await fetchAndResolveBaseSha(deps, options.baseBranch);
19242
+ if (!resolvedBase.ok) {
19243
+ return {
19244
+ ok: false,
19245
+ error: `failed to resolve remote base '${options.baseBranch}' for a fresh worktree: ${resolvedBase.error}`
19246
+ };
19247
+ }
19248
+ const baseSha = resolvedBase.base_sha;
18518
19249
  const row = await createWorktreeForTicket(
18519
19250
  toWorktreeCoreDeps(deps),
18520
19251
  key,
18521
19252
  { [key]: branch },
18522
19253
  options.worktrunkBinary,
18523
- baseStartPoint,
18524
- guardStaleWorktree
19254
+ baseSha,
19255
+ guardStaleWorktree,
19256
+ { alignExistingBranchTo: baseSha, verifyHeadMatches: baseSha }
18525
19257
  );
18526
19258
  if (row.status === "created" && typeof row.path === "string") {
18527
19259
  return { ok: true, worktreePath: row.path, branch };
@@ -18529,6 +19261,9 @@ async function ensureExecutorWorktree(job, options, deps, policy = {}) {
18529
19261
  return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
18530
19262
  }
18531
19263
 
19264
+ // src/executor/job-runner.ts
19265
+ init_pr_base_contract();
19266
+
18532
19267
  // src/executor/worker-command.ts
18533
19268
  init_agent_registry();
18534
19269
  function resolveExecutorModelAlias(payload) {
@@ -19013,9 +19748,21 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
19013
19748
  }
19014
19749
  }
19015
19750
  async function runSpawnJob(job, httpClient, options, deps, ownership, observation, seams) {
19016
- const prep = await prepareSpawn(job, httpClient, options, deps, seams);
19751
+ const baseResolution = resolveExecutorJobBaseBranch(job, options.baseBranch);
19752
+ if (!baseResolution.ok) {
19753
+ await httpClient.fail(job, {
19754
+ error_kind: "ContractError.BaseBranch",
19755
+ error_message: baseResolution.error,
19756
+ classification: "crashed"
19757
+ });
19758
+ return { status: "failed", reason: "base_branch_contract" };
19759
+ }
19760
+ const effectiveBaseBranch = baseResolution.baseBranch;
19761
+ const jobOptions = { ...options, baseBranch: effectiveBaseBranch };
19762
+ const prep = await prepareSpawn(job, httpClient, jobOptions, deps, seams);
19017
19763
  if (!prep.ok) return prep.result;
19018
- const { worktreePath, branch, prompt } = prep;
19764
+ const { worktreePath, branch } = prep;
19765
+ const prompt = isImplementationStyleJobType(job.job_type) ? `${prep.prompt} ${buildPrBaseContractLaunchInstruction()}` : prep.prompt;
19019
19766
  const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
19020
19767
  if (!timeout.ok) {
19021
19768
  await httpClient.fail(job, {
@@ -19027,7 +19774,7 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
19027
19774
  }
19028
19775
  const deny = await provisionExecutorDenyLayer(
19029
19776
  worktreePath,
19030
- { baseBranch: options.baseBranch },
19777
+ { baseBranch: effectiveBaseBranch },
19031
19778
  {
19032
19779
  readFile: deps.readFile,
19033
19780
  writeFile: deps.writeFile,
@@ -19096,7 +19843,7 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
19096
19843
  };
19097
19844
  const alias = resolveExecutorModelAlias(job.payload);
19098
19845
  const argv = buildClaudeExecutorArgv(prompt, alias);
19099
- const env = buildExecutorWorkerEnv(deps.env);
19846
+ const env = buildExecutorWorkerEnv(deps.env, effectiveBaseBranch);
19100
19847
  let proc;
19101
19848
  try {
19102
19849
  proc = deps.spawnProcess(CLAUDE_EXECUTABLE, argv, { cwd: worktreePath, env });
@@ -19115,11 +19862,11 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
19115
19862
  stdout: proc.stdout ? teeAsyncIterable(proc.stdout, tee, "stdout") : null,
19116
19863
  stderr: proc.stderr ? teeAsyncIterable(proc.stderr, tee, "stderr") : null
19117
19864
  } : proc;
19118
- const collectTelemetry = () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, options.baseBranch);
19865
+ const collectTelemetry = () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch);
19119
19866
  const procResult = await superviseProcess({
19120
19867
  job,
19121
19868
  httpClient,
19122
- options,
19869
+ options: jobOptions,
19123
19870
  deps,
19124
19871
  ownership,
19125
19872
  observation,
@@ -19191,7 +19938,8 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
19191
19938
  worktreePath,
19192
19939
  result,
19193
19940
  runCommand: deps.runCommand,
19194
- headSha: git.last_commit_sha
19941
+ headSha: git.last_commit_sha,
19942
+ expectedBaseBranch: effectiveBaseBranch
19195
19943
  });
19196
19944
  if (!finalization.ok) {
19197
19945
  await finalizeRegistry();
@@ -19668,25 +20416,452 @@ ${getExecutorUsage()}`);
19668
20416
  errorLog(`Error: ${access2.error}`);
19669
20417
  return 1;
19670
20418
  }
19671
- apiKeyByRepo[repo] = access2.apiKey;
19672
- baseUrl = access2.baseUrl;
20419
+ apiKeyByRepo[repo] = access2.apiKey;
20420
+ baseUrl = access2.baseUrl;
20421
+ }
20422
+ const createHttpClient = overrides.createHttpClient ?? createExecutorHttpClient;
20423
+ const httpClient = createHttpClient({
20424
+ baseUrl,
20425
+ apiKey: apiKeyByRepo[options.repoName],
20426
+ apiKeyByRepo,
20427
+ mcpVersion: VERSION,
20428
+ fetch: deps.fetch
20429
+ });
20430
+ const run = overrides.runExecutor ?? runExecutor;
20431
+ try {
20432
+ return await run(options, deps, httpClient);
20433
+ } catch (err) {
20434
+ const message = err instanceof Error ? err.message : String(err);
20435
+ errorLog(`Error: executor exited unexpectedly: ${message.slice(0, 200)}`);
20436
+ return 1;
20437
+ }
20438
+ }
20439
+
20440
+ // src/setup-epic.ts
20441
+ init_bridge_api_client();
20442
+ init_plan();
20443
+ import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
20444
+ import os12 from "node:os";
20445
+ function createDefaultSetupEpicDeps() {
20446
+ return {
20447
+ env: process.env,
20448
+ cwd: process.cwd(),
20449
+ platform: process.platform,
20450
+ homedir: os12.homedir,
20451
+ readFile: (p) => fsReadFile(p, "utf-8"),
20452
+ stat: (p) => fsStat(p),
20453
+ fetch: globalThis.fetch,
20454
+ log: (m) => console.log(m),
20455
+ errorLog: (m) => console.error(m)
20456
+ };
20457
+ }
20458
+ function getSetupEpicUsage() {
20459
+ return [
20460
+ "Usage: mcp-server setup-epic --epic-key <KEY> --plan-file <path> [options]",
20461
+ "",
20462
+ "Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,",
20463
+ "and approves it. Idempotent \u2014 re-running reuses an existing live run.",
20464
+ "",
20465
+ "Required:",
20466
+ " --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
20467
+ " --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)",
20468
+ "",
20469
+ "Options:",
20470
+ " --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)",
20471
+ " --plan-version <n> Assert the sidecar's plan_version equals <n>",
20472
+ " --dry-run Validate and preview; make no mutating calls",
20473
+ " --json Emit a single JSON result object on stdout",
20474
+ " -h, --help Show this help",
20475
+ "",
20476
+ "After setup, the server-side reconciler picks the run up within ~30s.",
20477
+ "To execute claimed jobs on this machine, run:",
20478
+ " npx -y @bridge_gpt/mcp-server executor --repo <name>"
20479
+ ].join("\n");
20480
+ }
20481
+ function takeValue2(argv, i, flag) {
20482
+ const next = argv[i + 1];
20483
+ if (next === void 0 || next.startsWith("-")) return null;
20484
+ return next;
20485
+ }
20486
+ function parseSetupEpicArgs(argv) {
20487
+ if (argv.includes("-h") || argv.includes("--help")) {
20488
+ return { status: "help", usage: getSetupEpicUsage() };
20489
+ }
20490
+ let epicKey;
20491
+ let planFile;
20492
+ let repo;
20493
+ let planVersion;
20494
+ let dryRun = false;
20495
+ let json = false;
20496
+ for (let i = 0; i < argv.length; i++) {
20497
+ const arg = argv[i];
20498
+ switch (arg) {
20499
+ case "--epic-key": {
20500
+ const v = takeValue2(argv, i, arg);
20501
+ if (v === null) return { status: "error", message: "--epic-key requires a value." };
20502
+ epicKey = v;
20503
+ i++;
20504
+ break;
20505
+ }
20506
+ case "--plan-file": {
20507
+ const v = takeValue2(argv, i, arg);
20508
+ if (v === null) return { status: "error", message: "--plan-file requires a value." };
20509
+ planFile = v;
20510
+ i++;
20511
+ break;
20512
+ }
20513
+ case "--repo": {
20514
+ const v = takeValue2(argv, i, arg);
20515
+ if (v === null) return { status: "error", message: "--repo requires a value." };
20516
+ repo = v;
20517
+ i++;
20518
+ break;
20519
+ }
20520
+ case "--plan-version": {
20521
+ const v = takeValue2(argv, i, arg);
20522
+ if (v === null) return { status: "error", message: "--plan-version requires a value." };
20523
+ if (!/^\d+$/.test(v)) {
20524
+ return { status: "error", message: `--plan-version must be a positive integer, got '${v}'.` };
20525
+ }
20526
+ planVersion = Number(v);
20527
+ if (planVersion < 1) {
20528
+ return { status: "error", message: "--plan-version must be >= 1." };
20529
+ }
20530
+ i++;
20531
+ break;
20532
+ }
20533
+ case "--dry-run":
20534
+ dryRun = true;
20535
+ break;
20536
+ case "--json":
20537
+ json = true;
20538
+ break;
20539
+ default:
20540
+ return {
20541
+ status: "error",
20542
+ message: `Unknown argument '${arg}'. Run "setup-epic --help" for usage.`
20543
+ };
20544
+ }
20545
+ }
20546
+ if (!epicKey) return { status: "error", message: "setup-epic requires --epic-key <KEY>." };
20547
+ if (!planFile) return { status: "error", message: "setup-epic requires --plan-file <path>." };
20548
+ return {
20549
+ status: "ok",
20550
+ options: { epicKey, planFile, repo, planVersion, dryRun, json }
20551
+ };
20552
+ }
20553
+ function validateEpicPlanSidecar(parsed) {
20554
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
20555
+ return { ok: false, error: "Plan sidecar must be a JSON object." };
20556
+ }
20557
+ const plan = parsed;
20558
+ const version = plan.plan_version;
20559
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
20560
+ return {
20561
+ ok: false,
20562
+ error: `plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`
20563
+ };
20564
+ }
20565
+ if (!Array.isArray(plan.nodes) || plan.nodes.length === 0) {
20566
+ return { ok: false, error: "plan.nodes must be a non-empty array." };
20567
+ }
20568
+ if (!Array.isArray(plan.edges)) {
20569
+ return { ok: false, error: "plan.edges must be an array (use [] for none)." };
20570
+ }
20571
+ const keys = /* @__PURE__ */ new Set();
20572
+ const warnings = [];
20573
+ for (const node of plan.nodes) {
20574
+ if (!node || typeof node !== "object") {
20575
+ return { ok: false, error: "Every plan node must be an object." };
20576
+ }
20577
+ const key = typeof node.ticket_key === "string" ? node.ticket_key.trim() : "";
20578
+ if (!key) return { ok: false, error: "Every plan node needs a non-empty ticket_key." };
20579
+ if (keys.has(key)) return { ok: false, error: `Duplicate ticket_key in plan: ${key}.` };
20580
+ keys.add(key);
20581
+ if (node.touched_files === void 0) {
20582
+ warnings.push(
20583
+ `Node ${key} has no touched_files. File-overlap serialization cannot protect it; if the repo has that flag on, the server will reject this plan.`
20584
+ );
20585
+ }
20586
+ }
20587
+ const adjacency = /* @__PURE__ */ new Map();
20588
+ const addEdge = (from, to) => {
20589
+ const list = adjacency.get(from) ?? [];
20590
+ list.push(to);
20591
+ adjacency.set(from, list);
20592
+ };
20593
+ for (const node of plan.nodes) {
20594
+ const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
20595
+ for (const dep of deps) {
20596
+ if (!keys.has(dep)) {
20597
+ return {
20598
+ ok: false,
20599
+ error: `Node ${node.ticket_key} depends_on unknown ticket '${dep}'.`
20600
+ };
20601
+ }
20602
+ addEdge(dep, node.ticket_key);
20603
+ }
20604
+ }
20605
+ for (const edge of plan.edges) {
20606
+ if (!edge || typeof edge !== "object") {
20607
+ return { ok: false, error: "Every plan edge must be an object." };
20608
+ }
20609
+ if (!keys.has(edge.from) || !keys.has(edge.to)) {
20610
+ return {
20611
+ ok: false,
20612
+ error: `Edge ${JSON.stringify(edge.from)} -> ${JSON.stringify(edge.to)} references an unknown ticket.`
20613
+ };
20614
+ }
20615
+ addEdge(edge.from, edge.to);
20616
+ }
20617
+ const cycle = findCycle(keys, adjacency);
20618
+ if (cycle) {
20619
+ return { ok: false, error: `Plan DAG has a cycle: ${cycle.join(" -> ")}.` };
20620
+ }
20621
+ return { ok: true, plan: parsed, warnings };
20622
+ }
20623
+ function findCycle(keys, adjacency) {
20624
+ const WHITE = 0;
20625
+ const GREY = 1;
20626
+ const BLACK = 2;
20627
+ const color = /* @__PURE__ */ new Map();
20628
+ for (const k of keys) color.set(k, WHITE);
20629
+ for (const start of keys) {
20630
+ if (color.get(start) !== WHITE) continue;
20631
+ const stack = [{ node: start, path: [start] }];
20632
+ while (stack.length > 0) {
20633
+ const { node, path: path34 } = stack[stack.length - 1];
20634
+ if (color.get(node) === WHITE) {
20635
+ color.set(node, GREY);
20636
+ for (const next of adjacency.get(node) ?? []) {
20637
+ if (color.get(next) === GREY) return [...path34, next];
20638
+ if (color.get(next) === WHITE) {
20639
+ stack.push({ node: next, path: [...path34, next] });
20640
+ }
20641
+ }
20642
+ } else {
20643
+ if (color.get(node) === GREY) color.set(node, BLACK);
20644
+ stack.pop();
20645
+ }
20646
+ }
20647
+ }
20648
+ return null;
20649
+ }
20650
+ function errorDetail(err) {
20651
+ if (err instanceof ConductorBridgeApiError) {
20652
+ const status = err.status !== void 0 ? ` (HTTP ${err.status})` : "";
20653
+ const preview = err.bodyPreview ? `: ${err.bodyPreview}` : "";
20654
+ return `${err.message}${status}${preview}`;
20655
+ }
20656
+ return err instanceof Error ? err.message : String(err);
20657
+ }
20658
+ async function runSetupEpicCli(argv, overrides = {}) {
20659
+ const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
20660
+ const parsed = parseSetupEpicArgs(argv);
20661
+ if (parsed.status === "help") {
20662
+ deps.log(parsed.usage);
20663
+ return 0;
20664
+ }
20665
+ if (parsed.status === "error") {
20666
+ deps.errorLog(parsed.message);
20667
+ deps.errorLog("");
20668
+ deps.errorLog(getSetupEpicUsage());
20669
+ return 1;
20670
+ }
20671
+ const opts = parsed.options;
20672
+ const say = opts.json ? deps.errorLog : deps.log;
20673
+ let raw;
20674
+ try {
20675
+ raw = await deps.readFile(opts.planFile);
20676
+ } catch (err) {
20677
+ deps.errorLog(`Could not read plan file '${opts.planFile}': ${errorDetail(err)}`);
20678
+ return 1;
20679
+ }
20680
+ let parsedJson;
20681
+ try {
20682
+ parsedJson = JSON.parse(raw);
20683
+ } catch (err) {
20684
+ deps.errorLog(`Plan file '${opts.planFile}' is not valid JSON: ${errorDetail(err)}`);
20685
+ return 1;
20686
+ }
20687
+ const validation = validateEpicPlanSidecar(parsedJson);
20688
+ if (!validation.ok) {
20689
+ deps.errorLog(`Invalid plan DAG: ${validation.error}`);
20690
+ return 1;
20691
+ }
20692
+ const plan = validation.plan;
20693
+ const warnings = [...validation.warnings];
20694
+ if (opts.planVersion !== void 0 && opts.planVersion !== plan.plan_version) {
20695
+ deps.errorLog(
20696
+ `--plan-version ${opts.planVersion} does not match the sidecar's plan_version ${plan.plan_version}. Fix the sidecar (or drop the flag) \u2014 setup-epic never rewrites the blob, because that would change its hash.`
20697
+ );
20698
+ return 1;
20699
+ }
20700
+ const localHash = hashPlan(plan);
20701
+ const accessResult = await resolveConductorBridgeApiAccess({
20702
+ env: deps.env,
20703
+ cwd: deps.cwd,
20704
+ homedir: deps.homedir,
20705
+ platform: deps.platform,
20706
+ readFile: deps.readFile,
20707
+ stat: deps.stat,
20708
+ repoName: opts.repo
20709
+ });
20710
+ if (!accessResult.ok) {
20711
+ deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
20712
+ return 1;
20713
+ }
20714
+ const access2 = accessResult.access;
20715
+ say(`Epic: ${opts.epicKey}`);
20716
+ say(`Repo: ${access2.repoName}`);
20717
+ say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
20718
+ say(`Local hash: ${localHash}`);
20719
+ for (const w of warnings) say(` [warn] ${w}`);
20720
+ let existingRunId = null;
20721
+ let existingStatus = null;
20722
+ try {
20723
+ const state = await fetchEpicRunState(access2, opts.epicKey, deps.fetch);
20724
+ existingRunId = state.epic_run?.epic_run_id ?? null;
20725
+ existingStatus = state.epic_run?.status ?? null;
20726
+ } catch (err) {
20727
+ if (err instanceof ConductorBridgeApiError && err.status === 404) {
20728
+ existingRunId = null;
20729
+ } else if (err instanceof ConductorBridgeApiError && err.status === 409) {
20730
+ deps.errorLog(
20731
+ `Epic ${opts.epicKey} has MULTIPLE active runs \u2014 it is wedged, and every plan call will keep failing. Abandon the duplicate before retrying:
20732
+ PATCH /jira/epic-runs/runs/<epic_run_id> {"status": "abandoned"}
20733
+ Detail: ${errorDetail(err)}`
20734
+ );
20735
+ return 1;
20736
+ } else {
20737
+ deps.errorLog(
20738
+ `Could not read the epic run state, so creating one would risk a duplicate (which wedges the epic permanently). Refusing to continue.
20739
+ Detail: ${errorDetail(err)}`
20740
+ );
20741
+ return 1;
20742
+ }
20743
+ }
20744
+ if (opts.dryRun) {
20745
+ say("");
20746
+ say("[dry-run] No changes made. Would:");
20747
+ if (existingRunId) {
20748
+ say(` - reuse existing run ${existingRunId} (status: ${existingStatus})`);
20749
+ } else {
20750
+ say(` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`);
20751
+ }
20752
+ say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`);
20753
+ say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`);
20754
+ if (opts.json) {
20755
+ deps.log(
20756
+ JSON.stringify(
20757
+ {
20758
+ dry_run: true,
20759
+ epic_key: opts.epicKey,
20760
+ repo_name: access2.repoName,
20761
+ plan_version: plan.plan_version,
20762
+ local_plan_hash: localHash,
20763
+ existing_run_id: existingRunId,
20764
+ warnings
20765
+ },
20766
+ null,
20767
+ 2
20768
+ )
20769
+ );
20770
+ }
20771
+ return 0;
20772
+ }
20773
+ const result = {
20774
+ epic_run_id: existingRunId ?? "",
20775
+ epic_key: opts.epicKey,
20776
+ repo_name: access2.repoName,
20777
+ plan_version: plan.plan_version,
20778
+ plan_hash: null,
20779
+ local_plan_hash: localHash,
20780
+ status: existingStatus,
20781
+ run_created: false,
20782
+ plan_stored: false,
20783
+ plan_approved: false,
20784
+ warnings
20785
+ };
20786
+ if (existingRunId) {
20787
+ say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);
20788
+ } else {
20789
+ try {
20790
+ const run = await createEpicRun(access2, { epicKey: opts.epicKey }, deps.fetch);
20791
+ result.epic_run_id = run.epic_run_id;
20792
+ result.status = run.status;
20793
+ result.run_created = true;
20794
+ say(`Run: created ${run.epic_run_id}`);
20795
+ } catch (err) {
20796
+ deps.errorLog(`Failed to create the epic run: ${errorDetail(err)}`);
20797
+ return 1;
20798
+ }
20799
+ }
20800
+ try {
20801
+ const stored = await storeEpicPlan(
20802
+ access2,
20803
+ {
20804
+ epicKey: opts.epicKey,
20805
+ planVersion: plan.plan_version,
20806
+ planBlob: plan,
20807
+ planHash: localHash
20808
+ },
20809
+ deps.fetch
20810
+ );
20811
+ result.plan_stored = true;
20812
+ const serverHash = stored?.plan_hash;
20813
+ if (typeof serverHash === "string") result.plan_hash = serverHash;
20814
+ say(`Plan: stored v${plan.plan_version}`);
20815
+ } catch (err) {
20816
+ if (err instanceof ConductorBridgeApiError && err.status === 409) {
20817
+ deps.errorLog(
20818
+ `Plan v${plan.plan_version} is already stored with a DIFFERENT hash. The stored blob is immutable \u2014 bump plan_version in the sidecar and re-run.
20819
+ Detail: ${errorDetail(err)}`
20820
+ );
20821
+ return 1;
20822
+ }
20823
+ deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`);
20824
+ return 1;
19673
20825
  }
19674
- const createHttpClient = overrides.createHttpClient ?? createExecutorHttpClient;
19675
- const httpClient = createHttpClient({
19676
- baseUrl,
19677
- apiKey: apiKeyByRepo[options.repoName],
19678
- apiKeyByRepo,
19679
- mcpVersion: VERSION,
19680
- fetch: deps.fetch
20826
+ const approval = await approveEpicPlan(
20827
+ access2,
20828
+ { epicKey: opts.epicKey, planVersion: plan.plan_version },
20829
+ deps.fetch
20830
+ ).catch((err) => {
20831
+ deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`);
20832
+ return null;
19681
20833
  });
19682
- const run = overrides.runExecutor ?? runExecutor;
19683
- try {
19684
- return await run(options, deps, httpClient);
19685
- } catch (err) {
19686
- const message = err instanceof Error ? err.message : String(err);
19687
- errorLog(`Error: executor exited unexpectedly: ${message.slice(0, 200)}`);
20834
+ if (approval === null) return 1;
20835
+ if (approval.ok) {
20836
+ result.plan_approved = true;
20837
+ result.plan_hash = approval.plan_hash;
20838
+ result.status = "active";
20839
+ say(`Plan: approved v${plan.plan_version}`);
20840
+ } else if (approval.reason === "multiple_active_runs") {
20841
+ deps.errorLog(
20842
+ `Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`
20843
+ );
19688
20844
  return 1;
20845
+ } else {
20846
+ const msg = `A later plan version is already approved \u2014 approval skipped.`;
20847
+ result.warnings.push(msg);
20848
+ say(`Plan: [warn] ${msg}`);
20849
+ }
20850
+ if (result.plan_hash && result.plan_hash !== localHash) {
20851
+ result.warnings.push(
20852
+ `Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative.`
20853
+ );
20854
+ }
20855
+ if (opts.json) {
20856
+ deps.log(JSON.stringify(result, null, 2));
20857
+ } else {
20858
+ say("");
20859
+ say(`Epic run ${result.epic_run_id} is ${result.status ?? "unknown"}.`);
20860
+ say("The server-side reconciler will pick it up within ~30s.");
20861
+ say("To execute claimed jobs on this machine, run:");
20862
+ say(` npx -y @bridge_gpt/mcp-server executor --repo ${access2.repoName}`);
19689
20863
  }
20864
+ return 0;
19690
20865
  }
19691
20866
 
19692
20867
  // src/regression-check.ts
@@ -19836,8 +21011,8 @@ function extractChangedSymbolsFromDiff(diffText) {
19836
21011
  let newLineNo = 0;
19837
21012
  for (const rawLine of diffText.split("\n")) {
19838
21013
  if (rawLine.startsWith("+++ ")) {
19839
- const path33 = rawLine.slice(4).trim();
19840
- currentFile = path33 === "/dev/null" ? null : path33.replace(/^b\//, "");
21014
+ const path34 = rawLine.slice(4).trim();
21015
+ currentFile = path34 === "/dev/null" ? null : path34.replace(/^b\//, "");
19841
21016
  continue;
19842
21017
  }
19843
21018
  if (rawLine.startsWith("--- ") || rawLine.startsWith("diff --git") || rawLine.startsWith("index ")) {
@@ -20396,10 +21571,11 @@ async function runRegressionCheckCli(argv, overrides = {}) {
20396
21571
  }
20397
21572
 
20398
21573
  // src/install-bridge.ts
20399
- import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7, stat as stat7, rename, chmod, unlink } from "fs/promises";
21574
+ import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7, stat as stat7, rename, chmod, unlink, open } from "fs/promises";
20400
21575
  import { spawn as spawn6 } from "child_process";
20401
- import os12 from "os";
20402
- import path22 from "path";
21576
+ import { randomBytes as cryptoRandomBytes, createHash as createHash3 } from "crypto";
21577
+ import os13 from "os";
21578
+ import path23 from "path";
20403
21579
  import readline from "readline";
20404
21580
  init_version_generated();
20405
21581
  init_bridge_config();
@@ -20432,26 +21608,52 @@ function getInstallBridgeUsage() {
20432
21608
  " --api-key <key> Bridge API key. Falls back to the BAPI_API_KEY env var,",
20433
21609
  " then an interactive (no-echo) prompt. Generate one in the",
20434
21610
  " Bridge API web UI Security page \u2014 this command consumes a",
20435
- " key, it does not create one. NEVER printed or logged.",
21611
+ " key, it does not create one (--invite is the one exception:",
21612
+ " it CREATES the project and its first admin key). NEVER",
21613
+ " printed or logged.",
20436
21614
  " --repo <name> Repository name. Falls back to BAPI_REPO_NAME, then an",
20437
21615
  " inferred default you confirm interactively. MUST match the",
20438
21616
  " server-side repo registration (it keys the credential",
20439
21617
  " store as bapi:<repo>). Required (no inference) when stdin",
20440
- " is non-interactive.",
21618
+ " is non-interactive. With --invite it is the name your NEW",
21619
+ " project is created under (globally unique).",
21620
+ "",
21621
+ "Bootstrap-invite onboarding (no web UI, no pre-existing key):",
21622
+ " --invite [token] Redeem a bootstrap invite: creates the project and mints",
21623
+ " your own admin API key in one command. Mutually exclusive",
21624
+ " with --api-key (in this mode the key is created, not",
21625
+ " consumed).",
21626
+ "",
21627
+ " Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the",
21628
+ " token is read from an interactive prompt with echo",
21629
+ " suppressed, then sent only in the request body. This is the",
21630
+ " default and the recommended path: 'a copy/paste one-liner'",
21631
+ " and 'the token never touches shell history' are",
21632
+ " contradictory, so the one-liner your operator sends you is",
21633
+ " SECRET-FREE and the CLI asks for the token.",
21634
+ "",
21635
+ " Passing the token inline (--invite <token>, --invite=<token>)",
21636
+ " or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE",
21637
+ " THE TOKEN to your shell history and to the process list.",
20441
21638
  "",
20442
21639
  "Flags:",
20443
21640
  " --force Overwrite an existing real BAPI_API_KEY in a",
20444
- " host config without prompting.",
21641
+ " host config (or in the credential store) without",
21642
+ " prompting.",
20445
21643
  " --dry-run Preview every step (scaffold targets, config",
20446
21644
  " files + keys with the key REDACTED, ping",
20447
21645
  " target, credential target, spawn command)",
20448
21646
  " without writing, pinging, or spawning anything.",
21647
+ " With --invite it also never calls the exchange",
21648
+ " endpoint and never generates or stores a secret.",
20449
21649
  " --agent claude|cursor-agent Agent to launch for the agentic remainder",
20450
21650
  " (default: claude).",
20451
21651
  " -h, --help Show this help.",
20452
21652
  "",
20453
21653
  "Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
20454
- "(default docs/tmp) are read from the environment with the shown fallbacks."
21654
+ "(default docs/tmp) are read from the environment with the shown fallbacks.",
21655
+ "BAPI_INVITE supplies the bootstrap-invite token non-interactively (scripting",
21656
+ "only \u2014 it is exposed to shell history; prefer the prompt)."
20455
21657
  ].join("\n");
20456
21658
  }
20457
21659
  function parseInstallBridgeArgs(argv) {
@@ -20463,6 +21665,9 @@ function parseInstallBridgeArgs(argv) {
20463
21665
  let force = false;
20464
21666
  let dryRun = false;
20465
21667
  let agentName = DEFAULT_AGENT_NAME;
21668
+ let invite;
21669
+ let inviteSupplied = false;
21670
+ let apiKeySupplied = false;
20466
21671
  const readValue = (arg, flag, i) => {
20467
21672
  if (arg.startsWith(`${flag}=`)) {
20468
21673
  return { value: arg.slice(flag.length + 1), nextIndex: i };
@@ -20486,9 +21691,23 @@ function parseInstallBridgeArgs(argv) {
20486
21691
  const r = readValue(arg, "--api-key", i);
20487
21692
  if ("error" in r) return { status: "error", message: r.error };
20488
21693
  apiKey = r.value;
21694
+ apiKeySupplied = true;
20489
21695
  i = r.nextIndex;
20490
21696
  continue;
20491
21697
  }
21698
+ if (arg === "--invite" || arg.startsWith("--invite=")) {
21699
+ inviteSupplied = true;
21700
+ if (arg.startsWith("--invite=")) {
21701
+ invite = arg.slice("--invite=".length);
21702
+ } else {
21703
+ const next = argv[i + 1];
21704
+ if (typeof next === "string" && !next.startsWith("-")) {
21705
+ invite = next;
21706
+ i += 1;
21707
+ }
21708
+ }
21709
+ continue;
21710
+ }
20492
21711
  if (arg === "--repo" || arg.startsWith("--repo=")) {
20493
21712
  const r = readValue(arg, "--repo", i);
20494
21713
  if ("error" in r) return { status: "error", message: r.error };
@@ -20517,33 +21736,55 @@ function parseInstallBridgeArgs(argv) {
20517
21736
  message: `Unexpected positional argument: '${arg}'. install-bridge does not accept positional arguments.`
20518
21737
  };
20519
21738
  }
20520
- return { status: "ok", options: { apiKey, repo, force, dryRun, agentName } };
21739
+ if (inviteSupplied && apiKeySupplied) {
21740
+ return {
21741
+ status: "error",
21742
+ message: "--invite and --api-key are mutually exclusive: a bootstrap invite creates your API key, it does not consume an existing one."
21743
+ };
21744
+ }
21745
+ return {
21746
+ status: "ok",
21747
+ options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied }
21748
+ };
20521
21749
  }
20522
- function promptSecretViaReadline(promptText) {
21750
+ function promptSecretViaReadline(promptText, input = process.stdin, output = process.stderr) {
20523
21751
  return new Promise((resolve2) => {
20524
21752
  const rl = readline.createInterface({
20525
- input: process.stdin,
20526
- output: process.stderr,
21753
+ input,
21754
+ output,
20527
21755
  terminal: true
20528
21756
  });
20529
21757
  const mutable = rl;
20530
21758
  let muted = false;
20531
21759
  mutable._writeToOutput = (s) => {
20532
- if (!muted) process.stderr.write(s);
21760
+ if (!muted) {
21761
+ output.write(s);
21762
+ } else if (s.includes(promptText)) {
21763
+ output.write(promptText);
21764
+ }
20533
21765
  };
20534
- process.stderr.write(promptText);
20535
- muted = true;
20536
- rl.question("", (answer) => {
21766
+ let answered = false;
21767
+ rl.on("close", () => {
21768
+ if (!answered) resolve2("");
21769
+ });
21770
+ rl.question(promptText, (answer) => {
21771
+ answered = true;
20537
21772
  rl.close();
20538
- process.stderr.write("\n");
21773
+ output.write("\n");
20539
21774
  resolve2(answer.trim());
20540
21775
  });
21776
+ muted = true;
20541
21777
  });
20542
21778
  }
20543
21779
  function promptLineViaReadline(promptText) {
20544
21780
  return new Promise((resolve2) => {
20545
21781
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
21782
+ let answered = false;
21783
+ rl.on("close", () => {
21784
+ if (!answered) resolve2("");
21785
+ });
20546
21786
  rl.question(promptText, (answer) => {
21787
+ answered = true;
20547
21788
  rl.close();
20548
21789
  resolve2(answer.trim());
20549
21790
  });
@@ -20553,6 +21794,7 @@ function spawnPrewarmDefault(command, args, env) {
20553
21794
  return new Promise((resolve2) => {
20554
21795
  const sanitizedEnv = { ...env };
20555
21796
  delete sanitizedEnv.BAPI_API_KEY;
21797
+ delete sanitizedEnv.BAPI_INVITE;
20556
21798
  try {
20557
21799
  const child = spawn6(command, args, {
20558
21800
  shell: false,
@@ -20581,7 +21823,7 @@ function createDefaultInstallBridgeDeps() {
20581
21823
  env: process.env,
20582
21824
  cwd: process.cwd(),
20583
21825
  platform: process.platform,
20584
- homedir: os12.homedir,
21826
+ homedir: os13.homedir,
20585
21827
  isTTY,
20586
21828
  readFile: (p) => readFile10(p, "utf-8"),
20587
21829
  writeFile: (p, data, options) => writeFile7(p, data, options),
@@ -20590,12 +21832,24 @@ function createDefaultInstallBridgeDeps() {
20590
21832
  rename: (a, b) => rename(a, b),
20591
21833
  chmod: (p, m) => chmod(p, m),
20592
21834
  unlink: (p) => unlink(p),
21835
+ open: async (p, flags, mode) => {
21836
+ const handle = await open(p, flags, mode);
21837
+ return {
21838
+ writeFile: (data) => handle.writeFile(data, { encoding: "utf-8" }),
21839
+ sync: () => handle.sync(),
21840
+ close: () => handle.close()
21841
+ };
21842
+ },
21843
+ randomBytes: (size) => cryptoRandomBytes(size),
20593
21844
  promptSecret: isTTY ? promptSecretViaReadline : void 0,
20594
21845
  promptLine: isTTY ? promptLineViaReadline : void 0,
20595
21846
  fetch: (...args) => fetch(...args),
20596
21847
  spawnPrewarm: spawnPrewarmDefault,
20597
21848
  runInit,
20598
21849
  upsertCredential: upsertBapiCredential,
21850
+ prepareBootstrapPending: prepareBootstrapPendingCredential,
21851
+ repointBootstrapPending: repointBootstrapPendingCredential,
21852
+ promoteBootstrapPending: promoteBootstrapPendingCredential,
20599
21853
  buildShellCommand: buildGenericAgentShellCommand,
20600
21854
  spawnTerminalTab: getDefaultSpawnTerminalTabForPlatform(process.platform),
20601
21855
  startTicketsDeps: createDefaultStartTicketsDeps(),
@@ -20623,6 +21877,26 @@ async function resolveApiKey(options, deps) {
20623
21877
  error: "An API key is required. Pass --api-key or set the BAPI_API_KEY environment variable (no interactive terminal is available to prompt for it)."
20624
21878
  };
20625
21879
  }
21880
+ async function resolveInviteToken(options, deps) {
21881
+ if (typeof options.invite === "string" && options.invite.trim().length > 0) {
21882
+ return { ok: true, value: options.invite.trim() };
21883
+ }
21884
+ const fromEnv = deps.env.BAPI_INVITE;
21885
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
21886
+ return { ok: true, value: fromEnv.trim() };
21887
+ }
21888
+ if (deps.isTTY && deps.promptSecret) {
21889
+ const entered = (await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();
21890
+ if (entered.length > 0) {
21891
+ return { ok: true, value: entered };
21892
+ }
21893
+ return { ok: false, error: "No bootstrap invite token entered." };
21894
+ }
21895
+ return {
21896
+ ok: false,
21897
+ error: "A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE environment variable (no interactive terminal is available to prompt for it). Note that both forms expose the token to your shell history and process list \u2014 prefer running 'install-bridge --invite' interactively."
21898
+ };
21899
+ }
20626
21900
  async function resolveRepoName(options, deps) {
20627
21901
  if (typeof options.repo === "string" && options.repo.trim().length > 0) {
20628
21902
  return { ok: true, value: options.repo.trim() };
@@ -20643,7 +21917,7 @@ async function resolveRepoName(options, deps) {
20643
21917
  readFile: deps.readFile
20644
21918
  });
20645
21919
  if (!inferred) {
20646
- const validated = validateRepoName(path22.basename(deps.cwd));
21920
+ const validated = validateRepoName(path23.basename(deps.cwd));
20647
21921
  if (validated.ok) inferred = validated.value;
20648
21922
  }
20649
21923
  if (inferred) {
@@ -20662,7 +21936,7 @@ async function resolveHostConfigTargets(deps) {
20662
21936
  ];
20663
21937
  const dirExists = async (rel) => {
20664
21938
  try {
20665
- await deps.stat(path22.join(deps.cwd, rel));
21939
+ await deps.stat(path23.join(deps.cwd, rel));
20666
21940
  return true;
20667
21941
  } catch {
20668
21942
  return false;
@@ -20709,7 +21983,7 @@ async function readHostConfig(deps, fullPath) {
20709
21983
  }
20710
21984
  async function detectExistingRealKey(deps, targets) {
20711
21985
  for (const target of targets) {
20712
- const parsed = await readHostConfig(deps, path22.join(deps.cwd, target.relPath));
21986
+ const parsed = await readHostConfig(deps, path23.join(deps.cwd, target.relPath));
20713
21987
  const entry = parsed?.[target.topLevelKey]?.["bridge-api"];
20714
21988
  if (entry?.env && !isPlaceholderApiKey(entry.env.BAPI_API_KEY)) {
20715
21989
  return true;
@@ -20720,13 +21994,13 @@ async function detectExistingRealKey(deps, targets) {
20720
21994
  async function writeHostConfigs(deps, targets, entry) {
20721
21995
  const written = [];
20722
21996
  for (const target of targets) {
20723
- const fullPath = path22.join(deps.cwd, target.relPath);
21997
+ const fullPath = path23.join(deps.cwd, target.relPath);
20724
21998
  const parsed = await readHostConfig(deps, fullPath) ?? {};
20725
21999
  if (!parsed[target.topLevelKey] || typeof parsed[target.topLevelKey] !== "object") {
20726
22000
  parsed[target.topLevelKey] = {};
20727
22001
  }
20728
22002
  parsed[target.topLevelKey]["bridge-api"] = entry;
20729
- await deps.mkdir(path22.dirname(fullPath), { recursive: true });
22003
+ await deps.mkdir(path23.dirname(fullPath), { recursive: true });
20730
22004
  await deps.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", {
20731
22005
  encoding: "utf-8"
20732
22006
  });
@@ -20772,7 +22046,91 @@ async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
20772
22046
  message: `Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`
20773
22047
  };
20774
22048
  }
22049
+ var BOOTSTRAP_KEY_SECRET_BYTES = 32;
22050
+ function generateBootstrapKeySecret(randomBytes3) {
22051
+ return randomBytes3(BOOTSTRAP_KEY_SECRET_BYTES).toString("base64url");
22052
+ }
22053
+ function fingerprintBootstrapInvite(token) {
22054
+ return createHash3("sha256").update(token, "utf-8").digest("hex");
22055
+ }
22056
+ function buildBootstrapExchangeUrl(baseUrl) {
22057
+ return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap`;
22058
+ }
22059
+ async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, keySecret) {
22060
+ const url = buildBootstrapExchangeUrl(baseUrl);
22061
+ let resp;
22062
+ try {
22063
+ resp = await deps.fetch(url, {
22064
+ method: "POST",
22065
+ headers: { "Content-Type": "application/json" },
22066
+ body: JSON.stringify({ token, repo_name: repoName, key_secret: keySecret }),
22067
+ signal: AbortSignal.timeout(1e4)
22068
+ });
22069
+ } catch (err) {
22070
+ void err;
22071
+ return {
22072
+ ok: false,
22073
+ kind: "failed",
22074
+ message: `Could not reach the Bridge API at ${baseUrl} to redeem the bootstrap invite. Check BAPI_BASE_URL and your network, then re-run \u2014 the invite has not been used, and the re-run will reuse the same locally-stored secret.`
22075
+ };
22076
+ }
22077
+ if (resp.ok) {
22078
+ let repo;
22079
+ try {
22080
+ const body = await resp.json();
22081
+ repo = body?.repo_name;
22082
+ } catch {
22083
+ return {
22084
+ ok: false,
22085
+ kind: "failed",
22086
+ message: "The Bridge API returned an unreadable response to the bootstrap exchange."
22087
+ };
22088
+ }
22089
+ const validated = validateRepoName(repo);
22090
+ if (!validated.ok) {
22091
+ return {
22092
+ ok: false,
22093
+ kind: "failed",
22094
+ message: "The Bridge API returned an unexpected repo name for the bootstrap exchange."
22095
+ };
22096
+ }
22097
+ return { ok: true, repoName: validated.value };
22098
+ }
22099
+ if (resp.status === 409) {
22100
+ return {
22101
+ ok: false,
22102
+ kind: "repo-name-taken",
22103
+ message: `The repo name '${repoName}' is already taken (HTTP 409). Repo names are globally unique.`
22104
+ };
22105
+ }
22106
+ if (resp.status === 401) {
22107
+ return { ok: false, kind: "invalid-invite", message: `The Bridge API rejected the bootstrap invite (HTTP ${resp.status}).` };
22108
+ }
22109
+ return {
22110
+ ok: false,
22111
+ kind: "failed",
22112
+ message: `The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`
22113
+ };
22114
+ }
22115
+ var BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE = [
22116
+ "The Bridge API rejected the bootstrap invite (HTTP 401).",
22117
+ "",
22118
+ "Either the invite is invalid, expired, or revoked \u2014 or it was ALREADY redeemed from this",
22119
+ "machine and the local secret has since been lost (e.g. ~/.config/bridge was deleted).",
22120
+ "",
22121
+ "If it was already redeemed, you cannot recover it yourself:",
22122
+ " \u2022 Re-running will NOT work: each run without the original local secret sends a new one,",
22123
+ " which cannot match what the server stored, so it will keep returning 401.",
22124
+ " \u2022 A new bootstrap invite will NOT work either: your repo name is globally unique and is",
22125
+ " now taken by the project you already created, so it cannot be redeemed again.",
22126
+ "",
22127
+ "Ask your Bridge API operator to recover it for you: they revoke the orphaned key",
22128
+ "(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project",
22129
+ "(POST /setup/keys), then send you that key. Run install-bridge with --api-key <that key>."
22130
+ ].join("\n");
22131
+ var BOOTSTRAP_INVITE_REJECTED_MESSAGE = "The Bridge API rejected the bootstrap invite (HTTP 401). The invite is invalid, expired, or revoked \u2014 ask your Bridge API operator for a new one. (Your locally-stored secret was sent unchanged, so this is not a lost-secret problem.)";
20775
22132
  function buildDryRunPreview(plan) {
22133
+ if (plan.bootstrapInvite) return buildBootstrapDryRunPreview(plan);
20776
22134
  return [
20777
22135
  "install-bridge --dry-run (no writes, no network, no spawns)",
20778
22136
  `Repo name: ${plan.repoName}`,
@@ -20793,6 +22151,33 @@ function buildDryRunPreview(plan) {
20793
22151
  `Step 5 \u2014 spawn agent session: ${plan.spawnCommand}`
20794
22152
  ];
20795
22153
  }
22154
+ function buildBootstrapDryRunPreview(plan) {
22155
+ const pendingTarget = `bootstrap-pending:${plan.repoName}`;
22156
+ return [
22157
+ "install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)",
22158
+ `Repo name: ${plan.repoName} (created by the exchange; globally unique)`,
22159
+ `Base URL: ${plan.baseUrl}`,
22160
+ `Docs dir: ${plan.docsDir}`,
22161
+ `Agent: ${plan.agentName}`,
22162
+ "",
22163
+ "Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
22164
+ `Step 2a \u2014 generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${plan.credentialStorePath}`,
22165
+ " BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.",
22166
+ `Step 2b \u2014 redeem the bootstrap invite (replaces the pre-flight ping \u2014 there is no key yet):`,
22167
+ ` POST ${plan.exchangeUrl}`,
22168
+ ` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${plan.repoName}", "key_secret": "${REDACTED_API_KEY}"}`,
22169
+ `Step 2c \u2014 connectivity ping with the newly-minted key (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,
22170
+ "Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned):",
22171
+ ...plan.configTargets.map(
22172
+ (t) => ` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`
22173
+ ),
22174
+ ...plan.manualEditors.length > 0 ? [` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`] : [],
22175
+ `Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
22176
+ MCP_TIMEOUT_GUIDANCE,
22177
+ `Step 4 \u2014 promote ${pendingTarget} \u2192 ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
22178
+ `Step 5 \u2014 spawn agent session: ${plan.spawnCommand}`
22179
+ ];
22180
+ }
20796
22181
  async function detectManualEditors(deps) {
20797
22182
  const exists = async (p) => {
20798
22183
  try {
@@ -20802,8 +22187,8 @@ async function detectManualEditors(deps) {
20802
22187
  return false;
20803
22188
  }
20804
22189
  };
20805
- const windsurf = await exists(path22.join(deps.cwd, ".windsurf")) || await exists(path22.join(deps.cwd, ".windsurfrules"));
20806
- const codex = await exists(path22.join(deps.homedir(), ".codex"));
22190
+ const windsurf = await exists(path23.join(deps.cwd, ".windsurf")) || await exists(path23.join(deps.cwd, ".windsurfrules"));
22191
+ const codex = await exists(path23.join(deps.homedir(), ".codex"));
20807
22192
  return { windsurf, codex };
20808
22193
  }
20809
22194
  function manualEditorNames(editors) {
@@ -20852,18 +22237,38 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20852
22237
  return 1;
20853
22238
  }
20854
22239
  const options = parsed.options;
20855
- const keyResult = await resolveApiKey(options, deps);
20856
- if (!keyResult.ok) {
20857
- errorLog(`Error: ${keyResult.error}`);
20858
- return 1;
22240
+ const bootstrapInviteMode = options.inviteMode === true || (deps.env.BAPI_INVITE ?? "").trim().length > 0;
22241
+ let apiKey = "";
22242
+ let inviteToken = "";
22243
+ if (bootstrapInviteMode) {
22244
+ const inviteResult = await resolveInviteToken(options, deps);
22245
+ if (!inviteResult.ok) {
22246
+ errorLog(`Error: ${inviteResult.error}`);
22247
+ return 1;
22248
+ }
22249
+ inviteToken = inviteResult.value;
22250
+ } else {
22251
+ const keyResult = await resolveApiKey(options, deps);
22252
+ if (!keyResult.ok) {
22253
+ errorLog(`Error: ${keyResult.error}`);
22254
+ return 1;
22255
+ }
22256
+ apiKey = keyResult.value;
20859
22257
  }
20860
- const apiKey = keyResult.value;
20861
22258
  const repoResult = await resolveRepoName(options, deps);
20862
22259
  if (!repoResult.ok) {
20863
22260
  errorLog(`Error: ${repoResult.error}`);
20864
22261
  return 1;
20865
22262
  }
20866
- const repoName = repoResult.value;
22263
+ let repoName = repoResult.value;
22264
+ if (bootstrapInviteMode) {
22265
+ const validated = validateRepoName(repoName);
22266
+ if (!validated.ok) {
22267
+ errorLog(`Error: invalid repo name \u2014 ${validated.error}.`);
22268
+ return 1;
22269
+ }
22270
+ repoName = validated.value;
22271
+ }
20867
22272
  const baseUrl = deps.env.BAPI_BASE_URL ?? DEFAULT_BAPI_BASE_URL2;
20868
22273
  const docsDir = deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR;
20869
22274
  const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
@@ -20890,14 +22295,27 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20890
22295
  credentialStorePath,
20891
22296
  pingUrl: buildPingUrl(baseUrl, repoName),
20892
22297
  prewarmCommand: buildPrewarmCommandPreview(),
20893
- spawnCommand
22298
+ spawnCommand,
22299
+ ...bootstrapInviteMode ? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) } : {}
20894
22300
  };
20895
22301
  if (options.dryRun) {
20896
22302
  for (const line of buildDryRunPreview(plan)) log(line);
20897
22303
  return 0;
20898
22304
  }
20899
- const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
22305
+ const credentialWriteDeps = {
22306
+ env: deps.env,
22307
+ homedir: deps.homedir,
22308
+ platform: deps.platform,
22309
+ readFile: deps.readFile,
22310
+ mkdir: deps.mkdir,
22311
+ writeFile: (p, d, o) => deps.writeFile(p, d, o),
22312
+ rename: deps.rename,
22313
+ chmod: deps.chmod,
22314
+ unlink: deps.unlink,
22315
+ open: deps.open
22316
+ };
20900
22317
  const hasRealKey = await detectExistingRealKey(deps, targets);
22318
+ let overwriteConsent = options.force;
20901
22319
  if (hasRealKey && !options.force) {
20902
22320
  if (deps.isTTY && deps.promptLine) {
20903
22321
  const answer = (await deps.promptLine(
@@ -20907,6 +22325,7 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20907
22325
  errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite).");
20908
22326
  return 1;
20909
22327
  }
22328
+ overwriteConsent = true;
20910
22329
  } else {
20911
22330
  errorLog(
20912
22331
  "Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."
@@ -20916,13 +22335,133 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20916
22335
  }
20917
22336
  log("Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026");
20918
22337
  await deps.runInit(deps.cwd);
20919
- log("Step 2/5 \u2014 verifying connectivity\u2026");
22338
+ let inviteFingerprint = "";
22339
+ if (bootstrapInviteMode) {
22340
+ inviteFingerprint = fingerprintBootstrapInvite(inviteToken);
22341
+ log("Step 2/5 \u2014 redeeming the bootstrap invite\u2026");
22342
+ let prepared = await deps.prepareBootstrapPending(
22343
+ {
22344
+ repoName,
22345
+ inviteFingerprint,
22346
+ generateKeySecret: () => generateBootstrapKeySecret(deps.randomBytes),
22347
+ allowOverwriteExistingCredential: overwriteConsent
22348
+ },
22349
+ credentialWriteDeps
22350
+ );
22351
+ if (!prepared.ok && prepared.kind === "credential-conflict") {
22352
+ if (deps.isTTY && deps.promptLine) {
22353
+ const answer = (await deps.promptLine(
22354
+ `The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `
22355
+ )).trim().toLowerCase();
22356
+ if (answer !== "y" && answer !== "yes") {
22357
+ errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite).");
22358
+ return 1;
22359
+ }
22360
+ overwriteConsent = true;
22361
+ prepared = await deps.prepareBootstrapPending(
22362
+ {
22363
+ repoName,
22364
+ inviteFingerprint,
22365
+ generateKeySecret: () => generateBootstrapKeySecret(deps.randomBytes),
22366
+ allowOverwriteExistingCredential: true
22367
+ },
22368
+ credentialWriteDeps
22369
+ );
22370
+ } else {
22371
+ errorLog(
22372
+ `Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`
22373
+ );
22374
+ return 1;
22375
+ }
22376
+ }
22377
+ if (!prepared.ok && prepared.kind === "pending-conflict") {
22378
+ errorLog(
22379
+ `Error: ${prepared.error} This invite has NOT been used, and re-running will not clear the conflict.`
22380
+ );
22381
+ return 1;
22382
+ }
22383
+ if (!prepared.ok) {
22384
+ errorLog(
22385
+ `Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`
22386
+ );
22387
+ return 1;
22388
+ }
22389
+ const keySecret = prepared.keySecret;
22390
+ const reusedPendingSecret = prepared.reused;
22391
+ log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`);
22392
+ let exchange = await exchangeBootstrapInvite(deps, baseUrl, inviteToken, repoName, keySecret);
22393
+ while (!exchange.ok && exchange.kind === "repo-name-taken") {
22394
+ if (!deps.isTTY || !deps.promptLine) {
22395
+ errorLog(
22396
+ `Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`
22397
+ );
22398
+ return 1;
22399
+ }
22400
+ errorLog(exchange.message);
22401
+ const answer = (await deps.promptLine("Choose a different repo name: ")).trim();
22402
+ const validated = validateRepoName(answer);
22403
+ if (!validated.ok) {
22404
+ errorLog(`Error: invalid repo name \u2014 ${validated.error}.`);
22405
+ return 1;
22406
+ }
22407
+ const nextRepo = validated.value;
22408
+ const repointed = await deps.repointBootstrapPending(
22409
+ {
22410
+ fromRepoName: repoName,
22411
+ toRepoName: nextRepo,
22412
+ inviteFingerprint,
22413
+ allowOverwriteExistingCredential: overwriteConsent
22414
+ },
22415
+ credentialWriteDeps
22416
+ );
22417
+ if (!repointed.ok) {
22418
+ errorLog(
22419
+ `Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`
22420
+ );
22421
+ return 1;
22422
+ }
22423
+ repoName = nextRepo;
22424
+ exchange = await exchangeBootstrapInvite(deps, baseUrl, inviteToken, repoName, keySecret);
22425
+ }
22426
+ if (!exchange.ok) {
22427
+ if (exchange.kind === "invalid-invite") {
22428
+ errorLog(
22429
+ reusedPendingSecret ? BOOTSTRAP_INVITE_REJECTED_MESSAGE : BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE
22430
+ );
22431
+ } else {
22432
+ errorLog(`Error: ${exchange.message}`);
22433
+ }
22434
+ return 1;
22435
+ }
22436
+ if (exchange.repoName !== repoName) {
22437
+ const repointed = await deps.repointBootstrapPending(
22438
+ {
22439
+ fromRepoName: repoName,
22440
+ toRepoName: exchange.repoName,
22441
+ inviteFingerprint,
22442
+ allowOverwriteExistingCredential: overwriteConsent
22443
+ },
22444
+ credentialWriteDeps
22445
+ );
22446
+ if (!repointed.ok) {
22447
+ errorLog(
22448
+ `Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`
22449
+ );
22450
+ return 1;
22451
+ }
22452
+ repoName = exchange.repoName;
22453
+ }
22454
+ log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`);
22455
+ apiKey = keySecret;
22456
+ }
22457
+ if (!bootstrapInviteMode) log("Step 2/5 \u2014 verifying connectivity\u2026");
20920
22458
  const ping = await verifyConnectivity(deps, baseUrl, repoName, apiKey);
20921
22459
  if (!ping.ok) {
20922
22460
  errorLog(`Error: ${ping.message}`);
20923
22461
  return 1;
20924
22462
  }
20925
22463
  log(" connectivity OK");
22464
+ const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
20926
22465
  log("Step 3/5 \u2014 writing per-host MCP config\u2026");
20927
22466
  const written = await writeHostConfigs(deps, targets, entry);
20928
22467
  for (const relPath of written) log(` wrote ${relPath}`);
@@ -20938,31 +22477,35 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20938
22477
  `Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`
20939
22478
  );
20940
22479
  }
20941
- log("Step 4/5 \u2014 persisting routing credential\u2026");
20942
- try {
20943
- const writeDeps = {
20944
- env: deps.env,
20945
- homedir: deps.homedir,
20946
- platform: deps.platform,
20947
- readFile: deps.readFile,
20948
- mkdir: deps.mkdir,
20949
- writeFile: (p, d, o) => deps.writeFile(p, d, o),
20950
- rename: deps.rename,
20951
- chmod: deps.chmod,
20952
- unlink: deps.unlink
20953
- };
20954
- const result = await deps.upsertCredential(repoName, apiKey, writeDeps);
20955
- if (result.ok) {
20956
- log(` stored routing credential for ${result.target} at ${result.path}`);
20957
- } else {
22480
+ if (bootstrapInviteMode) {
22481
+ log("Step 4/5 \u2014 promoting the bootstrap credential\u2026");
22482
+ const promoted = await deps.promoteBootstrapPending(
22483
+ { repoName, inviteFingerprint, allowOverwriteExistingCredential: overwriteConsent },
22484
+ credentialWriteDeps
22485
+ );
22486
+ if (!promoted.ok) {
22487
+ errorLog(
22488
+ `Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record \u2014 re-run install-bridge with the same bootstrap invite to finish (the redemption will replay and return the same key).`
22489
+ );
22490
+ return 1;
22491
+ }
22492
+ log(` stored routing credential for ${promoted.target} at ${promoted.path}`);
22493
+ } else {
22494
+ log("Step 4/5 \u2014 persisting routing credential\u2026");
22495
+ try {
22496
+ const result = await deps.upsertCredential(repoName, apiKey, credentialWriteDeps);
22497
+ if (result.ok) {
22498
+ log(` stored routing credential for ${result.target} at ${result.path}`);
22499
+ } else {
22500
+ log(
22501
+ ` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`
22502
+ );
22503
+ }
22504
+ } catch {
20958
22505
  log(
20959
- ` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for ${plan.credentialTarget} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`
22506
+ " warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'."
20960
22507
  );
20961
22508
  }
20962
- } catch {
20963
- log(
20964
- " warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'."
20965
- );
20966
22509
  }
20967
22510
  log(`Step 5/5 \u2014 opening a ${agent.name} session for /install-bridge + /learn-repository\u2026`);
20968
22511
  const terminal = detectTerminal(void 0, deps.env);
@@ -20990,7 +22533,7 @@ async function runInstallBridgeCli(argv, overrides = {}) {
20990
22533
  init_version_generated();
20991
22534
  import { spawn as spawn7 } from "child_process";
20992
22535
  import { stat as stat8 } from "fs/promises";
20993
- import path23 from "path";
22536
+ import path24 from "path";
20994
22537
  init_start_tickets();
20995
22538
  init_agent_registry();
20996
22539
  async function fetchLatestVersion() {
@@ -21057,7 +22600,7 @@ async function runUpgradeCli(argv) {
21057
22600
  );
21058
22601
  for (const target of configTargets) {
21059
22602
  try {
21060
- await stat8(path23.join(cwd, target));
22603
+ await stat8(path24.join(cwd, target));
21061
22604
  console.log(` - ${target}`);
21062
22605
  } catch {
21063
22606
  }
@@ -21076,7 +22619,7 @@ async function runUpgradeCli(argv) {
21076
22619
  return 0;
21077
22620
  }
21078
22621
  try {
21079
- const localModulePath = path23.join(cwd, "node_modules", "@bridge_gpt", "mcp-server");
22622
+ const localModulePath = path24.join(cwd, "node_modules", "@bridge_gpt", "mcp-server");
21080
22623
  await stat8(localModulePath);
21081
22624
  console.log(
21082
22625
  "Found stale local installation in node_modules. Removing to converge on pinned-npx..."
@@ -21144,13 +22687,13 @@ init_credential_store();
21144
22687
 
21145
22688
  // src/credentials-cli.ts
21146
22689
  import { readFile as readFile11, mkdir as mkdir8, writeFile as writeFile8, rename as rename2, chmod as chmod2, unlink as unlink2 } from "fs/promises";
21147
- import os13 from "os";
22690
+ import os14 from "os";
21148
22691
  import readline2 from "readline";
21149
22692
 
21150
22693
  // src/agent-config-credential-migration.ts
21151
22694
  init_credential_store();
21152
22695
  init_start_tickets_repo();
21153
- import path24 from "path";
22696
+ import path25 from "path";
21154
22697
  async function readAgentMcpConfigIfPresent(filePath, readFile14) {
21155
22698
  let raw;
21156
22699
  try {
@@ -21212,7 +22755,7 @@ function resolveAgentConfigScanTargets(cwd, sources) {
21212
22755
  const selected = sources && sources.length > 0 ? AGENT_CONFIG_SOURCE_NAMES.filter((name) => sources.includes(name)) : AGENT_CONFIG_SOURCE_NAMES;
21213
22756
  return selected.map((name) => ({
21214
22757
  name,
21215
- filePath: name === ".mcp.json" ? path24.join(cwd, ".mcp.json") : path24.join(cwd, ".cursor", "mcp.json")
22758
+ filePath: name === ".mcp.json" ? path25.join(cwd, ".mcp.json") : path25.join(cwd, ".cursor", "mcp.json")
21216
22759
  }));
21217
22760
  }
21218
22761
  async function scanAgentMcpConfigsForBapiApiKey(deps) {
@@ -21431,7 +22974,7 @@ function createDefaultCredentialsDeps(writeCredentials) {
21431
22974
  env: process.env,
21432
22975
  cwd: process.cwd(),
21433
22976
  platform: process.platform,
21434
- homedir: os13.homedir,
22977
+ homedir: os14.homedir,
21435
22978
  readFile: (p) => readFile11(p, "utf-8"),
21436
22979
  mkdir: (p, o) => mkdir8(p, o),
21437
22980
  writeFile: (p, d, o) => writeFile8(p, d, o),
@@ -21992,7 +23535,7 @@ async function getSfccVersionConfig(buildGetUrl2, getGetHeaders2, repoName) {
21992
23535
 
21993
23536
  // src/sfcc/credentials.ts
21994
23537
  import { readFile as readFile12, writeFile as writeFile9, mkdir as mkdir9 } from "fs/promises";
21995
- import path25 from "path";
23538
+ import path26 from "path";
21996
23539
  var ENV_HOSTNAME = "SFCC_HOSTNAME";
21997
23540
  var ENV_CLIENT_ID = "SFCC_CLIENT_ID";
21998
23541
  var ENV_CLIENT_SECRET = "SFCC_CLIENT_SECRET";
@@ -22042,7 +23585,7 @@ async function resolveSfccCredentials(explicitHostname, env = process.env, deps
22042
23585
  await ensureGitInfoExcluded(cwd, DW_JSON, { readFile: rf, writeFile: wf, mkdir: mk });
22043
23586
  } catch {
22044
23587
  }
22045
- const dwJsonPath = path25.join(cwd, DW_JSON);
23588
+ const dwJsonPath = path26.join(cwd, DW_JSON);
22046
23589
  let dwJson;
22047
23590
  try {
22048
23591
  const raw = await rf(dwJsonPath);
@@ -22115,11 +23658,11 @@ function mapOcapiWriteFault(status, body) {
22115
23658
  errorCode: known ? expected : "OCAPI_WRITE_FAULT"
22116
23659
  };
22117
23660
  }
22118
- function buildSyntheticIfMatchRequiredBody(path33) {
23661
+ function buildSyntheticIfMatchRequiredBody(path34) {
22119
23662
  return {
22120
23663
  fault: {
22121
23664
  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.`
23665
+ message: `PATCH ${path34} 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
23666
  }
22124
23667
  };
22125
23668
  }
@@ -22169,9 +23712,9 @@ async function getAmToken(credentials) {
22169
23712
  function invalidateAmToken(credentials) {
22170
23713
  tokenCache.delete(credentials.hostname);
22171
23714
  }
22172
- function buildOcapiUrl(hostname, ocapiVersion, path33) {
23715
+ function buildOcapiUrl(hostname, ocapiVersion, path34) {
22173
23716
  const baseUrl = `https://${hostname}/s/-/dw/data/${ocapiVersion}`;
22174
- return `${baseUrl}${path33.startsWith("/") ? path33 : "/" + path33}`;
23717
+ return `${baseUrl}${path34.startsWith("/") ? path34 : "/" + path34}`;
22175
23718
  }
22176
23719
  async function parseOcapiResponse(resp) {
22177
23720
  try {
@@ -22222,10 +23765,10 @@ async function fetchWith429Backoff(url, init) {
22222
23765
  }
22223
23766
  return resp;
22224
23767
  }
22225
- async function ocapiRequest(method, path33, body, credentials, ocapiVersion, extraHeaders) {
23768
+ async function ocapiRequest(method, path34, body, credentials, ocapiVersion, extraHeaders) {
22226
23769
  const doRequest = async () => {
22227
23770
  const token = await getAmToken(credentials);
22228
- const url = buildOcapiUrl(credentials.hostname, ocapiVersion, path33);
23771
+ const url = buildOcapiUrl(credentials.hostname, ocapiVersion, path34);
22229
23772
  const init = {
22230
23773
  method,
22231
23774
  headers: {
@@ -22258,23 +23801,23 @@ async function ocapiRequest(method, path33, body, credentials, ocapiVersion, ext
22258
23801
  }
22259
23802
  return first;
22260
23803
  }
22261
- async function ocapiGet(path33, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22262
- return ocapiRequest("GET", path33, void 0, credentials, ocapiVersion);
23804
+ async function ocapiGet(path34, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23805
+ return ocapiRequest("GET", path34, void 0, credentials, ocapiVersion);
22263
23806
  }
22264
- async function ocapiPost(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22265
- return ocapiRequest("POST", path33, body, credentials, ocapiVersion);
23807
+ async function ocapiPost(path34, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23808
+ return ocapiRequest("POST", path34, body, credentials, ocapiVersion);
22266
23809
  }
22267
- async function ocapiPut(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22268
- return ocapiRequest("PUT", path33, body, credentials, ocapiVersion);
23810
+ async function ocapiPut(path34, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23811
+ return ocapiRequest("PUT", path34, body, credentials, ocapiVersion);
22269
23812
  }
22270
- async function ocapiPatch(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22271
- const getResult = await ocapiGet(path33, credentials, ocapiVersion);
23813
+ async function ocapiPatch(path34, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23814
+ const getResult = await ocapiGet(path34, credentials, ocapiVersion);
22272
23815
  if (!getResult.ok) {
22273
23816
  return getResult;
22274
23817
  }
22275
23818
  const etag = getResult.etag;
22276
23819
  if (etag === null || etag === void 0 || etag.trim() === "") {
22277
- const syntheticBody = buildSyntheticIfMatchRequiredBody(path33);
23820
+ const syntheticBody = buildSyntheticIfMatchRequiredBody(path34);
22278
23821
  return {
22279
23822
  ok: false,
22280
23823
  status: 409,
@@ -22282,10 +23825,10 @@ async function ocapiPatch(path33, body, credentials, ocapiVersion = DEFAULT_OCAP
22282
23825
  fault: mapOcapiWriteFault(409, syntheticBody)
22283
23826
  };
22284
23827
  }
22285
- return ocapiRequest("PATCH", path33, body, credentials, ocapiVersion, { "If-Match": etag });
23828
+ return ocapiRequest("PATCH", path34, body, credentials, ocapiVersion, { "If-Match": etag });
22286
23829
  }
22287
- async function ocapiPatchDirect(path33, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
22288
- return ocapiRequest("PATCH", path33, body, credentials, ocapiVersion);
23830
+ async function ocapiPatchDirect(path34, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23831
+ return ocapiRequest("PATCH", path34, body, credentials, ocapiVersion);
22289
23832
  }
22290
23833
 
22291
23834
  // src/sfcc/setup-status.ts
@@ -22473,12 +24016,12 @@ function formatOcapiWriteGrantJson(ocapiVersion, clientIdPlaceholder = "<YOUR_CL
22473
24016
  return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder), null, 2);
22474
24017
  }
22475
24018
  function buildOcapiWriteGrant403Text(params) {
22476
- const { operation, path: path33, ocapiVersion, body } = params;
24019
+ const { operation, path: path34, ocapiVersion, body } = params;
22477
24020
  const bodyLine = body === void 0 ? "" : `
22478
24021
  Response body:
22479
24022
  ${JSON.stringify(body, null, 2)}
22480
24023
  `;
22481
- return `HTTP 403: OCAPI write access denied for ${operation} ${path33}.
24024
+ return `HTTP 403: OCAPI write access denied for ${operation} ${path34}.
22482
24025
  ` + bodyLine + `
22483
24026
  To grant write access, paste the JSON below in Business Manager:
22484
24027
  Administration > Site Development > Open Commerce API Settings \u2192 Data API tab
@@ -22601,11 +24144,11 @@ Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`;
22601
24144
  }
22602
24145
 
22603
24146
  // src/sfcc/reads-system-object.ts
22604
- import path27 from "path";
24147
+ import path28 from "path";
22605
24148
  import { z as z3 } from "zod";
22606
24149
 
22607
24150
  // src/sfcc/output.ts
22608
- import path26 from "path";
24151
+ import path27 from "path";
22609
24152
  import { mkdir as mkdir10, writeFile as writeFile10 } from "fs/promises";
22610
24153
  var SFCC_MAX_INLINE = 5e4;
22611
24154
  function truncationNote(savedPath) {
@@ -22619,7 +24162,7 @@ async function truncateAndSaveIfNeeded(text, dir, filename, deps = {}) {
22619
24162
  }
22620
24163
  const mk = deps.mkdir ?? mkdir10;
22621
24164
  const wf = deps.writeFile ?? writeFile10;
22622
- const filePath = path26.join(dir, filename);
24165
+ const filePath = path27.join(dir, filename);
22623
24166
  try {
22624
24167
  await mk(dir, { recursive: true });
22625
24168
  await wf(filePath, text, "utf-8");
@@ -22687,7 +24230,7 @@ function buildSystemObjectListHandler(gateDeps, getDocsDir2) {
22687
24230
  }
22688
24231
  const normalized = normalizeOcapiBody(result.body);
22689
24232
  const text = JSON.stringify(normalized, null, 2);
22690
- const dir = path27.join(await getDocsDir2(), "sfcc");
24233
+ const dir = path28.join(await getDocsDir2(), "sfcc");
22691
24234
  return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`);
22692
24235
  }
22693
24236
  );
@@ -22710,7 +24253,7 @@ function buildSystemObjectGetHandler(gateDeps, getDocsDir2) {
22710
24253
  }
22711
24254
  const normalized = normalizeOcapiBody(result.body);
22712
24255
  const text = JSON.stringify(normalized, null, 2);
22713
- const dir = path27.join(await getDocsDir2(), "sfcc");
24256
+ const dir = path28.join(await getDocsDir2(), "sfcc");
22714
24257
  return saveAndReturn(
22715
24258
  text,
22716
24259
  dir,
@@ -22742,7 +24285,7 @@ function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir2) {
22742
24285
  }
22743
24286
  const normalized = normalizeOcapiBody(result.body);
22744
24287
  const text = JSON.stringify(normalized, null, 2);
22745
- const dir = path27.join(await getDocsDir2(), "sfcc");
24288
+ const dir = path28.join(await getDocsDir2(), "sfcc");
22746
24289
  return saveAndReturn(
22747
24290
  text,
22748
24291
  dir,
@@ -22783,7 +24326,7 @@ function registerSystemObjectReadTools(registerTool2, deps) {
22783
24326
  }
22784
24327
 
22785
24328
  // src/sfcc/reads-custom-object-def.ts
22786
- import path28 from "path";
24329
+ import path29 from "path";
22787
24330
  import { z as z4 } from "zod";
22788
24331
  var READ_ANNOTATIONS2 = {
22789
24332
  readOnlyHint: true,
@@ -22837,7 +24380,7 @@ function buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir2) {
22837
24380
  }
22838
24381
  const normalized = normalizeOcapiBody(result.body);
22839
24382
  const text = JSON.stringify(normalized, null, 2);
22840
- const dir = path28.join(await getDocsDir2(), "sfcc");
24383
+ const dir = path29.join(await getDocsDir2(), "sfcc");
22841
24384
  return saveAndReturn2(
22842
24385
  text,
22843
24386
  dir,
@@ -22869,7 +24412,7 @@ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir2) {
22869
24412
  }
22870
24413
  const normalized = normalizeOcapiBody(result.body);
22871
24414
  const text = JSON.stringify(normalized, null, 2);
22872
- const dir = path28.join(await getDocsDir2(), "sfcc");
24415
+ const dir = path29.join(await getDocsDir2(), "sfcc");
22873
24416
  return saveAndReturn2(
22874
24417
  text,
22875
24418
  dir,
@@ -22901,7 +24444,7 @@ function registerSfccCustomObjectDefReadTools(registerTool2, deps) {
22901
24444
  }
22902
24445
 
22903
24446
  // src/sfcc/reads-site-preference.ts
22904
- import path29 from "path";
24447
+ import path30 from "path";
22905
24448
  import { z as z5 } from "zod";
22906
24449
  var READ_ANNOTATIONS3 = {
22907
24450
  readOnlyHint: true,
@@ -22981,7 +24524,7 @@ function buildSitePreferenceGetHandler(gateDeps, getDocsDir2) {
22981
24524
  }
22982
24525
  const normalized = normalizeOcapiBody(result.body);
22983
24526
  const text = JSON.stringify(normalized, null, 2);
22984
- const dir = path29.join(await getDocsDir2(), "sfcc");
24527
+ const dir = path30.join(await getDocsDir2(), "sfcc");
22985
24528
  return saveAndReturn3(
22986
24529
  text,
22987
24530
  dir,
@@ -23015,7 +24558,7 @@ function buildSitePreferenceSearchHandler(gateDeps, getDocsDir2) {
23015
24558
  }
23016
24559
  const normalized = normalizeOcapiBody(result.body);
23017
24560
  const text = JSON.stringify(normalized, null, 2);
23018
- const dir = path29.join(await getDocsDir2(), "sfcc");
24561
+ const dir = path30.join(await getDocsDir2(), "sfcc");
23019
24562
  return saveAndReturn3(
23020
24563
  text,
23021
24564
  dir,
@@ -23044,7 +24587,7 @@ function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir2) {
23044
24587
  }
23045
24588
  const normalized = normalizeOcapiBody(result.body);
23046
24589
  const text = JSON.stringify(normalized, null, 2);
23047
- const dir = path29.join(await getDocsDir2(), "sfcc");
24590
+ const dir = path30.join(await getDocsDir2(), "sfcc");
23048
24591
  return saveAndReturn3(
23049
24592
  text,
23050
24593
  dir,
@@ -23107,11 +24650,11 @@ function rejectIfNotSandboxForWrite(instance) {
23107
24650
  function textResult5(text) {
23108
24651
  return { content: [{ type: "text", text }] };
23109
24652
  }
23110
- function formatOcapiWriteToolResult(result, operation, path33, ocapiVersion = DEFAULT_OCAPI_VERSION) {
24653
+ function formatOcapiWriteToolResult(result, operation, path34, ocapiVersion = DEFAULT_OCAPI_VERSION) {
23111
24654
  if (result.status === 403) {
23112
24655
  return writeGrantForbiddenResult({
23113
24656
  operation,
23114
- path: path33,
24657
+ path: path34,
23115
24658
  ocapiVersion,
23116
24659
  body: result.body
23117
24660
  });
@@ -23369,20 +24912,20 @@ function buildCreateAttributeDefinitionHandler(gateDeps) {
23369
24912
  return withSfccGate(
23370
24913
  gateDeps,
23371
24914
  async (args, credentials) => {
23372
- let path33;
24915
+ let path34;
23373
24916
  let body;
23374
24917
  try {
23375
24918
  const parsed = createAttributeDefinitionInput.parse(args);
23376
24919
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23377
24920
  if (guard) return guard;
23378
- path33 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
24921
+ path34 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23379
24922
  body = buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id, parsed.definition);
23380
24923
  } catch (err) {
23381
24924
  return preTransportErrorEnvelope(err);
23382
24925
  }
23383
24926
  try {
23384
- const result = await ocapiPut(path33, body, credentials);
23385
- return formatOcapiWriteToolResult(result, "PUT", path33);
24927
+ const result = await ocapiPut(path34, body, credentials);
24928
+ return formatOcapiWriteToolResult(result, "PUT", path34);
23386
24929
  } catch {
23387
24930
  return unexpectedEnvelope();
23388
24931
  }
@@ -23393,20 +24936,20 @@ function buildUpdateAttributeDefinitionHandler(gateDeps) {
23393
24936
  return withSfccGate(
23394
24937
  gateDeps,
23395
24938
  async (args, credentials) => {
23396
- let path33;
24939
+ let path34;
23397
24940
  let body;
23398
24941
  try {
23399
24942
  const parsed = updateAttributeDefinitionInput.parse(args);
23400
24943
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23401
24944
  if (guard) return guard;
23402
- path33 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
24945
+ path34 = attributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23403
24946
  body = buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id, parsed.patch);
23404
24947
  } catch (err) {
23405
24948
  return preTransportErrorEnvelope(err);
23406
24949
  }
23407
24950
  try {
23408
- const result = await ocapiPatch(path33, body, credentials);
23409
- return formatOcapiWriteToolResult(result, "PATCH", path33);
24951
+ const result = await ocapiPatch(path34, body, credentials);
24952
+ return formatOcapiWriteToolResult(result, "PATCH", path34);
23410
24953
  } catch {
23411
24954
  return unexpectedEnvelope();
23412
24955
  }
@@ -23417,13 +24960,13 @@ function buildCreateAttributeGroupHandler(gateDeps) {
23417
24960
  return withSfccGate(
23418
24961
  gateDeps,
23419
24962
  async (args, credentials) => {
23420
- let path33;
24963
+ let path34;
23421
24964
  let body;
23422
24965
  try {
23423
24966
  const parsed = createAttributeGroupInput.parse(args);
23424
24967
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23425
24968
  if (guard) return guard;
23426
- path33 = attributeGroupPath(parsed.object_type, parsed.group_id);
24969
+ path34 = attributeGroupPath(parsed.object_type, parsed.group_id);
23427
24970
  body = buildAttributeGroupPutPayload({
23428
24971
  display_name: parsed.display_name,
23429
24972
  internal: parsed.internal
@@ -23432,8 +24975,8 @@ function buildCreateAttributeGroupHandler(gateDeps) {
23432
24975
  return preTransportErrorEnvelope(err);
23433
24976
  }
23434
24977
  try {
23435
- const result = await ocapiPut(path33, body, credentials);
23436
- return formatOcapiWriteToolResult(result, "PUT", path33);
24978
+ const result = await ocapiPut(path34, body, credentials);
24979
+ return formatOcapiWriteToolResult(result, "PUT", path34);
23437
24980
  } catch {
23438
24981
  return unexpectedEnvelope();
23439
24982
  }
@@ -23444,20 +24987,20 @@ function buildUpdateAttributeGroupHandler(gateDeps) {
23444
24987
  return withSfccGate(
23445
24988
  gateDeps,
23446
24989
  async (args, credentials) => {
23447
- let path33;
24990
+ let path34;
23448
24991
  let body;
23449
24992
  try {
23450
24993
  const parsed = updateAttributeGroupInput.parse(args);
23451
24994
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23452
24995
  if (guard) return guard;
23453
- path33 = attributeGroupPath(parsed.object_type, parsed.group_id);
24996
+ path34 = attributeGroupPath(parsed.object_type, parsed.group_id);
23454
24997
  body = buildAttributeGroupPatchPayload(parsed.patch);
23455
24998
  } catch (err) {
23456
24999
  return preTransportErrorEnvelope(err);
23457
25000
  }
23458
25001
  try {
23459
- const result = await ocapiPatch(path33, body, credentials);
23460
- return formatOcapiWriteToolResult(result, "PATCH", path33);
25002
+ const result = await ocapiPatch(path34, body, credentials);
25003
+ return formatOcapiWriteToolResult(result, "PATCH", path34);
23461
25004
  } catch {
23462
25005
  return unexpectedEnvelope();
23463
25006
  }
@@ -23468,12 +25011,12 @@ function buildAssignAttributeToGroupHandler(gateDeps) {
23468
25011
  return withSfccGate(
23469
25012
  gateDeps,
23470
25013
  async (args, credentials) => {
23471
- let path33;
25014
+ let path34;
23472
25015
  try {
23473
25016
  const parsed = assignAttributeToGroupInput.parse(args);
23474
25017
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23475
25018
  if (guard) return guard;
23476
- path33 = attributeGroupAssignmentPath(
25019
+ path34 = attributeGroupAssignmentPath(
23477
25020
  parsed.object_type,
23478
25021
  parsed.group_id,
23479
25022
  parsed.attribute_id
@@ -23482,8 +25025,8 @@ function buildAssignAttributeToGroupHandler(gateDeps) {
23482
25025
  return preTransportErrorEnvelope(err);
23483
25026
  }
23484
25027
  try {
23485
- const result = await ocapiPut(path33, buildEmptyRelationPayload(), credentials);
23486
- return formatOcapiWriteToolResult(result, "PUT", path33);
25028
+ const result = await ocapiPut(path34, buildEmptyRelationPayload(), credentials);
25029
+ return formatOcapiWriteToolResult(result, "PUT", path34);
23487
25030
  } catch {
23488
25031
  return unexpectedEnvelope();
23489
25032
  }
@@ -23494,21 +25037,21 @@ function buildCreateCustomPreferenceDefinitionHandler(gateDeps) {
23494
25037
  return withSfccGate(
23495
25038
  gateDeps,
23496
25039
  async (args, credentials) => {
23497
- let path33;
25040
+ let path34;
23498
25041
  let body;
23499
25042
  try {
23500
25043
  const parsed = createCustomPreferenceDefinitionInput.parse(args);
23501
25044
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23502
25045
  if (guard) return guard;
23503
25046
  const objectType = preferenceObjectTypeForScope(parsed.preference_scope);
23504
- path33 = attributeDefinitionPath(objectType, parsed.preference_id);
25047
+ path34 = attributeDefinitionPath(objectType, parsed.preference_id);
23505
25048
  body = buildObjectAttributeDefinitionCreatePayload(parsed.preference_id, parsed.definition);
23506
25049
  } catch (err) {
23507
25050
  return preTransportErrorEnvelope(err);
23508
25051
  }
23509
25052
  try {
23510
- const result = await ocapiPut(path33, body, credentials);
23511
- return formatOcapiWriteToolResult(result, "PUT", path33);
25053
+ const result = await ocapiPut(path34, body, credentials);
25054
+ return formatOcapiWriteToolResult(result, "PUT", path34);
23512
25055
  } catch {
23513
25056
  return unexpectedEnvelope();
23514
25057
  }
@@ -23723,11 +25266,11 @@ function buildCreateCustomObjectAttributeDefinitionHandler(gateDeps) {
23723
25266
  `Body id '${parsed.definition.id}' does not match URL attribute_id '${parsed.attribute_id}'.`
23724
25267
  );
23725
25268
  }
23726
- const path33 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
25269
+ const path34 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23727
25270
  const body = buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id, parsed.definition);
23728
25271
  try {
23729
- const result = await ocapiPut(path33, body, credentials);
23730
- return formatOcapiWriteToolResult(result, "PUT", path33);
25272
+ const result = await ocapiPut(path34, body, credentials);
25273
+ return formatOcapiWriteToolResult(result, "PUT", path34);
23731
25274
  } catch {
23732
25275
  return unexpectedEnvelope2();
23733
25276
  }
@@ -23746,11 +25289,11 @@ function buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps) {
23746
25289
  }
23747
25290
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23748
25291
  if (guard) return guard;
23749
- const path33 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
25292
+ const path34 = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
23750
25293
  const body = buildObjectAttributeDefinitionPatchPayload2(parsed.patch);
23751
25294
  try {
23752
- const result = await ocapiPatch(path33, body, credentials);
23753
- return formatOcapiWriteToolResult(result, "PATCH", path33);
25295
+ const result = await ocapiPatch(path34, body, credentials);
25296
+ return formatOcapiWriteToolResult(result, "PATCH", path34);
23754
25297
  } catch {
23755
25298
  return unexpectedEnvelope2();
23756
25299
  }
@@ -23841,11 +25384,11 @@ function buildSitePreferenceValuesSetHandler(gateDeps) {
23841
25384
  }
23842
25385
  const guard = rejectIfNotSandboxForWrite(parsed.instance);
23843
25386
  if (guard) return guard;
23844
- const path33 = sitePreferenceGroupPath(parsed.group);
25387
+ const path34 = sitePreferenceGroupPath(parsed.group);
23845
25388
  const body = buildSitePreferenceValuesPatchPayload(parsed.values);
23846
25389
  try {
23847
- const result = await ocapiPatchDirect(path33, body, credentials);
23848
- return formatOcapiWriteToolResult(result, "PATCH", path33);
25390
+ const result = await ocapiPatchDirect(path34, body, credentials);
25391
+ return formatOcapiWriteToolResult(result, "PATCH", path34);
23849
25392
  } catch {
23850
25393
  return unexpectedEnvelope2();
23851
25394
  }
@@ -24314,27 +25857,21 @@ async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
24314
25857
  backend: null,
24315
25858
  next_fire_iso: null,
24316
25859
  latest_run_status: null,
24317
- degraded: true,
24318
- warnings: [
24319
- "No epic-tick schedule is registered. Run: npx -y @bridge_gpt/mcp-server schedule-run create --in 1m --command epic-tick -- --epic-key <EPIC-KEY>"
24320
- ]
25860
+ degraded: false,
25861
+ warnings: []
24321
25862
  };
24322
25863
  }
24323
25864
  const m = entry.metadata;
24324
25865
  const latest = runStatus(m);
24325
- const degraded = entry.status !== "active";
24326
- const warnings = [];
24327
- if (degraded) {
24328
- const msg = entry.status === "backend-unavailable" ? `epic-tick schedule status is "backend-unavailable": the OS scheduler backend is unreachable. Check that the scheduler daemon is running.` : `epic-tick schedule status is "${entry.status}" (expected "active"); re-register if stale.`;
24329
- warnings.push(msg);
24330
- }
24331
25866
  return {
24332
25867
  registered: true,
24333
25868
  backend: m.backend ?? null,
24334
25869
  next_fire_iso: m.run_at_iso ?? null,
24335
25870
  latest_run_status: latest || null,
24336
- degraded,
24337
- warnings
25871
+ degraded: true,
25872
+ warnings: [
25873
+ `An epic-tick schedule is registered, but the v1 \`conductor epic-tick\` path is frozen (EPIC_TICK_V1_FROZEN) \u2014 it advances nothing. Cancel it: \`npx -y @bridge_gpt/mcp-server schedule-run cancel --id ${entry.metadata.id ?? "<id>"}\`. Epic Conductor v2 reconciles server-side; run jobs locally with \`npx -y @bridge_gpt/mcp-server executor --repo <name>\`.`
25874
+ ]
24338
25875
  };
24339
25876
  } catch (err) {
24340
25877
  const msg = err instanceof Error ? err.message : String(err);
@@ -24480,19 +26017,25 @@ function formatConductorDoctorReport(report) {
24480
26017
  for (const w of git_hooks.warnings) lines.push(` - ${w}`);
24481
26018
  }
24482
26019
  lines.push("");
24483
- lines.push("Epic Supervisor Schedule (optional, local)");
24484
- lines.push("\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
24485
- const registeredTag = epic_tick.registered ? "[SUCCESS] registered" : "[WARNING] not registered";
24486
- lines.push(`registered: ${registeredTag}`);
24487
- lines.push(`backend: ${epic_tick.backend ?? "n/a"}`);
24488
- lines.push(`next fire: ${epic_tick.next_fire_iso ?? "n/a"}`);
24489
- lines.push(`latest run status: ${epic_tick.latest_run_status ?? "n/a"}`);
26020
+ lines.push("Epic Supervisor Schedule (v1 epic-tick \u2014 frozen)");
26021
+ lines.push("\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
26022
+ const registeredTag = epic_tick.registered ? "[WARNING] registered \u2014 dead timer, remove it" : "[SUCCESS] none registered (v1 is frozen)";
26023
+ lines.push(`epic-tick schedule: ${registeredTag}`);
26024
+ if (epic_tick.registered) {
26025
+ lines.push(`backend: ${epic_tick.backend ?? "n/a"}`);
26026
+ lines.push(`next fire: ${epic_tick.next_fire_iso ?? "n/a"}`);
26027
+ lines.push(`latest run status: ${epic_tick.latest_run_status ?? "n/a"}`);
26028
+ }
24490
26029
  lines.push(`degraded: ${epic_tick.degraded}`);
24491
26030
  if (epic_tick.warnings.length > 0) {
24492
26031
  lines.push("epic-tick warnings:");
24493
26032
  for (const w of epic_tick.warnings) lines.push(` - ${w}`);
24494
26033
  }
24495
26034
  lines.push("");
26035
+ lines.push("Epic Conductor v2 reconciles epics server-side \u2014 nothing to schedule");
26036
+ lines.push("locally. To execute claimed jobs on this machine, run:");
26037
+ lines.push(" npx -y @bridge_gpt/mcp-server executor --repo <name>");
26038
+ lines.push("");
24496
26039
  lines.push("MCP Profile (optional, local)");
24497
26040
  lines.push("\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");
24498
26041
  const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
@@ -25513,7 +27056,7 @@ var DecisionPageLeanInputShape = {
25513
27056
 
25514
27057
  // src/brainstorm-files.ts
25515
27058
  import { writeFile as writeFile11, mkdir as mkdir11 } from "fs/promises";
25516
- import path30 from "path";
27059
+ import path31 from "path";
25517
27060
  function slugify(text, maxLength = 60) {
25518
27061
  return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, maxLength).replace(/-$/, "");
25519
27062
  }
@@ -25538,7 +27081,7 @@ async function saveBrainstormResultsToDir(envelope, dir, subject) {
25538
27081
  continue;
25539
27082
  }
25540
27083
  const filename = buildBrainstormResultFilename(envelope, row, subject);
25541
- const filePath = path30.join(dir, filename);
27084
+ const filePath = path31.join(dir, filename);
25542
27085
  try {
25543
27086
  await mkdir11(dir, { recursive: true });
25544
27087
  await writeFile11(filePath, markdown, "utf-8");
@@ -25550,10 +27093,10 @@ async function saveBrainstormResultsToDir(envelope, dir, subject) {
25550
27093
  }
25551
27094
 
25552
27095
  // src/pipeline-orchestrator.ts
25553
- import { createHash as createHash6 } from "node:crypto";
27096
+ import { createHash as createHash7 } from "node:crypto";
25554
27097
  function deriveIdeaHash(idea) {
25555
27098
  const normalized = String(idea ?? "").trim().toLowerCase().replace(/\s+/g, " ");
25556
- return createHash6("sha256").update(normalized).digest("hex").slice(0, 12);
27099
+ return createHash7("sha256").update(normalized).digest("hex").slice(0, 12);
25557
27100
  }
25558
27101
  var PipelinePersistenceError = class extends Error {
25559
27102
  code;
@@ -27287,7 +28830,7 @@ async function resumeFullAutomation(deps, input) {
27287
28830
  }
27288
28831
 
27289
28832
  // src/visual-diff.ts
27290
- import path31 from "path";
28833
+ import path32 from "path";
27291
28834
  import { Worker } from "worker_threads";
27292
28835
 
27293
28836
  // src/visual-diff-worker.ts
@@ -27659,12 +29202,12 @@ function toUint8(bytes) {
27659
29202
  }
27660
29203
  async function resolveCompRef(compRef, deps) {
27661
29204
  const candidates = [];
27662
- if (path31.isAbsolute(compRef)) {
29205
+ if (path32.isAbsolute(compRef)) {
27663
29206
  candidates.push(compRef);
27664
29207
  } else {
27665
29208
  const root = await deps.getProjectRoot();
27666
- candidates.push(path31.resolve(root, compRef));
27667
- const cwdCandidate = path31.resolve(process.cwd(), compRef);
29209
+ candidates.push(path32.resolve(root, compRef));
29210
+ const cwdCandidate = path32.resolve(process.cwd(), compRef);
27668
29211
  if (!candidates.includes(cwdCandidate)) candidates.push(cwdCandidate);
27669
29212
  }
27670
29213
  for (const candidate of candidates) {
@@ -27688,9 +29231,9 @@ async function resolveCompRef(compRef, deps) {
27688
29231
  try {
27689
29232
  const dir = await deps.getDocsPath("visual-diffs");
27690
29233
  const rawName = fetched.filename || (lookup.kind === "attachment_id" ? `attachment-${lookup.attachment_id}` : lookup.filename);
27691
- const base = path31.basename(rawName);
27692
- const target = path31.resolve(dir, `comp-${deps.safeTimestampForFilename()}-${base}`);
27693
- if (!target.startsWith(path31.resolve(dir) + path31.sep)) {
29234
+ const base = path32.basename(rawName);
29235
+ const target = path32.resolve(dir, `comp-${deps.safeTimestampForFilename()}-${base}`);
29236
+ if (!target.startsWith(path32.resolve(dir) + path32.sep)) {
27694
29237
  warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.");
27695
29238
  } else {
27696
29239
  await deps.mkdir(dir, { recursive: true });
@@ -27933,8 +29476,8 @@ function runInWorker(args) {
27933
29476
  async function saveHeatmap(heatmapBase64, deps) {
27934
29477
  try {
27935
29478
  const dir = await deps.getDocsPath("visual-diffs");
27936
- const target = path31.resolve(dir, `visual-diff-${deps.safeTimestampForFilename()}.png`);
27937
- if (!target.startsWith(path31.resolve(dir) + path31.sep)) {
29479
+ const target = path32.resolve(dir, `visual-diff-${deps.safeTimestampForFilename()}.png`);
29480
+ if (!target.startsWith(path32.resolve(dir) + path32.sep)) {
27938
29481
  return { ok: false, warning: "Heatmap not saved: resolved path escaped the visual-diffs directory." };
27939
29482
  }
27940
29483
  await deps.mkdir(dir, { recursive: true });
@@ -28135,7 +29678,7 @@ async function getResolvedApiKey() {
28135
29678
  try {
28136
29679
  const result = await resolveBapiCredentials(REPO_NAME, {
28137
29680
  env: process.env,
28138
- homedir: os14.homedir,
29681
+ homedir: os15.homedir,
28139
29682
  platform: process.platform,
28140
29683
  readFile: (p) => readFile13(p, "utf-8"),
28141
29684
  stat: (p) => stat9(p)
@@ -28152,7 +29695,7 @@ async function getResolvedApiKeyForRepo(repoName) {
28152
29695
  try {
28153
29696
  const result = await resolveBapiCredentials(repoName, {
28154
29697
  env: process.env,
28155
- homedir: os14.homedir,
29698
+ homedir: os15.homedir,
28156
29699
  platform: process.platform,
28157
29700
  readFile: (p) => readFile13(p, "utf-8"),
28158
29701
  stat: (p) => stat9(p)
@@ -28165,14 +29708,24 @@ async function getResolvedApiKeyForRepo(repoName) {
28165
29708
  function buildCredentialStoreWriteDeps() {
28166
29709
  return {
28167
29710
  env: process.env,
28168
- homedir: os14.homedir,
29711
+ homedir: os15.homedir,
28169
29712
  platform: process.platform,
28170
29713
  readFile: (p) => readFile13(p, "utf-8"),
28171
29714
  mkdir: (p, options) => mkdir12(p, options),
28172
29715
  writeFile: (p, data, options) => writeFile12(p, data, options),
28173
29716
  rename: (oldPath, newPath) => rename3(oldPath, newPath),
28174
29717
  chmod: (p, mode) => chmod3(p, mode),
28175
- unlink: (p) => unlink3(p)
29718
+ unlink: (p) => unlink3(p),
29719
+ // Supplies fsync + the exclusive lock (BAPI-606), so an MCP-triggered write
29720
+ // cannot last-writer-win against a concurrent install-bridge run.
29721
+ open: async (p, flags, mode) => {
29722
+ const handle = await open2(p, flags, mode);
29723
+ return {
29724
+ writeFile: (data) => handle.writeFile(data, { encoding: "utf-8" }),
29725
+ sync: () => handle.sync(),
29726
+ close: () => handle.close()
29727
+ };
29728
+ }
28176
29729
  };
28177
29730
  }
28178
29731
  async function getGetHeaders() {
@@ -28226,39 +29779,39 @@ async function getProjectRoot() {
28226
29779
  var docsDirPromise;
28227
29780
  async function getDocsDir() {
28228
29781
  if (!docsDirPromise) {
28229
- docsDirPromise = (async () => path32.resolve(await getProjectRoot(), process.env.BAPI_DOCS_DIR ?? "docs/tmp"))();
29782
+ docsDirPromise = (async () => path33.resolve(await getProjectRoot(), process.env.BAPI_DOCS_DIR ?? "docs/tmp"))();
28230
29783
  }
28231
29784
  return docsDirPromise;
28232
29785
  }
28233
29786
  var pipelinesDirPromise;
28234
29787
  async function getPipelinesDir() {
28235
29788
  if (!pipelinesDirPromise) {
28236
- pipelinesDirPromise = (async () => path32.resolve(await getProjectRoot(), process.env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines"))();
29789
+ pipelinesDirPromise = (async () => path33.resolve(await getProjectRoot(), process.env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines"))();
28237
29790
  }
28238
29791
  return pipelinesDirPromise;
28239
29792
  }
28240
- function buildUrl(path33) {
28241
- return `${BASE_URL.replace(/\/+$/, "")}/jira${path33}`;
29793
+ function buildUrl(path34) {
29794
+ return `${BASE_URL.replace(/\/+$/, "")}/jira${path34}`;
28242
29795
  }
28243
- function buildApiUrl(path33) {
28244
- return `${BASE_URL.replace(/\/+$/, "")}${path33}`;
29796
+ function buildApiUrl(path34) {
29797
+ return `${BASE_URL.replace(/\/+$/, "")}${path34}`;
28245
29798
  }
28246
- function buildGetUrl(path33, params) {
28247
- const url = new URL(buildUrl(path33));
29799
+ function buildGetUrl(path34, params) {
29800
+ const url = new URL(buildUrl(path34));
28248
29801
  for (const [key, value] of Object.entries(params)) {
28249
29802
  url.searchParams.set(key, value);
28250
29803
  }
28251
29804
  return url.toString();
28252
29805
  }
28253
29806
  async function getDocsPath(subdir) {
28254
- return path32.join(await getDocsDir(), subdir);
29807
+ return path33.join(await getDocsDir(), subdir);
28255
29808
  }
28256
29809
  var customPipelinesPromise;
28257
29810
  async function ensureCustomPipelinesLoaded() {
28258
29811
  if (!customPipelinesPromise) {
28259
29812
  customPipelinesPromise = (async () => {
28260
29813
  const pipelinesDir = await getPipelinesDir();
28261
- const instructionsDir = path32.join(path32.dirname(pipelinesDir), "instructions");
29814
+ const instructionsDir = path33.join(path33.dirname(pipelinesDir), "instructions");
28262
29815
  const customResult = await loadCustomPipelines(
28263
29816
  pipelinesDir,
28264
29817
  instructionsDir,
@@ -28343,7 +29896,7 @@ async function createTicketRequest(params) {
28343
29896
  return handleResponse(resp);
28344
29897
  }
28345
29898
  async function saveLocally(dir, filename, content) {
28346
- const filePath = path32.join(dir, filename);
29899
+ const filePath = path33.join(dir, filename);
28347
29900
  try {
28348
29901
  await mkdir12(dir, { recursive: true });
28349
29902
  await writeFile12(filePath, content, "utf-8");
@@ -28363,14 +29916,14 @@ function safeTimestampForFilename() {
28363
29916
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28364
29917
  }
28365
29918
  function safeTicketFileSegment(ticketNumber) {
28366
- const base = path32.basename(ticketNumber.trim());
29919
+ const base = path33.basename(ticketNumber.trim());
28367
29920
  const cleaned = base.replace(/[^A-Za-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
28368
29921
  return cleaned || "ticket";
28369
29922
  }
28370
29923
  function isContainedSaveTarget(dir, filename) {
28371
- const resolvedDir = path32.resolve(dir);
28372
- const target = path32.resolve(resolvedDir, filename);
28373
- return target.startsWith(resolvedDir + path32.sep);
29924
+ const resolvedDir = path33.resolve(dir);
29925
+ const target = path33.resolve(resolvedDir, filename);
29926
+ return target.startsWith(resolvedDir + path33.sep);
28374
29927
  }
28375
29928
  function saveLocallySucceeded(note) {
28376
29929
  return note.includes("Saved to ");
@@ -28479,7 +30032,7 @@ var ALLOWED_BINARY_UPLOAD_MIME_TYPES = Array.from(
28479
30032
  new Set(Object.values(ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION))
28480
30033
  ).sort();
28481
30034
  function deriveAllowedBinaryUploadMimeType(effectiveFileName) {
28482
- const ext = path32.extname(effectiveFileName).toLowerCase();
30035
+ const ext = path33.extname(effectiveFileName).toLowerCase();
28483
30036
  return ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION[ext];
28484
30037
  }
28485
30038
  async function resolveUploadAttachment(textValue, filePath, textLabel, effectiveFileName) {
@@ -28510,7 +30063,7 @@ async function resolveUploadAttachment(textValue, filePath, textLabel, effective
28510
30063
  }
28511
30064
  };
28512
30065
  }
28513
- const ext = path32.extname(filePath).toLowerCase();
30066
+ const ext = path33.extname(filePath).toLowerCase();
28514
30067
  const buf = await readFile13(filePath);
28515
30068
  let isBinary = BINARY_EXTENSIONS.has(ext);
28516
30069
  if (!isBinary) {
@@ -28885,7 +30438,7 @@ Raw body: ${result.text}`
28885
30438
  }
28886
30439
  async function ensurePackageJsonForCliCommand(flagName, cwd) {
28887
30440
  try {
28888
- await stat9(path32.join(cwd, "package.json"));
30441
+ await stat9(path33.join(cwd, "package.json"));
28889
30442
  return null;
28890
30443
  } catch {
28891
30444
  return `Error: No package.json found in current directory.
@@ -28933,6 +30486,9 @@ async function dispatchCliSubcommand(argv) {
28933
30486
  if (argv[0] === "executor") {
28934
30487
  return runExecutorCli(argv.slice(1));
28935
30488
  }
30489
+ if (argv[0] === "setup-epic") {
30490
+ return runSetupEpicCli(argv.slice(1));
30491
+ }
28936
30492
  if (argv[0] === "regression-check") {
28937
30493
  return runRegressionCheckCli(argv.slice(1));
28938
30494
  }
@@ -29846,7 +31402,7 @@ registerTool(
29846
31402
  switch (args.operation) {
29847
31403
  case "upload": {
29848
31404
  const { ticket_number, file_path, content, file_name, link_type, replace_existing } = args;
29849
- const derivedFileName = file_name || (file_path ? path32.basename(file_path) : `${ticket_number}-attachment.md`);
31405
+ const derivedFileName = file_name || (file_path ? path33.basename(file_path) : `${ticket_number}-attachment.md`);
29850
31406
  const resolved = await resolveUploadAttachment(content, file_path, "content", derivedFileName);
29851
31407
  if (!resolved.ok) return resolved.errorResponse;
29852
31408
  const payload = {
@@ -29906,12 +31462,12 @@ registerTool(
29906
31462
  const isText = body.is_text;
29907
31463
  const mimeType = body.mime_type;
29908
31464
  const size = body.size;
29909
- const safeFileName = path32.basename(serverFilename);
29910
- const safeTicket = path32.basename(ticket_number);
29911
- const savePath = file_path ? file_path : path32.join(await getDocsDir(), "attachments", safeTicket, safeFileName);
29912
- const resolvedSave = path32.resolve(savePath);
29913
- const resolvedRoot = path32.resolve(await getProjectRoot());
29914
- if (!resolvedSave.startsWith(resolvedRoot + path32.sep) && resolvedSave !== resolvedRoot) {
31465
+ const safeFileName = path33.basename(serverFilename);
31466
+ const safeTicket = path33.basename(ticket_number);
31467
+ const savePath = file_path ? file_path : path33.join(await getDocsDir(), "attachments", safeTicket, safeFileName);
31468
+ const resolvedSave = path33.resolve(savePath);
31469
+ const resolvedRoot = path33.resolve(await getProjectRoot());
31470
+ if (!resolvedSave.startsWith(resolvedRoot + path33.sep) && resolvedSave !== resolvedRoot) {
29915
31471
  return {
29916
31472
  content: [{
29917
31473
  type: "text",
@@ -29922,7 +31478,7 @@ registerTool(
29922
31478
  }]
29923
31479
  };
29924
31480
  }
29925
- await mkdir12(path32.dirname(resolvedSave), { recursive: true });
31481
+ await mkdir12(path33.dirname(resolvedSave), { recursive: true });
29926
31482
  if (isText) {
29927
31483
  await writeFile12(resolvedSave, content, "utf-8");
29928
31484
  } else {
@@ -31554,7 +33110,7 @@ registerTool(
31554
33110
  var REVIEW_WORKSPACE_PREFIX = "bridge-review-";
31555
33111
  var REVIEW_WORKSPACE_TTL_MS = 24 * 60 * 60 * 1e3;
31556
33112
  async function pruneStaleReviewWorkspaces() {
31557
- const tmpDir = os14.tmpdir();
33113
+ const tmpDir = os15.tmpdir();
31558
33114
  let entries;
31559
33115
  try {
31560
33116
  entries = await readdir3(tmpDir);
@@ -31564,7 +33120,7 @@ async function pruneStaleReviewWorkspaces() {
31564
33120
  const now = Date.now();
31565
33121
  for (const entry of entries) {
31566
33122
  if (!entry.startsWith(REVIEW_WORKSPACE_PREFIX)) continue;
31567
- const fullPath = path32.join(tmpDir, entry);
33123
+ const fullPath = path33.join(tmpDir, entry);
31568
33124
  try {
31569
33125
  const info = await stat9(fullPath);
31570
33126
  if (now - info.mtimeMs > REVIEW_WORKSPACE_TTL_MS) {
@@ -31669,7 +33225,7 @@ registerTool(
31669
33225
  }
31670
33226
  let tempDir;
31671
33227
  try {
31672
- tempDir = await mkdtemp3(path32.join(os14.tmpdir(), REVIEW_WORKSPACE_PREFIX));
33228
+ tempDir = await mkdtemp3(path33.join(os15.tmpdir(), REVIEW_WORKSPACE_PREFIX));
31673
33229
  } catch (err) {
31674
33230
  const message = err instanceof Error ? err.message : String(err);
31675
33231
  return {
@@ -31683,7 +33239,7 @@ registerTool(
31683
33239
  }]
31684
33240
  };
31685
33241
  }
31686
- const archivePath = path32.join(tempDir, "archive.tar");
33242
+ const archivePath = path33.join(tempDir, "archive.tar");
31687
33243
  const archiveResult = await startTicketsDeps.runCommand(
31688
33244
  "git",
31689
33245
  ["archive", "--format=tar", resolvedBaseSha, "-o", archivePath],
@@ -31755,11 +33311,11 @@ registerTool(
31755
33311
  }
31756
33312
  },
31757
33313
  async ({ fresh_base_root }) => {
31758
- const allowedPrefix = path32.join(os14.tmpdir(), REVIEW_WORKSPACE_PREFIX);
31759
- const resolvedTarget = path32.resolve(fresh_base_root);
31760
- const resolvedTmpDir = path32.resolve(os14.tmpdir());
31761
- const isDirectChildOfTmpDir = path32.dirname(resolvedTarget) === resolvedTmpDir;
31762
- const hasReviewPrefix = path32.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);
33314
+ const allowedPrefix = path33.join(os15.tmpdir(), REVIEW_WORKSPACE_PREFIX);
33315
+ const resolvedTarget = path33.resolve(fresh_base_root);
33316
+ const resolvedTmpDir = path33.resolve(os15.tmpdir());
33317
+ const isDirectChildOfTmpDir = path33.dirname(resolvedTarget) === resolvedTmpDir;
33318
+ const hasReviewPrefix = path33.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);
31763
33319
  if (!isDirectChildOfTmpDir || !hasReviewPrefix) {
31764
33320
  return {
31765
33321
  content: [{
@@ -32007,7 +33563,7 @@ function containsUnsafeEncodedPathToken(value) {
32007
33563
  return /%2e/i.test(value) || /%2f/i.test(value) || /%5c/i.test(value);
32008
33564
  }
32009
33565
  function isPlatformAbsolutePath(value) {
32010
- return path32.posix.isAbsolute(value) || path32.win32.isAbsolute(value) || path32.isAbsolute(value);
33566
+ return path33.posix.isAbsolute(value) || path33.win32.isAbsolute(value) || path33.isAbsolute(value);
32011
33567
  }
32012
33568
  function validateDecisionPageOutputSubdir(value) {
32013
33569
  if (value.trim().length === 0) {
@@ -32057,9 +33613,9 @@ async function resolveDecisionPageOutputTarget(outputSubdir, outputFilename) {
32057
33613
  if (subdirError) return { ok: false, message: subdirError };
32058
33614
  const filenameError = validateDecisionPageOutputFilename(outputFilename);
32059
33615
  if (filenameError) return { ok: false, message: filenameError };
32060
- const docsBase = path32.resolve(await getDocsDir());
32061
- const resolvedTarget = path32.resolve(docsBase, outputSubdir, outputFilename);
32062
- if (!resolvedTarget.startsWith(docsBase + path32.sep)) {
33616
+ const docsBase = path33.resolve(await getDocsDir());
33617
+ const resolvedTarget = path33.resolve(docsBase, outputSubdir, outputFilename);
33618
+ if (!resolvedTarget.startsWith(docsBase + path33.sep)) {
32063
33619
  return {
32064
33620
  ok: false,
32065
33621
  message: `Invalid output target: the resolved output path must stay under the docs directory.`
@@ -32067,7 +33623,7 @@ async function resolveDecisionPageOutputTarget(outputSubdir, outputFilename) {
32067
33623
  }
32068
33624
  return {
32069
33625
  ok: true,
32070
- docsPath: path32.dirname(resolvedTarget),
33626
+ docsPath: path33.dirname(resolvedTarget),
32071
33627
  filePath: resolvedTarget
32072
33628
  };
32073
33629
  }
@@ -32155,36 +33711,36 @@ registerTool(
32155
33711
  return validationError(outputTarget.message);
32156
33712
  }
32157
33713
  const projectRootForAssets = await getProjectRoot();
32158
- const pkgRoot = path32.resolve(path32.dirname(fileURLToPath3(import.meta.url)), "../");
33714
+ const pkgRoot = path33.resolve(path33.dirname(fileURLToPath3(import.meta.url)), "../");
32159
33715
  let assetsDir;
32160
33716
  try {
32161
- await stat9(path32.join(projectRootForAssets, "design-assets"));
32162
- assetsDir = path32.join(projectRootForAssets, "design-assets");
33717
+ await stat9(path33.join(projectRootForAssets, "design-assets"));
33718
+ assetsDir = path33.join(projectRootForAssets, "design-assets");
32163
33719
  } catch {
32164
- assetsDir = path32.join(pkgRoot, "design-assets");
33720
+ assetsDir = path33.join(pkgRoot, "design-assets");
32165
33721
  }
32166
33722
  let fontsDir;
32167
33723
  try {
32168
- await stat9(path32.join(projectRootForAssets, "public", "fonts"));
32169
- fontsDir = path32.join(projectRootForAssets, "public", "fonts");
33724
+ await stat9(path33.join(projectRootForAssets, "public", "fonts"));
33725
+ fontsDir = path33.join(projectRootForAssets, "public", "fonts");
32170
33726
  } catch {
32171
- fontsDir = path32.join(pkgRoot, "public", "fonts");
33727
+ fontsDir = path33.join(pkgRoot, "public", "fonts");
32172
33728
  }
32173
33729
  let faviconBase64 = "";
32174
33730
  let logoBase64 = "";
32175
33731
  try {
32176
- const faviconBuf = await readFile13(path32.join(assetsDir, "favicon", "favicon-32x32.png"));
33732
+ const faviconBuf = await readFile13(path33.join(assetsDir, "favicon", "favicon-32x32.png"));
32177
33733
  faviconBase64 = faviconBuf.toString("base64");
32178
33734
  } catch {
32179
33735
  }
32180
33736
  try {
32181
- const logoBuf = await readFile13(path32.join(assetsDir, "just-logo-rough-draft.png"));
33737
+ const logoBuf = await readFile13(path33.join(assetsDir, "just-logo-rough-draft.png"));
32182
33738
  logoBase64 = logoBuf.toString("base64");
32183
33739
  } catch {
32184
33740
  }
32185
33741
  const docsPath = outputTarget.docsPath;
32186
33742
  const filePath = outputTarget.filePath;
32187
- const fontsRelPath = path32.relative(docsPath, fontsDir);
33743
+ const fontsRelPath = path33.relative(docsPath, fontsDir);
32188
33744
  const html = generateDecisionPageHtml(parsed, {
32189
33745
  faviconBase64,
32190
33746
  logoBase64,