@bridge_gpt/mcp-server 0.2.29 → 0.2.30

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.29"}});import path3 from"path";function hasControlChars(value){for(let i=0;i<value.length;i++){let code=value.charCodeAt(i);if(code<=31||code===127)return!0}return!1}function validateRepoName(raw){if(typeof raw!="string")return{ok:!1,error:"repo_name must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"repo_name must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"repo_name must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"repo_name must not contain control characters"}:{ok:!0,value}}function validateMcpTarget(raw){if(typeof raw!="string")return{ok:!1,error:"mcp target must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"mcp target must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"mcp target must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"mcp target must not contain control characters"}:{ok:!0,value}}function parseQuotedString(value){if(value.length<2||value[0]!=='"'||value[value.length-1]!=='"')return null;let inner=value.slice(1,-1);return inner.includes('"')?null:inner}function parseStringArray(value){let trimmed=value.trim();if(trimmed.length<2||trimmed[0]!=="["||trimmed[trimmed.length-1]!=="]")return null;let inner=trimmed.slice(1,-1).trim();if(inner.length===0)return[];let parts=inner.split(","),out=[];for(let part of parts){let element=parseQuotedString(part.trim());if(element===null)return null;out.push(element)}return out}function parseBridgeConfigToml(text){let lines=text.split(`
2
+ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.30"}});import path3 from"path";function hasControlChars(value){for(let i=0;i<value.length;i++){let code=value.charCodeAt(i);if(code<=31||code===127)return!0}return!1}function validateRepoName(raw){if(typeof raw!="string")return{ok:!1,error:"repo_name must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"repo_name must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"repo_name must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"repo_name must not contain control characters"}:{ok:!0,value}}function validateMcpTarget(raw){if(typeof raw!="string")return{ok:!1,error:"mcp target must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"mcp target must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"mcp target must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"mcp target must not contain control characters"}:{ok:!0,value}}function parseQuotedString(value){if(value.length<2||value[0]!=='"'||value[value.length-1]!=='"')return null;let inner=value.slice(1,-1);return inner.includes('"')?null:inner}function parseStringArray(value){let trimmed=value.trim();if(trimmed.length<2||trimmed[0]!=="["||trimmed[trimmed.length-1]!=="]")return null;let inner=trimmed.slice(1,-1).trim();if(inner.length===0)return[];let parts=inner.split(","),out=[];for(let part of parts){let element=parseQuotedString(part.trim());if(element===null)return null;out.push(element)}return out}function parseBridgeConfigToml(text){let lines=text.split(`
3
3
  `),repoName,sawRepoName=!1,mcp=[],currentMcp=null;for(let i=0;i<lines.length;i++){let lineNo=i+1,line=lines[i].trim();if(line.length===0||line.startsWith("#"))continue;if(line==="[[mcp]]"){currentMcp={headerLine:lineNo},mcp.push(currentMcp);continue}if(line.startsWith("["))return{ok:!1,kind:"parse-error",error:`Unsupported table header on line ${lineNo}; only [[mcp]] is allowed`};let eq=line.indexOf("=");if(eq===-1)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; expected key = "value"`};let key=line.slice(0,eq).trim(),rawValue=line.slice(eq+1).trim();if(key.length===0)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; missing key`};if(currentMcp===null){if(key==="repo_name"){if(sawRepoName)return{ok:!1,kind:"parse-error",error:`Duplicate repo_name on line ${lineNo}`};sawRepoName=!0;let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};let validated=validateRepoName(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};repoName=validated.value;continue}return key==="target"?{ok:!1,kind:"parse-error",error:`target on line ${lineNo} must appear inside an [[mcp]] section`}:{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' on line ${lineNo}`}}if(key==="args"){if(currentMcp.args!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate args on line ${lineNo}`};let arr=parseStringArray(rawValue);if(arr===null)return{ok:!1,kind:"parse-error",error:`Expected a string array for 'args' on line ${lineNo}`};currentMcp.args=arr;continue}if(key==="target"||key==="command"||key==="secret_bundle"){let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};if(key==="target"){if(currentMcp.target!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate target on line ${lineNo}`};let validated=validateMcpTarget(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};currentMcp.target=validated.value;continue}if(key==="command"){if(currentMcp.command!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate command on line ${lineNo}`};currentMcp.command=stringValue;continue}if(currentMcp.secretBundle!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate secret_bundle on line ${lineNo}`};currentMcp.secretBundle=stringValue;continue}return{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' inside [[mcp]] on line ${lineNo}`}}if(!sawRepoName||repoName===void 0)return{ok:!1,kind:"validation-error",error:"Missing required repo_name"};let cleaned=[];for(let entry of mcp){if(entry.target===void 0)return{ok:!1,kind:"validation-error",error:`An [[mcp]] section on line ${entry.headerLine} is missing its target`};if(entry.target!=="bapi"){if(entry.command===void 0||entry.command.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty command`};if(entry.args===void 0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires an args array`};if(entry.secretBundle===void 0||entry.secretBundle.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty secret_bundle`}}let clean={target:entry.target};entry.command!==void 0&&(clean.command=entry.command),entry.args!==void 0&&(clean.args=entry.args),entry.secretBundle!==void 0&&(clean.secretBundle=entry.secretBundle),cleaned.push(clean)}return{ok:!0,manifest:{repoName,mcp:cleaned}}}function bridgeConfigPath(projectRoot){return path3.join(projectRoot,".bridge","config")}async function readBridgeConfig(projectRoot,deps){let filePath=bridgeConfigPath(projectRoot),raw;try{raw=await deps.readFile(filePath)}catch(err){return err&&typeof err=="object"&&err.code==="ENOENT"?{ok:!1,kind:"missing"}:{ok:!1,kind:"parse-error",error:"Unable to read .bridge/config"}}return parseBridgeConfigToml(raw)}async function deriveRepoNameFromGitCommonDir(projectRoot,deps){if(!deps.runCommand)return{ok:!1,error:"Cannot derive repo name: no command runner available"};let result;try{result=await deps.runCommand("git",["rev-parse","--git-common-dir"],{cwd:projectRoot})}catch(err){return{ok:!1,error:`git rev-parse --git-common-dir failed: ${err instanceof Error?err.message:String(err)}`}}if(result.exitCode!==0){let reason=(result.stderr||result.stdout||"").trim();return{ok:!1,error:`git rev-parse --git-common-dir failed${reason?`: ${reason}`:""}`}}let commonDir=result.stdout.trim();if(commonDir.length===0)return{ok:!1,error:"git rev-parse --git-common-dir returned no output"};let segments=(path3.isAbsolute(commonDir)?commonDir:path3.resolve(projectRoot,commonDir)).split(/[\\/]+/).filter(s=>s.length>0),gitIndex=segments.lastIndexOf(".git");if(gitIndex<1)return{ok:!1,error:"Unable to derive repo name from git common dir"};let derived=segments[gitIndex-1],validated=validateRepoName(derived);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:`Derived repo name is invalid: ${validated.error}`}}async function resolveRepoNameForProjectRoot(projectRoot,deps){let read=await readBridgeConfig(projectRoot,deps);return read.ok?{ok:!0,value:read.manifest.repoName}:read.kind==="missing"?deriveRepoNameFromGitCommonDir(projectRoot,deps):{ok:!1,error:read.error}}var init_bridge_config=__esm({"src/bridge-config.ts"(){"use strict"}});import path6 from"path";function getPrimaryCredentialStorePath(deps){let xdg=deps.env.XDG_CONFIG_HOME;return xdg&&xdg.trim().length>0?path6.join(xdg,"bridge","credentials.json"):path6.join(deps.homedir(),".config","bridge","credentials.json")}function getFallbackCredentialStorePath(deps){return path6.join(deps.homedir(),".bridge","credentials.json")}async function resolveCredentialStorePath(deps){let primaryPath=getPrimaryCredentialStorePath(deps),fallbackPath=getFallbackCredentialStorePath(deps);try{return await deps.stat(primaryPath),{found:!0,path:primaryPath,isPrimary:!0}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{found:!0,path:primaryPath,isPrimary:!0}}try{return await deps.stat(fallbackPath),{found:!0,path:fallbackPath,isPrimary:!1}}catch{return{found:!1,primaryPath,fallbackPath}}}function warnIfInsecureCredentialFileMode(statResult,platform,filePath,stderr){platform!=="win32"&&(statResult.mode&63)!==0&&(stderr??(m=>process.stderr.write(`${m}
4
4
  `)))(`Warning: credentials file ${filePath} is group/world-accessible; it should be mode 0600 (run: chmod 600 ${filePath}).`)}function parseCredentialStoreJson(text){let data;try{data=JSON.parse(text)}catch{return{ok:!1,error:"credentials file is not valid JSON"}}if(data===null||typeof data!="object"||Array.isArray(data))return{ok:!1,error:"credentials file must be a JSON object"};for(let[key,value]of Object.entries(data)){if(value===null||typeof value!="object"||Array.isArray(value))return{ok:!1,error:`credentials entry "${key}" must be an object of secret names to strings`};for(let[secretName,secretValue]of Object.entries(value))if(typeof secretValue!="string")return{ok:!1,error:`credentials entry "${key}" has a non-string value for "${secretName}"`}}return{ok:!0,value:data}}function collectEnvValues(env,requiredKeys){let values={},missing=[];for(let key of requiredKeys){let raw=(env[key]??"").trim();raw.length>0?values[key]=raw:missing.push(key)}return{values,missing}}async function resolveCredentialBundle(bundleKey,requiredKeys,deps){let primaryPath=getPrimaryCredentialStorePath(deps),fromEnv=collectEnvValues(deps.env,requiredKeys);if(fromEnv.missing.length===0)return{ok:!0,values:fromEnv.values,source:"env"};let resolution=await resolveCredentialStorePath(deps);if(!resolution.found)return{ok:!1,kind:"not-found",error:`No credentials found for "${bundleKey}". Set ${fromEnv.missing.join(", ")} in the environment, or add them under "${bundleKey}" in ${primaryPath}.`};try{let statResult=await deps.stat(resolution.path);warnIfInsecureCredentialFileMode(statResult,deps.platform,resolution.path,deps.stderr)}catch{}let raw;try{raw=await deps.readFile(resolution.path)}catch{return{ok:!1,kind:"read-error",error:`Unable to read credentials file at ${resolution.path}.`}}let parsed=parseCredentialStoreJson(raw);if(!parsed.ok)return{ok:!1,kind:"parse-error",error:`Invalid credentials file at ${resolution.path}: ${parsed.error}.`};let entry=parsed.value[bundleKey]??{},values={...fromEnv.values},stillMissing=[];for(let key of fromEnv.missing){let storeValue=typeof entry[key]=="string"?entry[key].trim():"";storeValue.length>0?values[key]=storeValue:stillMissing.push(key)}return stillMissing.length>0?{ok:!1,kind:"missing-key",error:`No usable value(s) for ${stillMissing.join(", ")} in "${bundleKey}". Add them under "${bundleKey}" in ${primaryPath}, or set them in the environment.`}:{ok:!0,values,source:"file"}}async function resolveBapiCredentials(repoName,deps){let result=await resolveCredentialBundle(`bapi:${repoName}`,["BAPI_API_KEY"],deps);return result.ok?{ok:!0,credentials:{apiKey:result.values.BAPI_API_KEY,source:result.source}}:{ok:!1,kind:result.kind,error:result.error}}function formatCredentialStoreJson(value){return`${JSON.stringify(value,null,2)}
5
5
  `}async function readCredentialStoreJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch(err){return(err&&typeof err=="object"?err.code:void 0)==="ENOENT"?{state:"missing"}:{state:"error",kind:"read-error",error:`Unable to read credentials file at ${filePath}.`}}let parsed=parseCredentialStoreJson(raw);return parsed.ok?{state:"present",value:parsed.value}:{state:"error",kind:"parse-error",error:`Invalid credentials file at ${filePath}: ${parsed.error}.`}}async function mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps){let fallbackPath=getFallbackCredentialStorePath(deps),fallback=await readCredentialStoreJsonIfPresent(fallbackPath,deps);return fallback.state==="present"?{base:{...fallback.value},migratedFallback:!0}:{base:{},migratedFallback:!1}}function defaultTempSuffix(){return tempSuffixCounter+=1,`${process.pid}.${tempSuffixCounter}`}function getCredentialStoreLockPath(deps){return`${getPrimaryCredentialStorePath(deps)}.lock`}async function acquireCredentialStoreLock(deps){let open3=deps.open;if(!open3)return{ok:!0,release:async()=>{}};let lockPath=getCredentialStoreLockPath(deps),isPosix=deps.platform!=="win32",now=deps.now??(()=>Date.now()),sleep3=deps.sleep??(ms=>new Promise(r=>setTimeout(r,ms))),release=async()=>{if(deps.unlink)try{await deps.unlink(lockPath)}catch{}},tryAcquire=async()=>{try{return await(await open3(lockPath,"wx",isPosix?384:void 0)).close(),{ok:!0}}catch(err){return{ok:!1,contended:(err&&typeof err=="object"?err.code:void 0)==="EEXIST"}}};try{await deps.mkdir(path6.dirname(lockPath),{recursive:!0})}catch{return{ok:!1,error:`Unable to prepare the credentials directory for ${lockPath}.`}}let deadline=now()+LOCK_TIMEOUT_MS;for(;;){let attempt=await tryAcquire();if(attempt.ok)return{ok:!0,release};if(!attempt.contended)return{ok:!1,error:`Unable to acquire the credentials lock at ${lockPath}.`};if(now()>=deadline)break;await sleep3(LOCK_POLL_INTERVAL_MS)}return await release(),(await tryAcquire()).ok?{ok:!0,release}:{ok:!1,error:`Timed out waiting for the credentials lock at ${lockPath} (another install may be running).`}}async function withCredentialStoreLock(deps,fn,onLockError){let lock=await acquireCredentialStoreLock(deps);if(!lock.ok)return onLockError(lock.error);try{return await fn()}finally{await lock.release()}}async function durablyReplaceCredentialStoreJson(primaryPath,value,deps){let open3=deps.open;if(!open3)return{ok:!1,kind:"durable-unavailable",error:`Cannot durably write ${primaryPath}: no file-handle primitive is available to fsync the write.`};let dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(value),isPosix=deps.platform!=="win32",handle;try{await deps.mkdir(dir,{recursive:!0}),handle=await open3(tempPath,"w",isPosix?384:void 0),await handle.writeFile(json,{encoding:"utf-8"}),await handle.sync(),await handle.close(),handle=void 0,isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(handle)try{await handle.close()}catch{}if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,kind:"write-error",error:`Failed to durably write the credentials file at ${primaryPath}.`}}try{let dirHandle=await open3(dir,"r");try{await dirHandle.sync()}finally{await dirHandle.close()}}catch{}return{ok:!0}}function getBootstrapPendingTarget(repoName){return`${BOOTSTRAP_PENDING_TARGET_PREFIX}${(repoName??"").trim()}`}function getBapiTarget(repoName){return`bapi:${(repoName??"").trim()}`}async function loadStoreForMutation(deps){let primaryPath=getPrimaryCredentialStorePath(deps),primary=await readCredentialStoreJsonIfPresent(primaryPath,deps);return primary.state==="error"?{ok:!1,kind:primary.kind,error:primary.error}:primary.state==="present"?{ok:!0,base:{...primary.value}}:{ok:!0,base:(await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps)).base}}function hasExistingBapiKey(store,repoName){let entry=store[getBapiTarget(repoName)];return!!entry&&typeof entry.BAPI_API_KEY=="string"&&entry.BAPI_API_KEY.trim().length>0}function readMatchingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return null;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD],fingerprint=entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD];return typeof secret!="string"||secret.trim().length===0||fingerprint!==inviteFingerprint?null:{keySecret:secret}}function hasConflictingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return!1;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD];return typeof secret!="string"||secret.trim().length===0?!1:entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]!==inviteFingerprint}function pendingConflictError(target,primaryPath){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.`}async function prepareBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot prepare a bootstrap-invite credential: a non-empty repo name is required."}:fingerprint.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-fingerprint",error:"Cannot prepare a bootstrap-invite credential: the invite fingerprint was empty."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base;if(hasExistingBapiKey(base,repoName)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(repoName),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(repoName)} in ${primaryPath}.`};let existing=readMatchingPending(base,repoName,fingerprint);if(existing)return{ok:!0,path:primaryPath,target,keySecret:existing.keySecret,reused:!0};if(hasConflictingPending(base,repoName,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath)};let keySecret=params.generateKeySecret(),next={...base,[target]:{...base[target]??{},[BOOTSTRAP_PENDING_SECRET_FIELD]:keySecret,[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]:fingerprint}},written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret,reused:!1}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function repointBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),fromRepo=(params.fromRepoName??"").trim(),toRepo=(params.toRepoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(toRepo);return fromRepo.length===0||toRepo.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot re-point a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,fromRepo,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(fromRepo)} in ${primaryPath}.`};if(toRepo===fromRepo)return{ok:!0,path:primaryPath,target,keySecret:pending.keySecret};if(hasExistingBapiKey(base,toRepo)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(toRepo),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(toRepo)} in ${primaryPath}.`};let destination=base[getBootstrapPendingTarget(toRepo)];if(hasConflictingPending(base,toRepo,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath)};let next={...base};delete next[getBootstrapPendingTarget(fromRepo)],next[target]={...destination??{},[BOOTSTRAP_PENDING_SECRET_FIELD]:pending.keySecret,[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]:fingerprint};let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret:pending.keySecret}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function promoteBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBapiTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot promote a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,repoName,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(repoName)} in ${primaryPath}.`};let hadKey=hasExistingBapiKey(base,repoName);if(hadKey&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target,kind:"credential-conflict",error:`A credential already exists for ${target} in ${primaryPath}.`};let next={...base};delete next[getBootstrapPendingTarget(repoName)],next[target]={...base[target]??{},BAPI_API_KEY:pending.keySecret};let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,action:hadKey?"updated":"created"}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function upsertBapiCredential(repoName,apiKey,deps){let primaryPath=getPrimaryCredentialStorePath(deps),trimmedRepo=(repoName??"").trim(),trimmedKey=(apiKey??"").trim(),target=`bapi:${trimmedRepo}`;return trimmedRepo.length===0?{ok:!1,path:primaryPath,target:"bapi:",kind:"invalid-repo",error:"Cannot store BAPI_API_KEY: a non-empty repo name is required."}:trimmedKey.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-key",error:`Cannot store BAPI_API_KEY for ${target}: the provided key was empty.`}:withCredentialStoreLock(deps,async()=>{let primary=await readCredentialStoreJsonIfPresent(primaryPath,deps),base,migratedFallback=!1;if(primary.state==="error")return{ok:!1,path:primaryPath,target,kind:primary.kind,error:primary.error};if(primary.state==="present")base={...primary.value};else{let seeded=await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);base=seeded.base,migratedFallback=seeded.migratedFallback}let existingEntry=base[target],action=!!existingEntry&&typeof existingEntry.BAPI_API_KEY=="string"&&existingEntry.BAPI_API_KEY.length>0?"updated":"created",nextEntry={...existingEntry??{},BAPI_API_KEY:trimmedKey},next={...base,[target]:nextEntry},dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(next),isPosix=deps.platform!=="win32";try{await deps.mkdir(dir,{recursive:!0});let writeOptions=isPosix?{encoding:"utf-8",mode:384}:{encoding:"utf-8"};await deps.writeFile(tempPath,json,writeOptions),isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,path:primaryPath,target,kind:"write-error",error:`Failed to write credentials file at ${primaryPath}.`}}return{ok:!0,path:primaryPath,target,action,migratedFallback}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}var tempSuffixCounter,LOCK_POLL_INTERVAL_MS,LOCK_TIMEOUT_MS,BOOTSTRAP_PENDING_TARGET_PREFIX,BOOTSTRAP_PENDING_SECRET_FIELD,BOOTSTRAP_PENDING_FINGERPRINT_FIELD,init_credential_store=__esm({"src/credential-store.ts"(){"use strict";tempSuffixCounter=0;LOCK_POLL_INTERVAL_MS=50,LOCK_TIMEOUT_MS=5e3;BOOTSTRAP_PENDING_TARGET_PREFIX="bootstrap-pending:",BOOTSTRAP_PENDING_SECRET_FIELD="BAPI_API_KEY",BOOTSTRAP_PENDING_FINGERPRINT_FIELD="BOOTSTRAP_INVITE_FINGERPRINT"}});async function resolveStartTicketsRepoName(deps){let fromEnv=deps.env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim();try{let result=await readBridgeConfig(deps.cwd,{readFile:deps.readFile});if(result.ok&&result.manifest.repoName)return result.manifest.repoName}catch{}return null}async function resolveRequiredStartTicketsRepoName(deps){let repoName=await resolveStartTicketsRepoName(deps);return repoName?{ok:!0,repoName}:{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"}}var init_start_tickets_repo=__esm({"src/start-tickets-repo.ts"(){"use strict";init_bridge_config()}});function getThirdPartyTargetDefinition(target){return THIRD_PARTY_TARGETS[target]}function validateThirdPartyTargetManifestEntry(entry){return entry.command===void 0||entry.command.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty command`}:entry.args===void 0?{ok:!1,error:`target '${entry.target}' requires an args array`}:entry.secretBundle===void 0||entry.secretBundle.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty secret_bundle`}:{ok:!0}}async function resolveThirdPartyTargetEnv(definition,secretBundle,deps){let result=await resolveCredentialBundle(secretBundle,definition.requiredEnvKeys,deps);if(!result.ok)return{ok:!1,error:result.error};let env={};for(let key of definition.requiredEnvKeys)env[key]=result.values[key];return{ok:!0,env}}var THIRD_PARTY_TARGETS,init_third_party_mcp_targets=__esm({"src/third-party-mcp-targets.ts"(){"use strict";init_credential_store();THIRD_PARTY_TARGETS={sfcc:{target:"sfcc",requiredEnvKeys:["SFCC_CLIENT_ID","SFCC_CLIENT_SECRET"]}}}});import path7 from"node:path";function buildMcpShimCommand(invocation,target,absoluteWorktreePath){return invocation.form==="absolute-build-path"?{command:invocation.nodeExecutable,args:[invocation.serverEntryPath,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}:{command:"npx",args:["-y",invocation.packageSpec,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}}function resolvePackageRootFromModuleUrl(moduleUrl){let pathname;try{pathname=decodeURIComponent(new URL(moduleUrl).pathname)}catch{return null}/^\/[A-Za-z]:/.test(pathname)&&(pathname=pathname.slice(1));let segments=pathname.split(/[\\/]/),markerIndex=-1;for(let i=segments.length-1;i>=0;i--)if(segments[i]==="src"||segments[i]==="build"){markerIndex=i;break}return markerIndex<=0?null:segments.slice(0,markerIndex).join("/")}function basenameAnySep(filePath){let segments=filePath.split(/[\\/]/);return segments[segments.length-1]??""}function resolveMcpShimInvocationForRuntime(deps){let nodeExecutable=deps.nodeExecutable??"node",packageRoot=resolvePackageRootFromModuleUrl(deps.moduleUrl);if(packageRoot){let candidate=`${packageRoot}/build/index.js`;if(deps.fileExists(candidate))return{form:"absolute-build-path",nodeExecutable,serverEntryPath:candidate}}let argv1=deps.argv1;return typeof argv1=="string"&&argv1.length>0&&path7.isAbsolute(argv1)&&basenameAnySep(argv1)==="index.js"&&deps.fileExists(argv1)?{form:"absolute-build-path",nodeExecutable,serverEntryPath:argv1}:{form:"npm-channel",command:"npx",packageSpec:deps.npmPackageSpec??DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC}}var DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC,init_mcp_server_invocation=__esm({"src/mcp-server-invocation.ts"(){"use strict";DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC="@bridge_gpt/mcp-server@latest"}});import path8 from"path";function pathApiForProvisioningPlatform(platform){return platform==="win32"?path8.win32:path8.posix}function normalizeWorktreePathForRegistration(worktreePath,deps){let api=pathApiForProvisioningPlatform(deps.platform);if(typeof worktreePath!="string"||worktreePath.trim().length===0)return{ok:!1,error:"worktree path is empty"};let resolved=api.isAbsolute(worktreePath)?api.normalize(worktreePath):api.resolve(deps.cwd,worktreePath);return api.isAbsolute(resolved)?{ok:!0,path:resolved}:{ok:!1,error:`unable to resolve an absolute worktree path from "${worktreePath}"`}}function serverNameForMcpTarget(target){return target==="bapi"?"bridge-api":target}function buildShimMcpServerEntry(target,absoluteWorktreePath,invocation){return buildMcpShimCommand(invocation,target,absoluteWorktreePath)}function buildMcpServerEntriesForManifest(manifest,absoluteWorktreePath,invocation){let entries={},warnings=[];for(let mcp of manifest.mcp){if(mcp.target==="bapi"){entries[serverNameForMcpTarget("bapi")]=buildShimMcpServerEntry("bapi",absoluteWorktreePath,invocation);continue}if(!getThirdPartyTargetDefinition(mcp.target)){warnings.push(`MCP target '${mcp.target}' is not a supported third-party target; skipping its registration.`);continue}let validation=validateThirdPartyTargetManifestEntry(mcp);if(!validation.ok){warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);continue}entries[serverNameForMcpTarget(mcp.target)]=buildShimMcpServerEntry(mcp.target,absoluteWorktreePath,invocation)}return{entries,warnings,registrationForm:invocation.form}}function getWorktreeMcpRegistrationTargets(worktreePath,platform){let api=pathApiForProvisioningPlatform(platform);return[{filePath:api.join(worktreePath,".mcp.json"),topLevelKey:"mcpServers"},{filePath:api.join(worktreePath,".cursor","mcp.json"),topLevelKey:"mcpServers"}]}function claudeSettingsTargetForWorktree(worktreePath,platform){return pathApiForProvisioningPlatform(platform).join(worktreePath,".claude","settings.local.json")}function mergeEnabledMcpjsonServers(existing,serverNames){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result.enabledMcpjsonServers,merged=Array.isArray(current)?current.filter(name=>typeof name=="string"):[];for(let name of serverNames)merged.includes(name)||merged.push(name);return result.enabledMcpjsonServers=merged,result}function mergeMcpRegistrations(existing,topLevelKey,entries){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result[topLevelKey],servers=current&&typeof current=="object"&&!Array.isArray(current)?{...current}:{};for(let[name,entry]of Object.entries(entries))servers[name]=entry;return result[topLevelKey]=servers,result}async function writeMcpRegistrationFile(target,entries,deps){let api=pathApiForProvisioningPlatform(deps.platform),existing;try{let raw=await deps.readFile(target.filePath);try{existing=JSON.parse(raw)}catch{return{ok:!1,error:`existing ${target.filePath} contains malformed JSON; not overwriting`}}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{ok:!1,error:`unable to read ${target.filePath}`}}let merged=mergeMcpRegistrations(existing,target.topLevelKey,entries);try{await deps.mkdir(api.dirname(target.filePath),{recursive:!0}),await deps.writeFile(target.filePath,`${JSON.stringify(merged,null,2)}
6
6
  `)}catch{return{ok:!1,error:`failed to write ${target.filePath}`}}return{ok:!0}}async function writeClaudeServerTrustSettings(worktreePath,serverNames,deps){let api=pathApiForProvisioningPlatform(deps.platform),filePath=claudeSettingsTargetForWorktree(worktreePath,deps.platform),existing;try{let raw=await deps.readFile(filePath);try{existing=JSON.parse(raw)}catch{return{ok:!1,error:`existing ${filePath} contains malformed JSON; not overwriting`}}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{ok:!1,error:`unable to read ${filePath}`}}let merged=mergeEnabledMcpjsonServers(existing,serverNames);try{await deps.mkdir(api.dirname(filePath),{recursive:!0}),await deps.writeFile(filePath,`${JSON.stringify(merged,null,2)}
7
- `)}catch{return{ok:!1,error:`failed to write ${filePath}`}}return{ok:!0}}function withWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function withWarnings(row,warnings){return warnings.length===0?row:{...row,warnings:[...row.warnings??[],...warnings]}}async function provisionMcpRegistrationForWorktree(row,deps){if(row.status!=="created"||!row.path)return row;let read=await readBridgeConfig(row.path,{readFile:deps.readFile});if(!read.ok)return read.kind==="missing"?withWarning(row,"MCP provisioning skipped: .bridge/config is missing in the worktree."):withWarning(row,"MCP provisioning skipped: .bridge/config is malformed or invalid.");let normalized=normalizeWorktreePathForRegistration(row.path,deps);if(!normalized.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${normalized.error}`};let built=buildMcpServerEntriesForManifest(read.manifest,normalized.path,deps.mcpServerInvocation);if(Object.keys(built.entries).length===0)return withWarnings(withWarning(row,"MCP registration skipped: .bridge/config declares no supported MCP targets."),built.warnings);let targets=getWorktreeMcpRegistrationTargets(normalized.path,deps.platform);for(let target of targets){let result2=await writeMcpRegistrationFile(target,built.entries,deps);if(!result2.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${result2.error}`}}let result={...row,mcpRegistrationForm:built.registrationForm};result=withWarnings(result,built.warnings),built.registrationForm!=="absolute-build-path"&&(result=withWarning(result,"MCP registration used npm-channel fallback because an on-disk build entry was not resolvable."));let serverNames=Object.keys(built.entries),trust=await writeClaudeServerTrustSettings(normalized.path,serverNames,deps);return trust.ok||(result=withWarning(result,`Claude MCP trust pre-approval skipped: ${trust.error}`)),result}async function provisionMcpRegistrationsForCreatedWorktrees(rows,deps){let out=[];for(let row of rows)out.push(await provisionMcpRegistrationForWorktree(row,deps));return out}var init_mcp_provisioning=__esm({"src/mcp-provisioning.ts"(){"use strict";init_bridge_config();init_third_party_mcp_targets();init_mcp_server_invocation()}});import path9 from"path";async function readJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch{return{state:"missing"}}try{return{state:"present",value:JSON.parse(raw)}}catch{return{state:"malformed",path:filePath}}}function flagValue(args,flag){let index=args.indexOf(flag);if(index>=0&&index+1<args.length)return args[index+1]}function isBridgeApiShimEntry(entry,worktreeRoot){if(!entry||typeof entry!="object"||Array.isArray(entry))return!1;let candidate=entry;if(typeof candidate.command!="string"||candidate.command.length===0||!Array.isArray(candidate.args))return!1;let args=candidate.args.filter(a=>typeof a=="string");if(candidate.command==="npx"){if(!args.some(a=>a.startsWith("@bridge_gpt/mcp-server")))return!1}else if(typeof args[0]!="string"||args[0].length===0)return!1;return!(!args.includes("mcp-invoke")||flagValue(args,"--target")!=="bapi"||flagValue(args,"--project-root")!==worktreeRoot)}async function probeWorktreeMcpRegistration(worktreeRoot,deps){let targets=[path9.join(worktreeRoot,".mcp.json"),path9.join(worktreeRoot,".cursor","mcp.json")];for(let filePath of targets){let read=await readJsonIfPresent(filePath,deps);if(read.state!=="present")continue;let doc=read.value;if(!doc||typeof doc!="object"||Array.isArray(doc))continue;let servers=doc.mcpServers;if(!servers||typeof servers!="object"||Array.isArray(servers))continue;let entry=servers["bridge-api"];if(isBridgeApiShimEntry(entry,worktreeRoot))return{found:!0,detail:`bridge-api shim registered in ${path9.basename(path9.dirname(filePath))===".cursor"?".cursor/mcp.json":".mcp.json"}`}}return{found:!1,detail:"No worktree .mcp.json or .cursor/mcp.json points at the bridge-api mcp-invoke shim. Re-run start-tickets to provision the worktree MCP registration."}}var init_mcp_registration_doctor=__esm({"src/mcp-registration-doctor.ts"(){"use strict"}});import path10 from"path";function isSupportedStartTicketsPlatform(platform){return platform==="darwin"||platform==="win32"||platform==="linux"}function unsupportedPlatformMessage(platform){return`start-tickets does not support this platform: '${platform}' is unsupported. Supported platforms are darwin, win32, and linux. Use --dry-run to preview the intended commands on any OS.`}function resolveWorktrunkBinary(platform,env){let override=env[WORKTRUNK_BINARY_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return platform==="win32"?DEFAULT_WINDOWS_WORKTRUNK_BINARY:DEFAULT_POSIX_WORKTRUNK_BINARY}function commandSucceeded(result){return result.exitCode===0}function getCommandProbe(tool,platform){return platform==="win32"?{file:"where",args:[tool]}:{file:"which",args:[tool]}}async function isCommandOnPath(deps,tool){let probe=getCommandProbe(tool,deps.platform),result=await deps.runCommand(probe.file,probe.args);return commandSucceeded(result)}async function resolveFirstCommandOnPath(deps,candidates){for(let candidate of candidates)if(await isCommandOnPath(deps,candidate))return candidate;return null}async function requireBashUsable(deps){let result=await deps.runCommand("bash",["--version"]);return commandSucceeded(result)?{ok:!0}:{ok:!1,error:`bash is required on Windows but could not be run. ${GIT_FOR_WINDOWS_BASH_HINT}`}}function appendDoctorHint(error){return`${error} Hint: Run ${START_TICKETS_DOCTOR_COMMAND} for a read-only start-tickets diagnostics report.`}function hintForPlatform(hints,platform){return platform==="win32"?hints.win32:platform==="linux"?hints.linux:hints.darwin}function commandDescriptor(tool,label,installHint){return{id:tool,label,installHint,preflightError:`Required command not found on PATH: ${tool}.`,probe:async deps=>await isCommandOnPath(deps,tool)?{found:!0,detail:"found on PATH"}:{found:!1}}}function worktrunkDescriptor(binary){return{id:"worktrunk",label:`Worktrunk (${binary})`,installHint:WORKTRUNK_INSTALL_HINTS,preflightError:`Required command not found on PATH: ${binary}.`,probe:async deps=>await isCommandOnPath(deps,binary)?{found:!0,detail:"found on PATH"}:{found:!1}}}function gitBashDescriptor(){return{id:"git-bash",label:"Git Bash (bash)",installHint:GIT_BASH_INSTALL_HINTS,probe:async deps=>{let result=await requireBashUsable(deps);return result.ok?{found:!0,detail:"bash --version ok"}:{found:!1,detail:result.error}}}}function windowsLauncherDescriptor(){let candidates=[WINDOWS_TERMINAL_COMMAND,...WINDOWS_POWERSHELL_CANDIDATES];return{id:"windows-launcher",label:"Windows Terminal or PowerShell",installHint:WINDOWS_LAUNCHER_INSTALL_HINTS,preflightError:"Windows Terminal (wt.exe) or PowerShell is required to open a tab. Install Windows Terminal or ensure powershell.exe is on PATH.",probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,candidates);return found?{found:!0,detail:found}:{found:!1}}}}function gitWorkTreeDescriptor(){return{id:"git-work-tree",label:"git work tree",installHint:GIT_WORK_TREE_INSTALL_HINTS,probe:async deps=>{let revParse=await deps.runCommand("git",["rev-parse","--is-inside-work-tree"],{cwd:deps.cwd});return commandSucceeded(revParse)?revParse.stdout.trim()!=="true"?{found:!1,detail:"start-tickets must be run inside a git work tree (git rev-parse --is-inside-work-tree did not report 'true')."}:{found:!0,detail:"inside a git work tree"}:{found:!1,detail:"start-tickets must be run inside a git repository (git rev-parse --is-inside-work-tree failed)."}}}}function agentDescriptor(agent){return{id:agent.command,label:agent.name,installHint:agent.installHint,authNote:agent.authNote,preflightError:`Required command not found on PATH: ${agent.command}.`,probe:async deps=>await isCommandOnPath(deps,agent.command)?{found:!0,detail:"found on PATH"}:{found:!1}}}function uvDescriptor(){return commandDescriptor("uv","uv",UV_INSTALL_HINTS)}function reviewTicketsGitDescriptor(){return{id:"review-tickets-git",label:"git (required by review-tickets base-branch fetch)",installHint:REVIEW_TICKETS_GIT_INSTALL_HINTS,probe:async deps=>await isCommandOnPath(deps,"git")?{found:!0,detail:"found on PATH"}:{found:!1,detail:"review-tickets' parent-fetch-once base pin needs git unless --no-refresh-base is passed"}}}function astGrepDescriptor(){return{id:"ast-grep",label:"ast-grep (or sg)",installHint:AST_GREP_INSTALL_HINTS,probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,["ast-grep","sg"]);return found?{found:!0,detail:`found on PATH (${found})`}:{found:!1}}}}function lizardDescriptor(){return commandDescriptor("lizard","lizard",LIZARD_INSTALL_HINTS)}function ripgrepDescriptor(){return commandDescriptor("rg","ripgrep (rg)",RIPGREP_INSTALL_HINTS)}function credentialResolutionDescriptor(){return{id:"bapi-credentials",label:"Bridge API credential resolution",installHint:CREDENTIAL_RESOLUTION_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15,stat:stat11,homedir}=deps;if(!readFile15||!stat11||!homedir)return{found:!1,detail:"credential probe unavailable (no read-only filesystem access)"};let repoName=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:readFile15});if(!repoName)return{found:!1,detail:"cannot determine repo identity (set BAPI_REPO_NAME or add a valid .bridge/config). "+CREDENTIAL_RESOLUTION_HINT};let storePath=getPrimaryCredentialStorePath({env:deps.env,homedir}),result=await resolveBapiCredentials(repoName,{env:deps.env,homedir,platform:deps.platform,readFile:readFile15,stat:stat11});return result.ok?{found:!0,detail:result.credentials.source==="env"?`credentials resolvable via env for repo ${repoName}`:`credentials resolvable via store target bapi:${repoName} at ${storePath}`}:{found:!1,detail:`no usable BAPI_API_KEY for bapi:${repoName} (store path ${storePath}). `+CREDENTIAL_RESOLUTION_HINT}}}}function worktreeMcpReachabilityDescriptor(){return{id:"worktree-mcp-registration",label:"Worktree MCP registration reachability",installHint:WORKTREE_MCP_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15}=deps;if(!readFile15)return{found:!1,detail:"registration probe unavailable (no read-only filesystem access)"};let result=await probeWorktreeMcpRegistration(deps.cwd,{readFile:readFile15});return{found:result.found,detail:result.detail}}}}function normalizeCheckoutPath(rawPath){let trimmed=rawPath.trim(),resolved=path10.resolve(trimmed);return resolved.length>1?resolved.replace(/[\\/]+$/,""):resolved}async function resolveRepoRootPath(deps,targetPath){let result=await deps.runCommand("git",["-C",targetPath,"rev-parse","--show-toplevel"],{cwd:deps.cwd});if(commandSucceeded(result)){let top=result.stdout.trim();if(top.length>0)return normalizeCheckoutPath(top)}return normalizeCheckoutPath(targetPath)}function isLiveSourceDispatchOverrideEnabled(env){let raw=env[CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV];return raw===void 0?!1:["1","true","yes","on"].includes(raw.trim().toLowerCase())}async function evaluateLiveSourceGuard(deps){let rawLiveSource=deps.env[CONDUCTOR_LIVE_SOURCE_PATH_ENV];if(!rawLiveSource||rawLiveSource.trim().length===0)return{state:"not-configured",detail:`no live dev-server source configured (${CONDUCTOR_LIVE_SOURCE_PATH_ENV} unset) \u2014 guard inactive`};let baseRepoPath=await resolveRepoRootPath(deps,deps.cwd),liveSourcePath=await resolveRepoRootPath(deps,rawLiveSource);return baseRepoPath===liveSourcePath?{state:"collision",detail:`COLLISION: the conductor base checkout (${baseRepoPath}) is the SAME checkout as the configured live dev-server source (${CONDUCTOR_LIVE_SOURCE_PATH_ENV}). Unattended dispatch would create worktrees / touch branches under a running dev server.`,baseRepoPath,liveSourcePath}:{state:"safe",detail:`safe: conductor base checkout (${baseRepoPath}) differs from the configured live dev-server source (${liveSourcePath})`,baseRepoPath,liveSourcePath}}function liveSourceGuardDescriptor(){return{id:LIVE_SOURCE_GUARD_ID,label:"Conductor live-source checkout guard",installHint:LIVE_SOURCE_GUARD_INSTALL_HINTS,probe:async deps=>{let outcome2=await evaluateLiveSourceGuard(deps);return{found:outcome2.state!=="collision",detail:outcome2.detail}}}}function getPreflightPrereqDescriptors(platform,env){if(!isSupportedStartTicketsPlatform(platform))return{ok:!1,error:unsupportedPlatformMessage(platform)};let worktrunkBinary=resolveWorktrunkBinary(platform,env),descriptors=[worktrunkDescriptor(worktrunkBinary)];return descriptors.push(commandDescriptor("git","git",GIT_INSTALL_HINTS)),platform==="darwin"?descriptors.push(commandDescriptor("osascript","osascript",OSASCRIPT_INSTALL_HINTS)):platform==="win32"?(descriptors.push(gitBashDescriptor()),descriptors.push(windowsLauncherDescriptor())):descriptors.push(commandDescriptor(TMUX_COMMAND,TMUX_COMMAND,TMUX_INSTALL_HINTS)),descriptors.push(gitWorkTreeDescriptor()),{ok:!0,descriptors}}function getDoctorOnlyPrereqDescriptors(_platform,_env,agent){return[uvDescriptor(),agentDescriptor(agent),credentialResolutionDescriptor(),worktreeMcpReachabilityDescriptor(),astGrepDescriptor(),lizardDescriptor(),ripgrepDescriptor(),reviewTicketsGitDescriptor(),liveSourceGuardDescriptor()]}function getDoctorPrereqDescriptors(platform,env,agent){let preflight=getPreflightPrereqDescriptors(platform,env);return preflight.ok?{ok:!0,descriptors:[...preflight.descriptors,...getDoctorOnlyPrereqDescriptors(platform,env,agent)]}:preflight}async function probePrerequisite(deps,descriptor){let outcome2;try{outcome2=await descriptor.probe(deps)}catch(err){outcome2={found:!1,detail:err instanceof Error?err.message:String(err)}}return{id:descriptor.id,label:descriptor.label,found:outcome2.found,detail:outcome2.detail,installHint:hintForPlatform(descriptor.installHint,deps.platform),authNote:descriptor.authNote}}async function enforcePreflightPrerequisites(deps,options={}){let descriptorsResult=getPreflightPrereqDescriptors(deps.platform,deps.env);if(!descriptorsResult.ok)return{ok:!1,reason:"unsupported-platform",error:descriptorsResult.error};for(let descriptor of descriptorsResult.descriptors){let probed=await probePrerequisite(deps,descriptor);if(!probed.found)return{ok:!1,reason:"missing-prerequisite",error:descriptor.preflightError??probed.detail??`Missing prerequisite: ${descriptor.label}.`}}if(options.enforceLiveSourceGuard){let guard=await evaluateLiveSourceGuard(deps);if(guard.state==="collision"){let base=`Live-source checkout guard (${LIVE_SOURCE_GUARD_ID}): ${guard.detail}`;return isLiveSourceDispatchOverrideEnabled(deps.env)?{ok:!0,warning:`${base} Proceeding anyway because ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV} is set \u2014 dispatching under a live dev-server checkout can corrupt the operator's working state.`}:{ok:!1,reason:"live-source-collision",error:`${base} Refusing unattended dispatch. ${LIVE_SOURCE_GUARD_HINT}`}}}return{ok:!0}}var WORKTRUNK_BINARY_OVERRIDE_ENV,WINDOWS_TERMINAL_COMMAND,WINDOWS_POWERSHELL_CANDIDATES,DEFAULT_WINDOWS_WORKTRUNK_BINARY,DEFAULT_POSIX_WORKTRUNK_BINARY,TMUX_COMMAND,GIT_FOR_WINDOWS_BASH_HINT,START_TICKETS_DOCTOR_COMMAND,CONDUCTOR_LIVE_SOURCE_PATH_ENV,CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV,LIVE_SOURCE_GUARD_ID,WORKTRUNK_INSTALL_HINTS,GIT_INSTALL_HINTS,OSASCRIPT_INSTALL_HINTS,TMUX_INSTALL_HINTS,GIT_BASH_INSTALL_HINTS,WINDOWS_LAUNCHER_INSTALL_HINTS,GIT_WORK_TREE_INSTALL_HINTS,UV_INSTALL_HINTS,REVIEW_TICKETS_GIT_INSTALL_HINTS,AST_GREP_INSTALL_HINTS,LIZARD_INSTALL_HINTS,RIPGREP_SHELL_FUNCTION_CAVEAT,RIPGREP_INSTALL_HINTS,CREDENTIAL_RESOLUTION_HINT,CREDENTIAL_RESOLUTION_INSTALL_HINTS,WORKTREE_MCP_HINT,WORKTREE_MCP_INSTALL_HINTS,LIVE_SOURCE_GUARD_HINT,LIVE_SOURCE_GUARD_INSTALL_HINTS,init_start_tickets_prereqs=__esm({"src/start-tickets-prereqs.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_mcp_registration_doctor();WORKTRUNK_BINARY_OVERRIDE_ENV="BAPI_WORKTRUNK_BIN",WINDOWS_TERMINAL_COMMAND="wt.exe",WINDOWS_POWERSHELL_CANDIDATES=["powershell.exe","powershell"],DEFAULT_WINDOWS_WORKTRUNK_BINARY="git-wt",DEFAULT_POSIX_WORKTRUNK_BINARY="wt",TMUX_COMMAND="tmux",GIT_FOR_WINDOWS_BASH_HINT="Install Git for Windows / Git Bash \u2014 Worktrunk runs its pre-start / post-start hooks via Git Bash.",START_TICKETS_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",CONDUCTOR_LIVE_SOURCE_PATH_ENV="BAPI_CONDUCTOR_LIVE_SOURCE_PATH",CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV="BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH",LIVE_SOURCE_GUARD_ID="conductor-live-source";WORKTRUNK_INSTALL_HINTS={darwin:"brew install worktrunk",win32:"Install Worktrunk via winget; it installs as the git-wt alias on Windows.",linux:"See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev"},GIT_INSTALL_HINTS={darwin:"xcode-select --install (or brew install git)",linux:"Install git with your distro package manager, e.g. apt install git",win32:"Install Git for Windows: https://git-scm.com/download/win"},OSASCRIPT_INSTALL_HINTS={darwin:"osascript ships with macOS; if it is missing, repair your macOS command line tools.",linux:"osascript is macOS-only.",win32:"osascript is macOS-only."},TMUX_INSTALL_HINTS={darwin:"brew install tmux",linux:"Install tmux with your distro package manager, e.g. apt install tmux",win32:"tmux is used only on Linux."},GIT_BASH_INSTALL_HINTS={darwin:GIT_FOR_WINDOWS_BASH_HINT,linux:GIT_FOR_WINDOWS_BASH_HINT,win32:GIT_FOR_WINDOWS_BASH_HINT},WINDOWS_LAUNCHER_INSTALL_HINTS={darwin:"Windows Terminal / PowerShell are used only on Windows.",linux:"Windows Terminal / PowerShell are used only on Windows.",win32:"Install Windows Terminal (winget install Microsoft.WindowsTerminal) or ensure powershell.exe is on PATH."},GIT_WORK_TREE_INSTALL_HINTS={darwin:"Run start-tickets from inside a git repository work tree.",linux:"Run start-tickets from inside a git repository work tree.",win32:"Run start-tickets from inside a git repository work tree."},UV_INSTALL_HINTS={darwin:"brew install uv",linux:"curl -LsSf https://astral.sh/uv/install.sh | sh",win32:'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'};REVIEW_TICKETS_GIT_INSTALL_HINTS={darwin:`${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,linux:`${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,win32:`${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`};AST_GREP_INSTALL_HINTS={darwin:"uv tool install ast-grep-cli",linux:"uv tool install ast-grep-cli",win32:"uv tool install ast-grep-cli"},LIZARD_INSTALL_HINTS={darwin:"uv tool install lizard",linux:"uv tool install lizard",win32:"uv tool install lizard"},RIPGREP_SHELL_FUNCTION_CAVEAT="a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.",RIPGREP_INSTALL_HINTS={darwin:`brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,linux:`Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,win32:`winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`};CREDENTIAL_RESOLUTION_HINT='Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials.',CREDENTIAL_RESOLUTION_INSTALL_HINTS={darwin:CREDENTIAL_RESOLUTION_HINT,linux:CREDENTIAL_RESOLUTION_HINT,win32:CREDENTIAL_RESOLUTION_HINT};WORKTREE_MCP_HINT="Re-run start-tickets to provision the worktree MCP registration (.mcp.json / .cursor/mcp.json pointing at the mcp-invoke shim).",WORKTREE_MCP_INSTALL_HINTS={darwin:WORKTREE_MCP_HINT,linux:WORKTREE_MCP_HINT,win32:WORKTREE_MCP_HINT};LIVE_SOURCE_GUARD_HINT=`Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it differs from this conductor base checkout, or dispatch the conductor from a separate clone. To override the guard and dispatch anyway (not recommended), set ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`,LIVE_SOURCE_GUARD_INSTALL_HINTS={darwin:LIVE_SOURCE_GUARD_HINT,linux:LIVE_SOURCE_GUARD_HINT,win32:LIVE_SOURCE_GUARD_HINT}}});function isValidModelAlias(value){return typeof value=="string"&&value.length>0&&MODEL_ALIAS_PATTERN.test(value)}function isModelTier(value){return value==="cheap"||value==="basic"||value==="premium"}function listAgentNames(){return Object.keys(AGENT_REGISTRY)}function isAgentName(value){return listAgentNames().includes(value)}function resolveAgentSpec(name){let resolved=name??DEFAULT_AGENT_NAME;return isAgentName(resolved)?AGENT_REGISTRY[resolved]:null}function formatValidAgentNames(){return listAgentNames().join(", ")}function resolveModelAlias(agent,tier,overrides){if(!agent.supportsModelOverride||!tier)return null;let override=overrides?.[tier],candidate=typeof override=="string"&&override.trim().length>0?override.trim():agent.tierModels[tier];return typeof candidate!="string"||!isValidModelAlias(candidate)||agent.staticModelAliasAllowlist&&!agent.staticModelAliasAllowlist.includes(candidate)?null:candidate}var MODEL_ALIAS_PATTERN,AGENT_REGISTRY,DEFAULT_AGENT_NAME,init_agent_registry=__esm({"src/agent-registry.ts"(){"use strict";MODEL_ALIAS_PATTERN=/^[A-Za-z0-9._:-]+$/;AGENT_REGISTRY={claude:{name:"claude",command:"claude",promptArgStyle:"positional",installHint:{darwin:"npm install -g @anthropic-ai/claude-code",linux:"npm install -g @anthropic-ai/claude-code",win32:"npm install -g @anthropic-ai/claude-code"},authNote:"Claude Code authenticates interactively on first run \u2014 follow its login/auth prompt if asked.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"haiku",basic:"sonnet",premium:"opus"},staticModelAliasAllowlist:["haiku","sonnet","opus"]},"cursor-agent":{name:"cursor-agent",command:"cursor-agent",promptArgStyle:"positional",installHint:{darwin:"curl https://cursor.com/install -fsSL | bash",linux:"curl https://cursor.com/install -fsSL | bash",win32:"irm 'https://cursor.com/install?win32=true' | iex"},authNote:"Run cursor-agent login to authenticate; doctor checks PATH presence only, not login state.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"auto",basic:"claude-4.6-sonnet-medium",premium:"claude-opus-4-8-thinking-high"},interactiveLaunchArgs:["--trust"]}},DEFAULT_AGENT_NAME="claude"}});import path11 from"node:path";function asHookEntries(value){return Array.isArray(value)?value:[]}function entriesContainCommand(entries,command){return entries.some(entry=>Array.isArray(entry?.hooks)&&entry.hooks.some(h=>h&&h.type==="command"&&h.command===command))}function detectExistingPreToolUseMatcher(settings){let hooks=settings.hooks;if(hooks===null||typeof hooks!="object"||Array.isArray(hooks))return;let entries=asHookEntries(hooks.PreToolUse);for(let entry of entries)if(typeof entry?.matcher=="string")return entry.matcher}function mergeClaudeSettingsWithCommandHook(settings,command,events,options={}){let hooks={...settings.hooks!==null&&typeof settings.hooks=="object"&&!Array.isArray(settings.hooks)?settings.hooks:{}},allEvents=[...events];options.enablePreToolUse&&!allEvents.includes("PreToolUse")&&allEvents.push("PreToolUse");for(let event of allEvents){let entries=asHookEntries(hooks[event]);if(entriesContainCommand(entries,command)){hooks[event]=entries;continue}let newEntry={hooks:[{type:"command",command}]};event==="PreToolUse"&&(newEntry.matcher=options.preToolUseMatcher??detectExistingPreToolUseMatcher(settings)??DEFAULT_PRE_TOOL_USE_MATCHER),hooks[event]=[...entries,newEntry]}return{...settings,hooks}}async function provisionClaudeSettingsForWorktree(worktreePath,mergeSettings,deps){let claudeDir=path11.join(worktreePath,".claude"),settingsPath=path11.join(claudeDir,"settings.local.json"),existing={},raw=null;try{raw=await deps.readFile(settingsPath)}catch{raw=null}if(raw!==null)try{let parsed=JSON.parse(raw);if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json is not a JSON object"};existing=parsed}catch{return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json contains invalid JSON"}}let merged=mergeSettings(existing);try{await deps.mkdir(claudeDir,{recursive:!0}),await deps.writeFile(settingsPath,`${JSON.stringify(merged,null,2)}
7
+ `)}catch{return{ok:!1,error:`failed to write ${filePath}`}}return{ok:!0}}function withWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function withWarnings(row,warnings){return warnings.length===0?row:{...row,warnings:[...row.warnings??[],...warnings]}}async function provisionMcpRegistrationForWorktree(row,deps){if(row.status!=="created"||!row.path)return row;let read=await readBridgeConfig(row.path,{readFile:deps.readFile});if(!read.ok)return read.kind==="missing"?withWarning(row,"MCP provisioning skipped: .bridge/config is missing in the worktree."):withWarning(row,"MCP provisioning skipped: .bridge/config is malformed or invalid.");let normalized=normalizeWorktreePathForRegistration(row.path,deps);if(!normalized.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${normalized.error}`};let built=buildMcpServerEntriesForManifest(read.manifest,normalized.path,deps.mcpServerInvocation);if(Object.keys(built.entries).length===0)return withWarnings(withWarning(row,"MCP registration skipped: .bridge/config declares no supported MCP targets."),built.warnings);let targets=getWorktreeMcpRegistrationTargets(normalized.path,deps.platform);for(let target of targets){let result2=await writeMcpRegistrationFile(target,built.entries,deps);if(!result2.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${result2.error}`}}let result={...row,mcpRegistrationForm:built.registrationForm};result=withWarnings(result,built.warnings),built.registrationForm!=="absolute-build-path"&&(result=withWarning(result,"MCP registration used npm-channel fallback because an on-disk build entry was not resolvable."));let serverNames=Object.keys(built.entries),trust=await writeClaudeServerTrustSettings(normalized.path,serverNames,deps);return trust.ok||(result=withWarning(result,`Claude MCP trust pre-approval skipped: ${trust.error}`)),result}async function provisionMcpRegistrationsForCreatedWorktrees(rows,deps){let out=[];for(let row of rows)out.push(await provisionMcpRegistrationForWorktree(row,deps));return out}var init_mcp_provisioning=__esm({"src/mcp-provisioning.ts"(){"use strict";init_bridge_config();init_third_party_mcp_targets();init_mcp_server_invocation()}});import path9 from"path";async function readJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch{return{state:"missing"}}try{return{state:"present",value:JSON.parse(raw)}}catch{return{state:"malformed",path:filePath}}}function flagValue(args,flag){let index=args.indexOf(flag);if(index>=0&&index+1<args.length)return args[index+1]}function isBridgeApiShimEntry(entry,worktreeRoot){if(!entry||typeof entry!="object"||Array.isArray(entry))return!1;let candidate=entry;if(typeof candidate.command!="string"||candidate.command.length===0||!Array.isArray(candidate.args))return!1;let args=candidate.args.filter(a=>typeof a=="string");if(candidate.command==="npx"){if(!args.some(a=>a.startsWith("@bridge_gpt/mcp-server")))return!1}else if(typeof args[0]!="string"||args[0].length===0)return!1;return!(!args.includes("mcp-invoke")||flagValue(args,"--target")!=="bapi"||flagValue(args,"--project-root")!==worktreeRoot)}async function probeWorktreeMcpRegistration(worktreeRoot,deps){let targets=[path9.join(worktreeRoot,".mcp.json"),path9.join(worktreeRoot,".cursor","mcp.json")];for(let filePath of targets){let read=await readJsonIfPresent(filePath,deps);if(read.state!=="present")continue;let doc=read.value;if(!doc||typeof doc!="object"||Array.isArray(doc))continue;let servers=doc.mcpServers;if(!servers||typeof servers!="object"||Array.isArray(servers))continue;let entry=servers["bridge-api"];if(isBridgeApiShimEntry(entry,worktreeRoot))return{found:!0,detail:`bridge-api shim registered in ${path9.basename(path9.dirname(filePath))===".cursor"?".cursor/mcp.json":".mcp.json"}`}}return{found:!1,detail:"No worktree .mcp.json or .cursor/mcp.json points at the bridge-api mcp-invoke shim. Re-run start-tickets to provision the worktree MCP registration."}}var init_mcp_registration_doctor=__esm({"src/mcp-registration-doctor.ts"(){"use strict"}});import path10 from"path";function isSupportedStartTicketsPlatform(platform){return platform==="darwin"||platform==="win32"||platform==="linux"}function unsupportedPlatformMessage(platform){return`start-tickets does not support this platform: '${platform}' is unsupported. Supported platforms are darwin, win32, and linux. Use --dry-run to preview the intended commands on any OS.`}function resolveWorktrunkBinary(platform,env){let override=env[WORKTRUNK_BINARY_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return platform==="win32"?DEFAULT_WINDOWS_WORKTRUNK_BINARY:DEFAULT_POSIX_WORKTRUNK_BINARY}function commandSucceeded(result){return result.exitCode===0}function getCommandProbe(tool,platform){return platform==="win32"?{file:"where",args:[tool]}:{file:"which",args:[tool]}}async function isCommandOnPath(deps,tool){let probe=getCommandProbe(tool,deps.platform),result=await deps.runCommand(probe.file,probe.args);return commandSucceeded(result)}async function resolveFirstCommandOnPath(deps,candidates){for(let candidate of candidates)if(await isCommandOnPath(deps,candidate))return candidate;return null}async function requireBashUsable(deps){let result=await deps.runCommand("bash",["--version"]);return commandSucceeded(result)?{ok:!0}:{ok:!1,error:`bash is required on Windows but could not be run. ${GIT_FOR_WINDOWS_BASH_HINT}`}}function appendDoctorHint(error){return`${error} Hint: Run ${START_TICKETS_DOCTOR_COMMAND} for a read-only start-tickets diagnostics report.`}function hintForPlatform(hints,platform){return platform==="win32"?hints.win32:platform==="linux"?hints.linux:hints.darwin}function commandDescriptor(tool,label,installHint){return{id:tool,label,installHint,preflightError:`Required command not found on PATH: ${tool}.`,probe:async deps=>await isCommandOnPath(deps,tool)?{found:!0,detail:"found on PATH"}:{found:!1}}}function worktrunkDescriptor(binary){return{id:"worktrunk",label:`Worktrunk (${binary})`,installHint:WORKTRUNK_INSTALL_HINTS,preflightError:`Required command not found on PATH: ${binary}.`,probe:async deps=>await isCommandOnPath(deps,binary)?{found:!0,detail:"found on PATH"}:{found:!1}}}function gitBashDescriptor(){return{id:"git-bash",label:"Git Bash (bash)",installHint:GIT_BASH_INSTALL_HINTS,probe:async deps=>{let result=await requireBashUsable(deps);return result.ok?{found:!0,detail:"bash --version ok"}:{found:!1,detail:result.error}}}}function windowsLauncherDescriptor(){let candidates=[WINDOWS_TERMINAL_COMMAND,...WINDOWS_POWERSHELL_CANDIDATES];return{id:"windows-launcher",label:"Windows Terminal or PowerShell",installHint:WINDOWS_LAUNCHER_INSTALL_HINTS,preflightError:"Windows Terminal (wt.exe) or PowerShell is required to open a tab. Install Windows Terminal or ensure powershell.exe is on PATH.",probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,candidates);return found?{found:!0,detail:found}:{found:!1}}}}function gitWorkTreeDescriptor(){return{id:"git-work-tree",label:"git work tree",installHint:GIT_WORK_TREE_INSTALL_HINTS,probe:async deps=>{let revParse=await deps.runCommand("git",["rev-parse","--is-inside-work-tree"],{cwd:deps.cwd});return commandSucceeded(revParse)?revParse.stdout.trim()!=="true"?{found:!1,detail:"start-tickets must be run inside a git work tree (git rev-parse --is-inside-work-tree did not report 'true')."}:{found:!0,detail:"inside a git work tree"}:{found:!1,detail:"start-tickets must be run inside a git repository (git rev-parse --is-inside-work-tree failed)."}}}}function agentDescriptor(agent){return{id:agent.command,label:agent.name,installHint:agent.installHint,authNote:agent.authNote,preflightError:`Required command not found on PATH: ${agent.command}.`,probe:async deps=>await isCommandOnPath(deps,agent.command)?{found:!0,detail:"found on PATH"}:{found:!1}}}function uvDescriptor(){return commandDescriptor("uv","uv",UV_INSTALL_HINTS)}function reviewTicketsGitDescriptor(){return{id:"review-tickets-git",label:"git (required by review-tickets base-branch fetch)",installHint:REVIEW_TICKETS_GIT_INSTALL_HINTS,probe:async deps=>await isCommandOnPath(deps,"git")?{found:!0,detail:"found on PATH"}:{found:!1,detail:"review-tickets' parent-fetch-once base pin needs git unless --no-refresh-base is passed"}}}function astGrepDescriptor(){return{id:"ast-grep",label:"ast-grep (or sg)",installHint:AST_GREP_INSTALL_HINTS,probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,["ast-grep","sg"]);return found?{found:!0,detail:`found on PATH (${found})`}:{found:!1}}}}function lizardDescriptor(){return commandDescriptor("lizard","lizard",LIZARD_INSTALL_HINTS)}function ripgrepDescriptor(){return commandDescriptor("rg","ripgrep (rg)",RIPGREP_INSTALL_HINTS)}function credentialResolutionDescriptor(){return{id:"bapi-credentials",label:"Bridge API credential resolution",installHint:CREDENTIAL_RESOLUTION_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15,stat:stat11,homedir}=deps;if(!readFile15||!stat11||!homedir)return{found:!1,detail:"credential probe unavailable (no read-only filesystem access)"};let repoName=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:readFile15});if(!repoName)return{found:!1,detail:"cannot determine repo identity (set BAPI_REPO_NAME or add a valid .bridge/config). "+CREDENTIAL_RESOLUTION_HINT};let storePath=getPrimaryCredentialStorePath({env:deps.env,homedir}),result=await resolveBapiCredentials(repoName,{env:deps.env,homedir,platform:deps.platform,readFile:readFile15,stat:stat11});return result.ok?{found:!0,detail:result.credentials.source==="env"?`credentials resolvable via env for repo ${repoName}`:`credentials resolvable via store target bapi:${repoName} at ${storePath}`}:{found:!1,detail:`no usable BAPI_API_KEY for bapi:${repoName} (store path ${storePath}). `+CREDENTIAL_RESOLUTION_HINT}}}}function worktreeMcpReachabilityDescriptor(){return{id:"worktree-mcp-registration",label:"Worktree MCP registration reachability",installHint:WORKTREE_MCP_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15}=deps;if(!readFile15)return{found:!1,detail:"registration probe unavailable (no read-only filesystem access)"};let result=await probeWorktreeMcpRegistration(deps.cwd,{readFile:readFile15});return{found:result.found,detail:result.detail}}}}function normalizeCheckoutPath(rawPath){let trimmed=rawPath.trim(),resolved=path10.resolve(trimmed);return resolved.length>1?resolved.replace(/[\\/]+$/,""):resolved}async function resolveRepoRootPath(deps,targetPath){let result=await deps.runCommand("git",["-C",targetPath,"rev-parse","--show-toplevel"],{cwd:deps.cwd});if(commandSucceeded(result)){let top=result.stdout.trim();if(top.length>0)return normalizeCheckoutPath(top)}return normalizeCheckoutPath(targetPath)}function isLiveSourceDispatchOverrideEnabled(env){let raw=env[CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV];return raw===void 0?!1:["1","true","yes","on"].includes(raw.trim().toLowerCase())}async function evaluateLiveSourceGuard(deps){let rawLiveSource=deps.env[CONDUCTOR_LIVE_SOURCE_PATH_ENV];if(!rawLiveSource||rawLiveSource.trim().length===0)return{state:"not-configured",detail:`no live dev-server source configured (${CONDUCTOR_LIVE_SOURCE_PATH_ENV} unset) \u2014 guard inactive`};let baseRepoPath=await resolveRepoRootPath(deps,deps.cwd),liveSourcePath=await resolveRepoRootPath(deps,rawLiveSource);return baseRepoPath===liveSourcePath?{state:"collision",detail:`COLLISION: the conductor base checkout (${baseRepoPath}) is the SAME checkout as the configured live dev-server source (${CONDUCTOR_LIVE_SOURCE_PATH_ENV}). Unattended dispatch would create worktrees / touch branches under a running dev server.`,baseRepoPath,liveSourcePath}:{state:"safe",detail:`safe: conductor base checkout (${baseRepoPath}) differs from the configured live dev-server source (${liveSourcePath})`,baseRepoPath,liveSourcePath}}function liveSourceGuardDescriptor(){return{id:LIVE_SOURCE_GUARD_ID,label:"Conductor live-source checkout guard",installHint:LIVE_SOURCE_GUARD_INSTALL_HINTS,probe:async deps=>{let outcome2=await evaluateLiveSourceGuard(deps);return{found:outcome2.state!=="collision",detail:outcome2.detail}}}}function getPreflightPrereqDescriptors(platform,env){if(!isSupportedStartTicketsPlatform(platform))return{ok:!1,error:unsupportedPlatformMessage(platform)};let worktrunkBinary=resolveWorktrunkBinary(platform,env),descriptors=[worktrunkDescriptor(worktrunkBinary)];return descriptors.push(commandDescriptor("git","git",GIT_INSTALL_HINTS)),platform==="darwin"?descriptors.push(commandDescriptor("osascript","osascript",OSASCRIPT_INSTALL_HINTS)):platform==="win32"?(descriptors.push(gitBashDescriptor()),descriptors.push(windowsLauncherDescriptor())):descriptors.push(commandDescriptor(TMUX_COMMAND,TMUX_COMMAND,TMUX_INSTALL_HINTS)),descriptors.push(gitWorkTreeDescriptor()),{ok:!0,descriptors}}function getDoctorOnlyPrereqDescriptors(_platform,_env,agent){return[uvDescriptor(),agentDescriptor(agent),credentialResolutionDescriptor(),worktreeMcpReachabilityDescriptor(),astGrepDescriptor(),lizardDescriptor(),ripgrepDescriptor(),reviewTicketsGitDescriptor(),liveSourceGuardDescriptor()]}function getDoctorPrereqDescriptors(platform,env,agent){let preflight=getPreflightPrereqDescriptors(platform,env);return preflight.ok?{ok:!0,descriptors:[...preflight.descriptors,...getDoctorOnlyPrereqDescriptors(platform,env,agent)]}:preflight}async function probePrerequisite(deps,descriptor){let outcome2;try{outcome2=await descriptor.probe(deps)}catch(err){outcome2={found:!1,detail:err instanceof Error?err.message:String(err)}}return{id:descriptor.id,label:descriptor.label,found:outcome2.found,detail:outcome2.detail,installHint:hintForPlatform(descriptor.installHint,deps.platform),authNote:descriptor.authNote}}async function enforcePreflightPrerequisites(deps,options={}){let descriptorsResult=getPreflightPrereqDescriptors(deps.platform,deps.env);if(!descriptorsResult.ok)return{ok:!1,reason:"unsupported-platform",error:descriptorsResult.error};for(let descriptor of descriptorsResult.descriptors){let probed=await probePrerequisite(deps,descriptor);if(!probed.found)return{ok:!1,reason:"missing-prerequisite",error:descriptor.preflightError??probed.detail??`Missing prerequisite: ${descriptor.label}.`}}if(options.enforceLiveSourceGuard){let guard=await evaluateLiveSourceGuard(deps);if(guard.state==="collision"){let base=`Live-source checkout guard (${LIVE_SOURCE_GUARD_ID}): ${guard.detail}`;return isLiveSourceDispatchOverrideEnabled(deps.env)?{ok:!0,warning:`${base} Proceeding anyway because ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV} is set \u2014 dispatching under a live dev-server checkout can corrupt the operator's working state.`}:{ok:!1,reason:"live-source-collision",error:`${base} Refusing unattended dispatch. ${LIVE_SOURCE_GUARD_HINT}`}}}return{ok:!0}}var WORKTRUNK_BINARY_OVERRIDE_ENV,WINDOWS_TERMINAL_COMMAND,WINDOWS_POWERSHELL_CANDIDATES,DEFAULT_WINDOWS_WORKTRUNK_BINARY,DEFAULT_POSIX_WORKTRUNK_BINARY,TMUX_COMMAND,GIT_FOR_WINDOWS_BASH_HINT,START_TICKETS_DOCTOR_COMMAND,CONDUCTOR_LIVE_SOURCE_PATH_ENV,CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV,LIVE_SOURCE_GUARD_ID,WORKTRUNK_INSTALL_HINTS,GIT_INSTALL_HINTS,OSASCRIPT_INSTALL_HINTS,TMUX_INSTALL_HINTS,GIT_BASH_INSTALL_HINTS,WINDOWS_LAUNCHER_INSTALL_HINTS,GIT_WORK_TREE_INSTALL_HINTS,UV_INSTALL_HINTS,REVIEW_TICKETS_GIT_INSTALL_HINTS,AST_GREP_INSTALL_HINTS,LIZARD_INSTALL_HINTS,RIPGREP_SHELL_FUNCTION_CAVEAT,RIPGREP_INSTALL_HINTS,CREDENTIAL_RESOLUTION_HINT,CREDENTIAL_RESOLUTION_INSTALL_HINTS,WORKTREE_MCP_HINT,WORKTREE_MCP_INSTALL_HINTS,LIVE_SOURCE_GUARD_HINT,LIVE_SOURCE_GUARD_INSTALL_HINTS,init_start_tickets_prereqs=__esm({"src/start-tickets-prereqs.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_mcp_registration_doctor();WORKTRUNK_BINARY_OVERRIDE_ENV="BAPI_WORKTRUNK_BIN",WINDOWS_TERMINAL_COMMAND="wt.exe",WINDOWS_POWERSHELL_CANDIDATES=["powershell.exe","powershell"],DEFAULT_WINDOWS_WORKTRUNK_BINARY="git-wt",DEFAULT_POSIX_WORKTRUNK_BINARY="wt",TMUX_COMMAND="tmux",GIT_FOR_WINDOWS_BASH_HINT="Install Git for Windows / Git Bash \u2014 Worktrunk runs its pre-start / post-start hooks via Git Bash.",START_TICKETS_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",CONDUCTOR_LIVE_SOURCE_PATH_ENV="BAPI_CONDUCTOR_LIVE_SOURCE_PATH",CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV="BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH",LIVE_SOURCE_GUARD_ID="conductor-live-source";WORKTRUNK_INSTALL_HINTS={darwin:"brew install worktrunk",win32:"Install Worktrunk via winget; it installs as the git-wt alias on Windows.",linux:"See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev"},GIT_INSTALL_HINTS={darwin:"xcode-select --install (or brew install git)",linux:"Install git with your distro package manager, e.g. apt install git",win32:"Install Git for Windows: https://git-scm.com/download/win"},OSASCRIPT_INSTALL_HINTS={darwin:"osascript ships with macOS; if it is missing, repair your macOS command line tools.",linux:"osascript is macOS-only.",win32:"osascript is macOS-only."},TMUX_INSTALL_HINTS={darwin:"brew install tmux",linux:"Install tmux with your distro package manager, e.g. apt install tmux",win32:"tmux is used only on Linux."},GIT_BASH_INSTALL_HINTS={darwin:GIT_FOR_WINDOWS_BASH_HINT,linux:GIT_FOR_WINDOWS_BASH_HINT,win32:GIT_FOR_WINDOWS_BASH_HINT},WINDOWS_LAUNCHER_INSTALL_HINTS={darwin:"Windows Terminal / PowerShell are used only on Windows.",linux:"Windows Terminal / PowerShell are used only on Windows.",win32:"Install Windows Terminal (winget install Microsoft.WindowsTerminal) or ensure powershell.exe is on PATH."},GIT_WORK_TREE_INSTALL_HINTS={darwin:"Run start-tickets from inside a git repository work tree.",linux:"Run start-tickets from inside a git repository work tree.",win32:"Run start-tickets from inside a git repository work tree."},UV_INSTALL_HINTS={darwin:"brew install uv",linux:"curl -LsSf https://astral.sh/uv/install.sh | sh",win32:'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'};REVIEW_TICKETS_GIT_INSTALL_HINTS={darwin:`${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,linux:`${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,win32:`${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`};AST_GREP_INSTALL_HINTS={darwin:"uv tool install ast-grep-cli",linux:"uv tool install ast-grep-cli",win32:"uv tool install ast-grep-cli"},LIZARD_INSTALL_HINTS={darwin:"uv tool install lizard",linux:"uv tool install lizard",win32:"uv tool install lizard"},RIPGREP_SHELL_FUNCTION_CAVEAT="a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.",RIPGREP_INSTALL_HINTS={darwin:`brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,linux:`Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,win32:`winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`};CREDENTIAL_RESOLUTION_HINT='Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials.',CREDENTIAL_RESOLUTION_INSTALL_HINTS={darwin:CREDENTIAL_RESOLUTION_HINT,linux:CREDENTIAL_RESOLUTION_HINT,win32:CREDENTIAL_RESOLUTION_HINT};WORKTREE_MCP_HINT="Re-run start-tickets to provision the worktree MCP registration (.mcp.json / .cursor/mcp.json pointing at the mcp-invoke shim).",WORKTREE_MCP_INSTALL_HINTS={darwin:WORKTREE_MCP_HINT,linux:WORKTREE_MCP_HINT,win32:WORKTREE_MCP_HINT};LIVE_SOURCE_GUARD_HINT=`Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it differs from this conductor base checkout, or dispatch the conductor from a separate clone. To override the guard and dispatch anyway (not recommended), set ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`,LIVE_SOURCE_GUARD_INSTALL_HINTS={darwin:LIVE_SOURCE_GUARD_HINT,linux:LIVE_SOURCE_GUARD_HINT,win32:LIVE_SOURCE_GUARD_HINT}}});function isValidModelAlias(value){return typeof value=="string"&&value.length>0&&MODEL_ALIAS_PATTERN.test(value)}function isModelTier(value){return value==="cheap"||value==="basic"||value==="premium"}function listAgentNames(){return Object.keys(AGENT_REGISTRY)}function isAgentName(value){return listAgentNames().includes(value)}function resolveAgentSpec(name){let resolved=name??DEFAULT_AGENT_NAME;return isAgentName(resolved)?AGENT_REGISTRY[resolved]:null}function formatValidAgentNames(){return listAgentNames().join(", ")}function resolveModelAlias(agent,tier,overrides){if(!agent.supportsModelOverride||!tier)return null;let override=overrides?.[tier],candidate=typeof override=="string"&&override.trim().length>0?override.trim():agent.tierModels[tier];return typeof candidate!="string"||!isValidModelAlias(candidate)||agent.staticModelAliasAllowlist&&!agent.staticModelAliasAllowlist.includes(candidate)?null:candidate}var MODEL_ALIAS_PATTERN,AGENT_REGISTRY,DEFAULT_AGENT_NAME,init_agent_registry=__esm({"src/agent-registry.ts"(){"use strict";MODEL_ALIAS_PATTERN=/^[A-Za-z0-9._:-]+$/;AGENT_REGISTRY={claude:{name:"claude",command:"claude",promptArgStyle:"positional",installHint:{darwin:"npm install -g @anthropic-ai/claude-code",linux:"npm install -g @anthropic-ai/claude-code",win32:"npm install -g @anthropic-ai/claude-code"},authNote:"Claude Code authenticates interactively on first run \u2014 follow its login/auth prompt if asked.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"haiku",basic:"sonnet",premium:"opus"},staticModelAliasAllowlist:["haiku","sonnet","opus"]},"cursor-agent":{name:"cursor-agent",command:"cursor-agent",promptArgStyle:"positional",installHint:{darwin:"curl https://cursor.com/install -fsSL | bash",linux:"curl https://cursor.com/install -fsSL | bash",win32:"irm 'https://cursor.com/install?win32=true' | iex"},authNote:"Run cursor-agent login to authenticate; doctor checks PATH presence only, not login state.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"auto",basic:"claude-4.6-sonnet-medium",premium:"claude-opus-4-8-thinking-high"}}},DEFAULT_AGENT_NAME="claude"}});import path11 from"node:path";function asHookEntries(value){return Array.isArray(value)?value:[]}function entriesContainCommand(entries,command){return entries.some(entry=>Array.isArray(entry?.hooks)&&entry.hooks.some(h=>h&&h.type==="command"&&h.command===command))}function detectExistingPreToolUseMatcher(settings){let hooks=settings.hooks;if(hooks===null||typeof hooks!="object"||Array.isArray(hooks))return;let entries=asHookEntries(hooks.PreToolUse);for(let entry of entries)if(typeof entry?.matcher=="string")return entry.matcher}function mergeClaudeSettingsWithCommandHook(settings,command,events,options={}){let hooks={...settings.hooks!==null&&typeof settings.hooks=="object"&&!Array.isArray(settings.hooks)?settings.hooks:{}},allEvents=[...events];options.enablePreToolUse&&!allEvents.includes("PreToolUse")&&allEvents.push("PreToolUse");for(let event of allEvents){let entries=asHookEntries(hooks[event]);if(entriesContainCommand(entries,command)){hooks[event]=entries;continue}let newEntry={hooks:[{type:"command",command}]};event==="PreToolUse"&&(newEntry.matcher=options.preToolUseMatcher??detectExistingPreToolUseMatcher(settings)??DEFAULT_PRE_TOOL_USE_MATCHER),hooks[event]=[...entries,newEntry]}return{...settings,hooks}}async function provisionClaudeSettingsForWorktree(worktreePath,mergeSettings,deps){let claudeDir=path11.join(worktreePath,".claude"),settingsPath=path11.join(claudeDir,"settings.local.json"),existing={},raw=null;try{raw=await deps.readFile(settingsPath)}catch{raw=null}if(raw!==null)try{let parsed=JSON.parse(raw);if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json is not a JSON object"};existing=parsed}catch{return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json contains invalid JSON"}}let merged=mergeSettings(existing);try{await deps.mkdir(claudeDir,{recursive:!0}),await deps.writeFile(settingsPath,`${JSON.stringify(merged,null,2)}
8
8
  `)}catch{return{ok:!1,reason:"io",error:"failed to write .claude/settings.local.json"}}return{ok:!0}}var DEFAULT_PRE_TOOL_USE_MATCHER,init_claude_settings=__esm({"src/claude-settings.ts"(){"use strict";DEFAULT_PRE_TOOL_USE_MATCHER="*"}});import{spawnSync}from"node:child_process";function normalizeRepoRelativePath(input){if(typeof input!="string")return null;let trimmed=input.trim();if(trimmed.length===0||trimmed.startsWith("/")||trimmed.startsWith("\\")||/^[A-Za-z]:[\\/]/.test(trimmed))return null;let p=trimmed.replace(/\\/g,"/");p.startsWith("./")&&(p=p.slice(2));let segments=p.split("/");if(segments.some(s=>s===".."))return null;let cleaned=segments.filter(s=>s!==""&&s!==".").join("/");return cleaned.length>0?cleaned:null}function normalizeDeclaredTouchedFiles(list){if(!Array.isArray(list))return[];let out=new Set;for(let item of list){let norm=normalizeRepoRelativePath(item);norm&&out.add(norm)}return Array.from(out).sort()}function parseDeclaredTouchedFilesFromEnv(env=process.env){let raw=env[DECLARED_TOUCHED_FILES_ENV];if(typeof raw!="string"||raw.trim().length===0)return{specified:!1};let parsed;try{parsed=JSON.parse(raw)}catch{return{specified:!1}}if(!Array.isArray(parsed))return{specified:!1};let files=normalizeDeclaredTouchedFiles(parsed);return files.length===0?{specified:!1}:{specified:!0,files}}function collectBranchChangedFiles(opts={}){let baseRef=opts.baseRef??FILE_SCOPE_GUARD_BASE_REF,spawn9=opts.spawnSyncFn??defaultSpawnSync,result;try{result=spawn9("git",["diff","--name-only",`${baseRef}...HEAD`],{cwd:opts.cwd,encoding:"utf-8",shell:!1})}catch{return{ok:!1,files:[]}}if(result.error||result.status!==0)return{ok:!1,files:[]};let stdout=typeof result.stdout=="string"?result.stdout:result.stdout?.toString("utf-8")??"",files=[],seen=new Set;for(let line of stdout.split(`
9
9
  `)){let norm=normalizeRepoRelativePath(line);norm&&!seen.has(norm)&&(seen.add(norm),files.push(norm))}return{ok:!0,files}}function analyzeDiffScope(input){if(!input.declared.specified)return{checked:!1,outOfScopeFiles:[],warning:null};let declaredSet=new Set(input.declared.files),outOfScope=input.changedFiles.filter(f=>!declaredSet.has(f)).sort();if(outOfScope.length===0)return{checked:!0,outOfScopeFiles:[],warning:null};let warning=`[file-scope-guard] ${input.ticketKey&&input.ticketKey.trim().length>0?input.ticketKey.trim():"unknown-ticket"}: ${outOfScope.length} file(s) changed outside the declared touched-file set (${input.declared.files.length} declared): ${outOfScope.join(", ")}. Warn-only \u2014 PR creation continues.`;return{checked:!0,outOfScopeFiles:outOfScope,warning}}function runFileScopeGuardCli(deps={}){let env=deps.env??process.env,writeOut=deps.writeOut??(m=>process.stdout.write(`${m}
10
10
  `)),writeErr=deps.writeErr??(m=>process.stderr.write(`${m}
@@ -106,7 +106,7 @@ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyN
106
106
  ORDER BY seq ASC
107
107
  LIMIT @limit`),toDelivered=db.prepare("UPDATE messages SET state = 'delivered', updated_at = datetime('now') WHERE seq = @seq AND state = 'pending'"),toAcked=db.prepare("UPDATE messages SET state = 'acked', acked_at = datetime('now'), updated_at = datetime('now') WHERE seq = @seq AND state = 'delivered'"),reread=db.prepare("SELECT * FROM messages WHERE seq = ?"),delivered=[];return db.transaction(()=>{let pending=selectPending.all({run_id:runId,worker_id:workerId,limit});for(let row of pending){if(toDelivered.run({seq:row.seq}).changes!==1)continue;insertRelayAuditEvent(db,{id:`message.delivered:${row.id}`,source:"conductor-worker",type:"message.delivered",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message delivered to worker",status:"delivered",details:{message_id:row.id,message_type:row.type}}}),toAcked.run({seq:row.seq}),insertRelayAuditEvent(db,{id:`message.acked:${row.id}`,source:"conductor-worker",type:"message.acked",run_id:row.run_id,worker_id:row.worker_id,producer:"worker-message-relay",observed_via:"message-relay",data:{summary:"supervisor message acknowledged by worker",status:"acked",details:{message_id:row.id,message_type:row.type}}});let finalRow=reread.get(row.seq);delivered.push(rowToConductorWorkerMessage(finalRow))}}).immediate(),{messages:delivered,count:delivered.length,acked_count:delivered.length}}finally{db.close()}}var ConductorPersistenceUnavailableError,LEDGER_NATIVE_MODULE_NAME,ConductorNativeModuleLoadError,databaseModulePromise,dbLoadFailure,dbLoadDiagnosticEmitted,BUSY_TIMEOUT_DEFAULT,BUSY_TIMEOUT_MIN,BUSY_TIMEOUT_MAX,RETENTION_DAYS_DEFAULT,RETENTION_DAYS_MAX,RETENTION_MAX_ROWS_DEFAULT,RETENTION_MAX_ROWS_MIN,RETENTION_MAX_ROWS_MAX,POLL_LIMIT_DEFAULT,POLL_LIMIT_MAX,MESSAGE_COOLDOWN_DEFAULT_MS,MESSAGE_COOLDOWN_MIN_MS,MESSAGE_COOLDOWN_MAX_MS,CHECK_MESSAGES_LIMIT_DEFAULT,CHECK_MESSAGES_LIMIT_MAX,WAIT_TIMEOUT_MAX_MS,WAIT_POLL_INTERVAL_MS,SUMMARY_FIELD_MAX_CHARS,CURRENT_CONDUCTOR_SCHEMA_VERSION,MESSAGE_TYPE_PATTERN,init_store=__esm({"src/conductor/store.ts"(){"use strict";init_taxonomy();init_errors();init_data_normalization();init_paths();ConductorPersistenceUnavailableError=class extends Error{constructor(message="Conductor persistence is unavailable: the optional 'better-sqlite3' native module could not be loaded."){super(message),this.name="ConductorPersistenceUnavailableError"}},LEDGER_NATIVE_MODULE_NAME="better-sqlite3",ConductorNativeModuleLoadError=class extends ConductorPersistenceUnavailableError{details;failureKind;constructor(failureKind,details){super("Conductor ledger native module failed to load for this Node runtime."),this.name="ConductorNativeModuleLoadError",this.failureKind=failureKind,this.details=details}};databaseModulePromise=null,dbLoadFailure=null,dbLoadDiagnosticEmitted=!1;BUSY_TIMEOUT_DEFAULT=1e4,BUSY_TIMEOUT_MIN=250,BUSY_TIMEOUT_MAX=12e4,RETENTION_DAYS_DEFAULT=30,RETENTION_DAYS_MAX=3650,RETENTION_MAX_ROWS_DEFAULT=5e4,RETENTION_MAX_ROWS_MIN=100,RETENTION_MAX_ROWS_MAX=1e7,POLL_LIMIT_DEFAULT=100,POLL_LIMIT_MAX=1e3,MESSAGE_COOLDOWN_DEFAULT_MS=3e5,MESSAGE_COOLDOWN_MIN_MS=1e3,MESSAGE_COOLDOWN_MAX_MS=864e5,CHECK_MESSAGES_LIMIT_DEFAULT=10,CHECK_MESSAGES_LIMIT_MAX=100,WAIT_TIMEOUT_MAX_MS=12e4,WAIT_POLL_INTERVAL_MS=500,SUMMARY_FIELD_MAX_CHARS=500;CURRENT_CONDUCTOR_SCHEMA_VERSION=8;MESSAGE_TYPE_PATTERN=/^[A-Za-z0-9._:-]{1,100}$/}});import{randomBytes}from"node:crypto";import{fileURLToPath}from"node:url";function randomCorrelationFragment(){return randomBytes(4).toString("hex")}function sanitizeIdSegment(value){return value.replace(/[^A-Za-z0-9_-]/g,"-")}function mintStartTicketsRunId(keys,fragment=randomCorrelationFragment()){return`${keys.length>0?keys[0]:"start-tickets"}-start-tickets-${fragment}`}function mintStartTicketsWorkerId(ticketKey,agentName,fragment=randomCorrelationFragment()){return`${ticketKey}-${sanitizeIdSegment(agentName)}-${fragment}`}function buildEpicIdentityEnv(epic){let env={BAPI_CONDUCTOR_EPIC_KEY:epic.epic_key,BAPI_CONDUCTOR_EPIC_RUN_ID:epic.epic_run_id,BAPI_CONDUCTOR_PLAN_VERSION:String(epic.plan_version)},declared=normalizeDeclaredTouchedFiles(epic.declared_touched_files);return declared.length>0&&(env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON=JSON.stringify(declared)),env}function defaultResolveBinPath(filename){return fileURLToPath(new URL(`./${filename}`,import.meta.url))}function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}async function createStartTicketsConductorContext(options,agent,deps){let resolveRepoName2=deps.resolveRepoName??resolveStartTicketsRepoName,repoName=null;try{repoName=await resolveRepoName2({env:deps.env,cwd:deps.cwd,readFile:deps.readFile})}catch{repoName=null}let gateName=nonEmpty(deps.env.BAPI_CONDUCTOR_GATE_NAME)?deps.env.BAPI_CONDUCTOR_GATE_NAME.trim():DEFAULT_CONDUCTOR_GATE_NAME,supervisorMode=nonEmpty(deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE)?deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE.trim():options.autoApprove?"auto":"interactive",resolveBinPath=deps.resolveBinPath??defaultResolveBinPath,context={runId:mintStartTicketsRunId(options.keys,deps.fragment),repoName,gateName,supervisorMode,agentName:agent.name,cliFile:resolveBinPath("conductor-bin.js"),conductorNodePath:deps.execPath??process.execPath,hookBinPath:resolveBinPath("conductor-claude-hook-bin.js")};return options.epic&&(context.epic=options.epic),context}function isConductorFlagEnabled(value){if(typeof value!="string")return!1;let v=value.trim().toLowerCase();return v==="1"||v==="true"}function buildConductorWorkerEnv(context,worker,parentEnv){let parentActiveGroups=new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));parentActiveGroups.add("conductor");let env={BAPI_CONDUCTOR_ENABLED:"1",BRIDGE_MCP_PROFILE:Array.from(parentActiveGroups).join(","),BAPI_CONDUCTOR_RUN_ID:context.runId,BAPI_CONDUCTOR_WORKER_ID:worker.workerId,BAPI_CONDUCTOR_TICKET_KEY:worker.ticketKey,BAPI_CONDUCTOR_WORKTREE_PATH:worker.worktreePath,BAPI_CONDUCTOR_GATE_NAME:context.gateName,BAPI_CONDUCTOR_SUPERVISOR_MODE:context.supervisorMode,BAPI_CONDUCTOR_CLI_FILE:context.cliFile,CONDUCTOR_NODE_PATH:context.conductorNodePath};if(context.repoName&&(env.BAPI_CONDUCTOR_REPO_NAME=context.repoName),context.epic){let epicEnv=buildEpicIdentityEnv(context.epic);for(let[k,v]of Object.entries(epicEnv))env[k]=v}isConductorFlagEnabled(parentEnv.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE)&&(env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE="1");for(let key of CONDUCTOR_TUNING_ENV_KEYS)nonEmpty(parentEnv[key])&&(env[key]=parentEnv[key].trim());return env}function shellQuotePath(value){return`'${value.replace(/'/g,"'\\''")}'`}function resolveConductorHookCommand(env,hookBinPath,execPath=process.execPath){return nonEmpty(env.BAPI_CONDUCTOR_HOOK_COMMAND)?env.BAPI_CONDUCTOR_HOOK_COMMAND:`${shellQuotePath(execPath)} ${shellQuotePath(hookBinPath)}`}function mergeClaudeSettingsWithConductorHook(settings,command,options={}){return mergeClaudeSettingsWithCommandHook(settings,command,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,options)}async function provisionConductorHookForWorktree(worktreePath,command,options,deps){return provisionClaudeSettingsForWorktree(worktreePath,existing=>mergeClaudeSettingsWithConductorHook(existing,command,{enablePreToolUse:options.enablePreToolUse,preToolUseMatcher:options.preToolUseMatcher??detectExistingPreToolUseMatcher(existing)}),deps)}async function provisionConductorHooksForRows(rows,context,deps){let isClaude=context.agentName==="claude",enablePreToolUse=isConductorFlagEnabled(deps.env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE),command=resolveConductorHookCommand(deps.env,context.hookBinPath,deps.execPath),fragment=deps.workerFragment,out=[];for(let row of rows){let base={...row,runId:context.runId};if(row.status!=="created"||!row.path){out.push(base);continue}if(!isClaude){out.push(base);continue}let workerId=mintStartTicketsWorkerId(row.key,context.agentName,fragment?fragment():randomCorrelationFragment()),conductorEnv=buildConductorWorkerEnv(context,{workerId,ticketKey:row.key,worktreePath:row.path},deps.env),result=await provisionConductorHookForWorktree(row.path,command,{enablePreToolUse},deps);if(!result.ok){out.push({...base,workerId,warnings:[...base.warnings??[],`conductor hook not injected: ${result.error}`]});continue}out.push({...base,workerId,conductorEnv,conductorHookInjected:!0})}return out}function buildStartTicketsRunStartedEventInput(context,rows,options){let worktreeRows=rows.filter(r=>typeof r.path=="string"&&r.path.length>0),workers=worktreeRows.map(r=>({ticket_key:r.key,worker_id:r.workerId??null,worktree_path:r.path??null,status:r.status})),subject=context.repoName??(options.keys.length>0?options.keys[0]:"start-tickets");return{source:"start-tickets",type:"run.started",run_id:context.runId,producer:"bridge-api-mcp-server",observed_via:"start-tickets",subject,data:{summary:"start-tickets run started",status:"started",details:{repo:context.repoName,requested_ticket_keys:options.keys,ticket_keys:worktreeRows.map(r=>r.key),worktree_paths:worktreeRows.map(r=>r.path),workers,gate_name:context.gateName,supervisor_mode:context.supervisorMode,dry_run:options.dryRun,agent:context.agentName,...context.epic?{epic_key:context.epic.epic_key,epic_run_id:context.epic.epic_run_id,plan_version:context.epic.plan_version}:{}}}}}async function emitStartTicketsRunStarted(context,rows,options,deps={}){let event=buildStartTicketsRunStartedEventInput(context,rows,options);try{if(deps.emit)deps.emit(event);else{let{emitConductorEvent:emitConductorEvent2}=await Promise.resolve().then(()=>(init_store(),store_exports));emitConductorEvent2(event)}return rows}catch{if(rows.length===0)return rows;let[first,...rest]=rows;return[{...first,warnings:[...first.warnings??[],CONDUCTOR_RUN_START_EMIT_FAILED_WARNING]},...rest]}}function posixSingleQuote(value){return`'${value.replace(/'/g,"'\\''")}'`}function powershellSingleQuote(value){return`'${value.replace(/'/g,"''")}'`}function injectConductorEnvIntoShellCommand(platform,shellCommand,env){if(!env)return shellCommand;let entries=Object.entries(env).filter(([key])=>ENV_KEY_PATTERN.test(key));if(entries.length===0)return shellCommand;let isWindows=platform==="win32";return`${entries.map(([key,value])=>isWindows?`$env:${key}=${powershellSingleQuote(value)};`:`export ${key}=${posixSingleQuote(value)};`).join(" ")} ${shellCommand}`}function buildSupervisorTabCommand(context,platform,nodeExecPath=process.execPath){let quote=platform==="win32"?powershellSingleQuote:posixSingleQuote;return`${quote(nodeExecPath)} ${quote(context.cliFile)} supervise --run-id ${quote(context.runId)}`}function isSupervisorLaunchEnabled(context){return context.supervisorMode.trim().toLowerCase()!=="off"}function supervisorSpawnKey(keys){return`${keys.length>0?keys[0]:"start-tickets"}-${SUPERVISOR_SPAWN_KEY_SUFFIX}`}var DEFAULT_CONDUCTOR_GATE_NAME,CONDUCTOR_TUNING_ENV_KEYS,CONDUCTOR_HOOK_LIFECYCLE_EVENTS,CONDUCTOR_RUN_START_EMIT_FAILED_WARNING,ENV_KEY_PATTERN,SUPERVISOR_SPAWN_KEY_SUFFIX,init_start_tickets_conductor=__esm({"src/start-tickets-conductor.ts"(){"use strict";init_claude_settings();init_file_scope_guard();init_mcp_profile();init_start_tickets_repo();DEFAULT_CONDUCTOR_GATE_NAME="implement-ticket";CONDUCTOR_TUNING_ENV_KEYS=["BAPI_CONDUCTOR_BUSY_TIMEOUT_MS","BAPI_CONDUCTOR_RETENTION_DAYS","BAPI_CONDUCTOR_RETENTION_MAX_ROWS","BAPI_CONDUCTOR_MESSAGE_COOLDOWN_MS"];CONDUCTOR_HOOK_LIFECYCLE_EVENTS=["SessionStart","SessionEnd","Notification"];CONDUCTOR_RUN_START_EMIT_FAILED_WARNING="conductor run-start emit failed (continuing without run-level event)";ENV_KEY_PATTERN=/^[A-Z_][A-Z0-9_]*$/;SUPERVISOR_SPAWN_KEY_SUFFIX="supervisor"}});import{createHash}from"node:crypto";function normalizeRepoName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function normalizeSha(value){if(typeof value!="string")return null;let lowered=value.trim().toLowerCase();return SHA_RE.test(lowered)?lowered:null}function normalizePrNumber(value){return typeof value!="number"||!Number.isSafeInteger(value)||value<=0?null:value}function normalizeCheckName(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0||CONTROL_CHAR_RE.test(trimmed)?null:trimmed}function canonicalize(value){if(Array.isArray(value))return value.map(item=>canonicalize(item));if(value!==null&&typeof value=="object"){let record=value,sortedKeys=Object.keys(record).sort(),out={};for(let key of sortedKeys)out[key]=canonicalize(record[key]);return out}return value}function stableJsonHash(value){let canonical=canonicalize(value),json=JSON.stringify(canonical)??"null";return createHash("sha256").update(json).digest("hex")}var GIT_CI_PRODUCER,GIT_HOOK_PRODUCER,REQUIRED_CI_CHECKS_GREEN,REVIEW_STATE,DEFAULT_GATE_NAME,REVIEW_PASSED,REVIEW_CHANGES_REQUESTED,CONTROL_CHAR_RE,SHA_RE,init_git_ci_types=__esm({"src/conductor/git-ci-types.ts"(){"use strict";GIT_CI_PRODUCER="git-pr-ci-producer",GIT_HOOK_PRODUCER="git-hook",REQUIRED_CI_CHECKS_GREEN="required_ci_checks_green",REVIEW_STATE="review_state",DEFAULT_GATE_NAME="done",REVIEW_PASSED="review.passed",REVIEW_CHANGES_REQUESTED="review.changes_requested",CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/,SHA_RE=/^[0-9a-f]{40}$|^[0-9a-f]{64}$/}});var bridge_api_client_exports={};__export(bridge_api_client_exports,{CONDUCTOR_DEFAULT_BASE_URL:()=>CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS:()=>CONDUCTOR_FETCH_TIMEOUT_MS,ConductorBridgeApiError:()=>ConductorBridgeApiError,advanceEpicTicketStatus:()=>advanceEpicTicketStatus,approveEpicPlan:()=>approveEpicPlan,buildConductorJiraUrl:()=>buildConductorJiraUrl,buildConductorVcsUrl:()=>buildConductorVcsUrl,buildEpicDispatchKey:()=>buildEpicDispatchKey,claimEpicSupervisionLease:()=>claimEpicSupervisionLease,createEpicRun:()=>createEpicRun,createEpicTicketStatus:()=>createEpicTicketStatus,deletePullRequestBranch:()=>deletePullRequestBranch,extractSanitizedErrorDiagnostics:()=>extractSanitizedErrorDiagnostics,fetchActiveEpicRuns:()=>fetchActiveEpicRuns,fetchConductorConfigField:()=>fetchConductorConfigField,fetchConductorJsonPatchWithTimeout:()=>fetchConductorJsonPatchWithTimeout,fetchConductorJsonPostWithTimeout:()=>fetchConductorJsonPostWithTimeout,fetchConductorJsonPutWithTimeout:()=>fetchConductorJsonPutWithTimeout,fetchConductorJsonWithTimeout:()=>fetchConductorJsonWithTimeout,fetchEffectiveSupervisorConfig:()=>fetchEffectiveSupervisorConfig,fetchEffectiveSupervisorSetup:()=>fetchEffectiveSupervisorSetup,fetchEpicRunState:()=>fetchEpicRunState,fetchParseStatus:()=>fetchParseStatus,fetchPrReviewStatus:()=>fetchPrReviewStatus,fetchShadowDispatchFreshness:()=>fetchShadowDispatchFreshness,getEpicPlan:()=>getEpicPlan,mergePullRequestForGate:()=>mergePullRequestForGate,pollCiChecksForCommit:()=>pollCiChecksForCommit,reconcileShadowMerge:()=>reconcileShadowMerge,recordEpicDispatch:()=>recordEpicDispatch,remediateEpicTicket:()=>remediateEpicTicket,resolveConductorBridgeApiAccess:()=>resolveConductorBridgeApiAccess,safeDiagnosticMessage:()=>safeDiagnosticMessage,storeEpicPlan:()=>storeEpicPlan,transitionEpicDispatch:()=>transitionEpicDispatch,transitionJiraStatus:()=>transitionJiraStatus,triggerRepositoryParse:()=>triggerRepositoryParse,updateEpicRunStatus:()=>updateEpicRunStatus});import os3 from"node:os";import{readFile as readFile4,stat as stat2}from"node:fs/promises";async function resolveConductorBridgeApiAccess(deps={}){let env=deps.env??process.env,cwd=deps.cwd??process.cwd(),homedir=deps.homedir??os3.homedir,platform=deps.platform??process.platform,readFileImpl=deps.readFile??(p=>readFile4(p,"utf-8")),statImpl=deps.stat??(p=>stat2(p)),repoName=deps.repoName?.trim()||await resolveStartTicketsRepoName({env,cwd,readFile:readFileImpl});if(!repoName)return{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"};let credResult;try{credResult=await resolveBapiCredentials(repoName,{env,homedir,platform,readFile:readFileImpl,stat:statImpl})}catch{return{ok:!1,kind:"credentials-unavailable",error:"failed to resolve Bridge API credentials"}}if(!credResult.ok)return{ok:!1,kind:"credentials-unavailable",error:"Bridge API credentials unavailable"};let baseUrlRaw=env.BAPI_BASE_URL,baseUrl=typeof baseUrlRaw=="string"&&baseUrlRaw.trim().length>0?baseUrlRaw.trim():CONDUCTOR_DEFAULT_BASE_URL;return{ok:!0,access:{repoName,apiKey:credResult.credentials.apiKey,baseUrl}}}function buildConductorJiraUrl(baseUrl,apiPath,params={}){let trimmed=baseUrl.replace(/\/+$/,""),url=new URL(`${trimmed}/jira${apiPath}`);for(let[k,v]of Object.entries(params))url.searchParams.set(k,v);return url.toString()}function redactErrorPreview(text){return text.replace(/sk-[A-Za-z0-9_-]{8,}/g,"[REDACTED]").replace(/(Bearer|X-API-Key|api[_-]?key)\b\s*[:=]?\s*\S+/gi,"$1 [REDACTED]")}function boundedErrorPreview(text){let redacted=redactErrorPreview(text).replace(/\s+/g," ").trim();return redacted.length>CONDUCTOR_ERROR_PREVIEW_MAX?`${redacted.slice(0,CONDUCTOR_ERROR_PREVIEW_MAX)}\u2026`:redacted}function extractSanitizedErrorDiagnostics(body){if(typeof body=="string"){let trimmed=body.trim();return trimmed?{bodyPreview:boundedErrorPreview(trimmed)}:{}}if(!body||typeof body!="object")return{};let record=body,detail=record.detail,errorCode,message;if(detail&&typeof detail=="object"){let d=detail;typeof d.error_code=="string"&&(errorCode=d.error_code),typeof d.message=="string"&&(message=d.message)}else typeof detail=="string"&&(message=detail);!errorCode&&typeof record.error_code=="string"&&(errorCode=record.error_code),!message&&typeof record.message=="string"&&(message=record.message);let diagnostics={};return errorCode&&(diagnostics.errorCode=boundedErrorPreview(errorCode)),message&&(diagnostics.bodyPreview=boundedErrorPreview(message)),diagnostics}function redactDiagnosticValues(diagnostics,secrets){let scrub=text=>{let out2=text;for(let secret of secrets)secret&&secret.length>=4&&(out2=out2.split(secret).join("[REDACTED]"));return out2},out={};return diagnostics.errorCode&&(out.errorCode=scrub(diagnostics.errorCode)),diagnostics.bodyPreview&&(out.bodyPreview=scrub(diagnostics.bodyPreview)),out}async function readSanitizedErrorDiagnostics(resp,headers={}){try{let diagnostics=extractSanitizedErrorDiagnostics(await resp.json()),secrets=Object.entries(headers).filter(([k])=>/key|authorization|token/i.test(k)).map(([,v])=>v);return redactDiagnosticValues(diagnostics,secrets)}catch{return{}}}function safeDiagnosticMessage(err,fallback){if(err instanceof ConductorBridgeApiError){let parts=[`kind=${err.kind}`];return typeof err.status=="number"&&parts.push(`status=${err.status}`),err.errorCode&&parts.push(`code=${err.errorCode}`),err.bodyPreview&&parts.push(err.bodyPreview),parts.join(" ")}return err instanceof Error?err.constructor.name:fallback}function conductorGetHeaders(access2){return{"X-API-Key":access2.apiKey}}async function fetchConductorJsonWithTimeout(url,headers,timeoutMs,fetchImpl=globalThis.fetch){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{headers,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorConfigField(access2,fieldName,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`/config-field/${encodeURIComponent(fieldName)}`,{repo_name:access2.repoName}),body=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(body&&typeof body=="object"&&"value"in body)return body.value}async function fetchEffectiveSupervisorSetup(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-setup/`:`${EPIC_RUNS_API_PREFIX}/supervisor-setup/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchEffectiveSupervisorConfig(access2,epicKey,fetchImpl=globalThis.fetch){let apiPath=epicKey?`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}/supervisor-config/`:`${EPIC_RUNS_API_PREFIX}/supervisor-config/defaults/`,url=buildConductorJiraUrl(access2.baseUrl,apiPath,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function pollCiChecksForCommit(access2,commitRef,fetchImpl=globalThis.fetch){let sha=normalizeSha(commitRef);if(sha===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,"/poll-ci-checks",{repo_name:access2.repoName,commit_ref:sha});return fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchPrReviewStatus(access2,prNumber,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/reviews/status`),fullUrl=new URL(url);return fullUrl.searchParams.set("repo_name",access2.repoName),fetchConductorJsonWithTimeout(fullUrl.toString(),conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function buildConductorVcsUrl(baseUrl,apiPath){let trimmed=baseUrl.replace(/\/+$/,""),path37=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path37}`).toString()}function conductorPostHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}async function fetchConductorJsonWithMethodAndTimeout(method,url,headers,body,timeoutMs,fetchImpl){let controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let resp;try{resp=await fetchImpl(url,{method,headers,body,signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(!resp.ok){let diagnostics=await readSanitizedErrorDiagnostics(resp,headers);throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status,diagnostics):resp.status>=500?new ConductorBridgeApiError("server",resp.status,diagnostics):new ConductorBridgeApiError("http",resp.status,diagnostics)}try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function fetchConductorJsonPostWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("POST",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPatchWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PATCH",url,headers,body,timeoutMs,fetchImpl)}async function fetchConductorJsonPutWithTimeout(url,headers,body,timeoutMs,fetchImpl){return fetchConductorJsonWithMethodAndTimeout("PUT",url,headers,body,timeoutMs,fetchImpl)}async function mergePullRequestForGate(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number),sha=normalizeSha(request.expected_head_sha);if(pr===null||sha===null||!request.action_key||!request.repo_name)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/merge`),body=JSON.stringify({repo_name:request.repo_name,expected_head_sha:sha,gate:request.gate,action_key:request.action_key,...request.gate_event?{gate_event:request.gate_event}:{}});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function remediateEpicTicket(access2,request,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(request.pr_number);if(pr===null||!request.epic_run_id||!request.ticket_key||!request.head_sha||!request.idempotency_key)throw new ConductorBridgeApiError("invalid-input");if(requireNonNegativeSafeInteger(request.expected_row_version),request.attempt_kind!=="nudge"&&request.attempt_kind!=="redispatch")throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/remediate`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epic_run_id,ticket_key:request.ticket_key,expected_row_version:request.expected_row_version,head_sha:request.head_sha,idempotency_key:request.idempotency_key,attempt_kind:request.attempt_kind,...request.reason?{reason:request.reason}:{}});try{return{ok:!0,conflict:!1,response:await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===409)return{ok:!0,conflict:!0};throw err}}function requireNonEmptyString(value){if(typeof value!="string"||value.trim().length===0)throw new ConductorBridgeApiError("invalid-input")}function requirePositiveSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<=0)throw new ConductorBridgeApiError("invalid-input")}function requireNonNegativeSafeInteger(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<0)throw new ConductorBridgeApiError("invalid-input")}function requireNoSlashPathSegment(value){if(value.includes("/"))throw new ConductorBridgeApiError("invalid-input")}function requireEpicTicketStatusValue(value){if(typeof value!="string"||!EPIC_TICKET_STATUS_VALUES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function requireEpicDispatchTransitionStatus(value){if(typeof value!="string"||!EPIC_DISPATCH_TRANSITION_STATUSES.includes(value))throw new ConductorBridgeApiError("invalid-input")}function epicRunApiPath(epicKey){return`${EPIC_RUNS_API_PREFIX}/runs/${encodeURIComponent(epicKey)}`}function epicDispatchTransitionApiPath(dispatchKey,nextStatus){return nextStatus==="run_spawned"?`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/run-spawned`:`/epic-runs/dispatch/${encodeURIComponent(dispatchKey)}/terminal`}function buildEpicDispatchKey(epicKey,ticketKey,planVersion,attempt=0){requireNonEmptyString(epicKey),requireNonEmptyString(ticketKey),requireNonNegativeSafeInteger(planVersion),requireNonNegativeSafeInteger(attempt);let base=`dispatch:${epicKey}:${ticketKey}:${planVersion}`;return attempt>0?`${base}:r${attempt}`:base}function parseEpicSupervisionLeaseResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row=p.row;if(p.claimed===!0&&row&&typeof row=="object")return{ok:!0,kind:"acquired-or-renewed",row};if(p.claimed===!1&&p.reason==="lease_held"&&row&&typeof row=="object")return{ok:!1,kind:"held-by-other",reason:"lease_held",row};if(p.claimed===!1&&p.reason==="terminal"&&row&&typeof row=="object")return{ok:!1,kind:"terminal",reason:"terminal",row};throw new ConductorBridgeApiError("server")}async function claimEpicSupervisionLease(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.leaseOwner),requirePositiveSafeInteger(request.ttlSeconds);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/lease/claim`),body=JSON.stringify({repo_name:access2.repoName,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicSupervisionLeaseResult(parsed)}async function fetchEpicRunState(access2,epicKey,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/state`,{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchActiveEpicRuns(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`,{repo_name:access2.repoName,status:"active",limit:"20"}),parsed=await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);if(parsed&&typeof parsed=="object"){let runs=parsed.runs;if(Array.isArray(runs))return runs}return[]}async function createEpicRun(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let body={repo_name:access2.repoName,epic_key:request.epicKey,status:request.status??"planning",current_plan_version:request.currentPlanVersion??0};request.policyJson!==void 0&&(body.policy_json=request.policyJson),request.budgetWallClockSeconds!==void 0&&(body.budget_wall_clock_seconds=request.budgetWallClockSeconds),request.budgetCostCents!==void 0&&(body.budget_cost_cents=request.budgetCostCents);let url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/runs`);return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),JSON.stringify(body),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function updateEpicRunStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,epicRunApiPath(request.epicKey)),body=JSON.stringify({repo_name:access2.repoName,status:request.status,...request.expectedStatus?{expected_status:request.expectedStatus}:{}});return await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseAdvanceEpicTicketStatusResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed;if(p.ok===!1&&p.kind==="cas-conflict")return{ok:!1,kind:"cas-conflict",...typeof p.current_row_version=="number"?{current_row_version:p.current_row_version}:{},...p.ticket_status&&typeof p.ticket_status=="object"?{ticket_status:p.ticket_status}:{}};if(p.ok===!0&&p.ticket_status&&typeof p.ticket_status=="object")return{ok:!0,ticket_status:p.ticket_status};if("ticket_key"in p&&"status"in p&&"row_version"in p)return{ok:!0,ticket_status:parsed};throw new ConductorBridgeApiError("server")}async function advanceEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonNegativeSafeInteger(request.expectedRowVersion),requireNonNegativeSafeInteger(request.planVersion),requireEpicTicketStatusValue(request.nextStatus),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let CAS_ENDPOINT_PATH=`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}`,url=buildConductorJiraUrl(access2.baseUrl,CAS_ENDPOINT_PATH),body=JSON.stringify({repo_name:access2.repoName,status:request.nextStatus,plan_version:request.planVersion,expected_row_version:request.expectedRowVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}}),parsed=await fetchConductorJsonPatchWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseAdvanceEpicTicketStatusResult(parsed)}async function createEpicTicketStatus(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireEpicTicketStatusValue(request.status),requireNonNegativeSafeInteger(request.planVersion),request.dispatchRunId!==void 0&&requireNonEmptyString(request.dispatchRunId);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets`),body=JSON.stringify({repo_name:access2.repoName,ticket_key:request.ticketKey,status:request.status,plan_version:request.planVersion,...request.dispatchRunId?{dispatch_run_id:request.dispatchRunId}:{}});await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseEpicDispatchResult(parsed){if(!parsed||typeof parsed!="object")throw new ConductorBridgeApiError("server");let p=parsed,row=p.row;if(!row||typeof row!="object")throw new ConductorBridgeApiError("server");let dispatch=row;if(p.claimed===!0)return{ok:!0,kind:"claimed",dispatch};if(p.claimed===!1){let reason=p.reason;if(reason==="already_spawned")return{ok:!0,kind:"already-spawned",dispatch};if(reason==="already_exists")return{ok:!0,kind:"already-exists",dispatch};if(reason==="terminal")return{ok:!0,kind:"terminal",terminal:!0,dispatch};if(reason==="lease_held")return{ok:!1,kind:"lease-held",dispatch}}throw new ConductorBridgeApiError("server")}async function recordEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNonEmptyString(request.leaseOwner),requireNonNegativeSafeInteger(request.planVersion),requirePositiveSafeInteger(request.ttlSeconds);let dispatchKey=buildEpicDispatchKey(request.epicKey,request.ticketKey,request.planVersion,request.attempt??0)+(request.reviewRole?":review":""),url=buildConductorJiraUrl(access2.baseUrl,`${EPIC_RUNS_API_PREFIX}/dispatch/claim`),body=JSON.stringify({repo_name:access2.repoName,epic_run_id:request.epicKey,ticket_key:request.ticketKey,plan_version:request.planVersion,lease_owner:request.leaseOwner,ttl_seconds:request.ttlSeconds,dispatch_key:dispatchKey}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseEpicDispatchResult(parsed)}async function transitionEpicDispatch(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.dispatchKey),requireNoSlashPathSegment(request.dispatchKey),requireEpicDispatchTransitionStatus(request.nextStatus),request.nextStatus==="run_spawned"&&requireNonEmptyString(request.runId);let path37=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path37),body=request.nextStatus==="run_spawned"?JSON.stringify({repo_name:access2.repoName,run_id:request.runId}):JSON.stringify({repo_name:access2.repoName});return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseShadowMergeReconcileResult(parsed){let obj=parsed??{};return{applies:obj.applies===!0,scheduled:obj.scheduled===!0,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:"unknown",shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,requiredCommitSha:typeof obj.required_commit_sha=="string"?obj.required_commit_sha:null,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null}}function parseShadowDispatchFreshnessResult(parsed){let obj=parsed??{},rawVerdict=typeof obj.verdict=="string"?obj.verdict:"",verdict=SHADOW_FRESHNESS_VERDICTS.has(rawVerdict)?rawVerdict:"stale";return{verdict,reasonCode:typeof obj.reason_code=="string"?obj.reason_code:verdict,lifecycleState:typeof obj.lifecycle_state=="string"?obj.lifecycle_state:null,indexedCommitSha:typeof obj.indexed_commit_sha=="string"?obj.indexed_commit_sha:null,shadowRepoName:typeof obj.shadow_repo_name=="string"?obj.shadow_repo_name:null,lastError:typeof obj.last_error=="string"?obj.last_error:null}}async function reconcileShadowMerge(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/shadow/merge-reconcile`),body=JSON.stringify({repo_name:access2.repoName,merged_ticket_key:request.mergedTicketKey??null}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowMergeReconcileResult(parsed)}async function fetchShadowDispatchFreshness(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requireNonEmptyString(request.ticketKey),requireNoSlashPathSegment(request.ticketKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/tickets/${encodeURIComponent(request.ticketKey)}/shadow-freshness`),body=JSON.stringify({repo_name:access2.repoName}),parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseShadowDispatchFreshnessResult(parsed)}async function storeEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion),requireNonEmptyString(request.planHash);let blobVersion=request.planBlob.plan_version;if(blobVersion!==void 0&&blobVersion!==request.planVersion)throw new ConductorValidationError(`planVersion mismatch: request.planVersion=${request.planVersion} but planBlob.plan_version=${blobVersion}`);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/plan`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion,plan_blob:request.planBlob,plan_hash:request.planHash});return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}function parseApproveEpicPlanSuccess(parsed){let record=parsed??{},result={ok:!0,plan_hash:record.plan_hash},prov=record.feature_branch_provisioning;if(prov&&typeof prov=="object"&&!Array.isArray(prov)){let p=prov;(p.status==="created"||p.status==="already_exists")&&typeof p.feature_branch=="string"&&typeof p.source_branch=="string"&&typeof p.source_sha=="string"&&typeof p.remote_head_sha=="string"&&(result.featureBranchProvisioning={status:p.status,feature_branch:p.feature_branch,source_branch:p.source_branch,source_sha:p.source_sha,remote_head_sha:p.remote_head_sha})}return result}async function approveEpicPlan(access2,request,fetchImpl=globalThis.fetch){requireNonEmptyString(request.epicKey),requirePositiveSafeInteger(request.planVersion);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(request.epicKey)}/approve-plan`),body=JSON.stringify({repo_name:access2.repoName,plan_version:request.planVersion});try{let parsed=await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl);return parseApproveEpicPlanSuccess(parsed)}catch(error){if(error instanceof ConductorBridgeApiError&&error.status===409){let preview=error.bodyPreview??"";return/multiple active runs/i.test(preview)?{ok:!1,kind:"conflict",reason:"multiple_active_runs"}:{ok:!1,kind:"conflict",reason:"superseded"}}throw error}}async function getEpicPlan(access2,epicKey,planVersion,fetchImpl=globalThis.fetch){requireNonEmptyString(epicKey);let url=buildConductorJiraUrl(access2.baseUrl,`${epicRunApiPath(epicKey)}/plan`,{repo_name:access2.repoName,plan_version:String(planVersion)});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function fetchParseStatus(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-status",{repo_name:access2.repoName});return await fetchConductorJsonWithTimeout(url,conductorGetHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function triggerRepositoryParse(access2,fetchImpl=globalThis.fetch){let url=buildConductorJiraUrl(access2.baseUrl,"/parse-repository"),body=JSON.stringify({repo_name:access2.repoName});return fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}async function deletePullRequestBranch(access2,prNumber,expectedHeadSha,fetchImpl=globalThis.fetch){let pr=normalizePrNumber(prNumber);if(pr===null)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorVcsUrl(access2.baseUrl,`/vcs/pull-requests/${pr}/branch?repo_name=${encodeURIComponent(access2.repoName)}&expected_head_sha=${encodeURIComponent(expectedHeadSha)}`),controller=new AbortController,timer=setTimeout(()=>controller.abort(),CONDUCTOR_FETCH_TIMEOUT_MS);try{let resp;try{resp=await fetchImpl(url,{method:"DELETE",headers:{"X-API-Key":access2.apiKey},body:"",signal:controller.signal})}catch{throw new ConductorBridgeApiError(controller.signal.aborted?"timeout":"network")}if(resp.status===404)return{deleted:!1,branch:null,reason:"not_found"};if(!resp.ok)throw resp.status===401||resp.status===403?new ConductorBridgeApiError("unauthorized",resp.status):resp.status>=500?new ConductorBridgeApiError("server",resp.status):new ConductorBridgeApiError("http",resp.status);try{return await resp.json()}catch{throw new ConductorBridgeApiError("network")}}finally{clearTimeout(timer)}}async function transitionJiraStatus(access2,ticketNumber,targetStatus="auto",fetchImpl=globalThis.fetch){if(!ticketNumber)throw new ConductorBridgeApiError("invalid-input");let url=buildConductorJiraUrl(access2.baseUrl,`/tickets/${encodeURIComponent(ticketNumber)}/jira-status`),body=JSON.stringify({repo_name:access2.repoName,target_status:targetStatus});try{return await fetchConductorJsonPutWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl),{status:"transitioned"}}catch(err){if(err instanceof ConductorBridgeApiError&&err.kind==="http"&&err.status===400)return{status:"skipped"};throw err}}var CONDUCTOR_DEFAULT_BASE_URL,CONDUCTOR_FETCH_TIMEOUT_MS,CONDUCTOR_BRIDGE_API_ERROR_KINDS,CONDUCTOR_ERROR_PREVIEW_MAX,ConductorBridgeApiError,EPIC_TICKET_STATUS_VALUES,EPIC_DISPATCH_TRANSITION_STATUSES,EPIC_RUNS_API_PREFIX,SHADOW_FRESHNESS_VERDICTS,init_bridge_api_client=__esm({"src/conductor/bridge-api-client.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_git_ci_types();init_errors();CONDUCTOR_DEFAULT_BASE_URL="https://bridgegpt-api.com",CONDUCTOR_FETCH_TIMEOUT_MS=3e4;CONDUCTOR_BRIDGE_API_ERROR_KINDS=["invalid-input","network","timeout","unauthorized","server","http"],CONDUCTOR_ERROR_PREVIEW_MAX=200;ConductorBridgeApiError=class extends Error{kind;status;errorCode;bodyPreview;constructor(kindOrMessage,status,diagnostics){let isKnownKind=CONDUCTOR_BRIDGE_API_ERROR_KINDS.includes(kindOrMessage),errorCode=diagnostics?.errorCode,bodyPreview=diagnostics?.bodyPreview;if(isKnownKind){let parts=[`Conductor Bridge API request failed (${kindOrMessage}${typeof status=="number"?`, status ${status}`:""})`];errorCode&&parts.push(`code=${errorCode}`),bodyPreview&&parts.push(bodyPreview),super(parts.join(": "))}else super(kindOrMessage);this.name="ConductorBridgeApiError",this.kind=isKnownKind?kindOrMessage:"http",typeof status=="number"&&(this.status=status),errorCode&&(this.errorCode=errorCode),bodyPreview&&(this.bodyPreview=bodyPreview)}};EPIC_TICKET_STATUS_VALUES=["planned","ready","dispatched","running","blocked","abandoned","done","ready_for_review","reviewing","parse_pending"];EPIC_DISPATCH_TRANSITION_STATUSES=["run_spawned","terminal"];EPIC_RUNS_API_PREFIX="/epic-runs";SHADOW_FRESHNESS_VERDICTS=new Set(["not_applicable","covered","stale","failed"])}});function buildPrBaseContractLaunchInstruction(){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.'}var PR_BASE_BRANCH_ENV_VAR,init_pr_base_contract=__esm({"src/pr-base-contract.ts"(){"use strict";PR_BASE_BRANCH_ENV_VAR="BAPI_BASE_BRANCH"}});import path13 from"path";function resolveBranchForTicket(key,overrides){return Object.prototype.hasOwnProperty.call(overrides,key)?overrides[key]:`feature/${key}`}async function branchExists(deps,branch){let result=await deps.runCommand("git",["show-ref","--verify","--quiet",`refs/heads/${branch}`],{cwd:deps.cwd});return commandSucceeded(result)}function buildWtSwitchArgs(branch,exists,baseStartPoint="main"){return exists?["switch","-y",branch,"--format=json"]:["switch","--create","-y",branch,"-b",baseStartPoint,"--format=json"]}function pathApiForPlatform(platform){return platform==="win32"?path13.win32:path13.posix}function extractWorktreePath(stdout,cwd,platform=process.platform){let parsed;try{parsed=JSON.parse(stdout)}catch{throw new Error(`Could not parse Worktrunk JSON output: ${stdout.slice(0,200)}`)}let candidate=pickWorktreePathField(parsed);if(!candidate)throw new Error(`Worktrunk JSON did not include a worktree path: ${stdout.slice(0,200)}`);let pathApi=pathApiForPlatform(platform);return pathApi.isAbsolute(candidate)?candidate:pathApi.resolve(cwd,candidate)}function pickWorktreePathField(parsed){if(!parsed||typeof parsed!="object")return;let obj=parsed;if(typeof obj.path=="string")return obj.path;if(typeof obj.worktree_path=="string")return obj.worktree_path;if(typeof obj.directory=="string")return obj.directory;if(obj.worktree&&typeof obj.worktree=="object"){let nested=obj.worktree;if(typeof nested.path=="string")return nested.path}}async function isExistingBranchSafeToReuse(deps,branch,baseStartPoint){let baseRef=baseStartPoint,originRef=`origin/${baseStartPoint}`,originExists=await deps.runCommand("git",["rev-parse","--verify","--quiet",originRef],{cwd:deps.cwd});commandSucceeded(originExists)&&(baseRef=originRef);let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",branch,baseRef],{cwd:deps.cwd});return commandSucceeded(ancestor)?{safe:!0}:{safe:!1,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.`}}async function hardResetWorktree(deps,worktreePath,ref){let resetArgs=["reset","--hard",ref],reset=await deps.runCommand("git",resetArgs,{cwd:worktreePath});if(!commandSucceeded(reset)){let reason=(reset.stderr||reset.stdout||"").trim();return`git ${resetArgs.join(" ")} failed${reason?`: ${reason}`:""}`}return null}async function verifyWorktreeHead(deps,worktreePath,expected){let headRes=await deps.runCommand("git",["rev-parse","--verify","HEAD^{commit}"],{cwd:worktreePath});if(!commandSucceeded(headRes))return"failed to resolve worktree HEAD after creation (git rev-parse HEAD^{commit} failed).";let expectedRes=await deps.runCommand("git",["rev-parse","--verify",`${expected}^{commit}`],{cwd:worktreePath});if(!commandSucceeded(expectedRes))return"failed to resolve the expected base commit after creation (git rev-parse --verify failed).";let head=headRes.stdout.trim(),want=expectedRes.stdout.trim();return head!==want?`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.`:null}async function createWorktreeForTicket(deps,key,branchOverrides,worktrunkBinary,baseStartPoint="main",guardStaleWorktree=!1,behavior={}){let branch=resolveBranchForTicket(key,branchOverrides);try{let exists=await branchExists(deps,branch);if(exists&&guardStaleWorktree){let safety=await isExistingBranchSafeToReuse(deps,branch,baseStartPoint);if(!safety.safe)return{key,branch,status:"create-failed",error:`stale worktree guard: ${safety.reason}`}}let args=buildWtSwitchArgs(branch,exists,baseStartPoint),result=await deps.runCommand(worktrunkBinary,args,{cwd:deps.cwd});if(!commandSucceeded(result)){let reason=(result.stderr||result.stdout||"").trim();return{key,branch,status:"create-failed",error:`${worktrunkBinary} ${args.join(" ")} failed${reason?`: ${reason}`:""}`}}let worktreePath=extractWorktreePath(result.stdout,deps.cwd,deps.platform);if(exists&&behavior.freshenFromOrigin){let resetError=await hardResetWorktree(deps,worktreePath,behavior.freshenFromOrigin);if(resetError)return{key,branch,status:"create-failed",error:resetError}}if(exists&&behavior.alignExistingBranchTo){let resetError=await hardResetWorktree(deps,worktreePath,behavior.alignExistingBranchTo);if(resetError)return{key,branch,status:"create-failed",error:resetError}}if(behavior.verifyHeadMatches){let verifyError=await verifyWorktreeHead(deps,worktreePath,behavior.verifyHeadMatches);if(verifyError)return{key,branch,status:"create-failed",error:verifyError}}return{key,branch,status:"created",path:worktreePath}}catch(err){let message=err instanceof Error?err.message:String(err);return{key,branch,status:"create-failed",error:message}}}var init_worktree_core=__esm({"src/worktree-core.ts"(){"use strict";init_start_tickets_prereqs()}});import path14 from"path";function validateBranchName(branch){if(branch.trim().length===0)return"branch name must not be empty.";if(branch.length>255)return"branch name must be 255 characters or fewer.";if(branch.startsWith("-"))return"branch name must not start with '-'.";if(branch.includes(".."))return"branch name must not contain '..'.";if(branch.endsWith(".lock"))return"branch name must not end with '.lock'.";for(let i=0;i<branch.length;i++){let code=branch.charCodeAt(i);if(code<=31||code===127)return"branch name must not contain control characters."}return null}function normalizeRepoKey(cwd){return path14.resolve(cwd)}async function withRepoFetchLock(repoKey,fn){let previous=repoFetchLocks.get(repoKey)??Promise.resolve(),releaseCurrent,current=new Promise(resolve2=>{releaseCurrent=resolve2}),chained=previous.then(()=>current);repoFetchLocks.set(repoKey,chained),await previous.catch(()=>{});try{return await fn()}finally{releaseCurrent(),repoFetchLocks.get(repoKey)===chained&&repoFetchLocks.delete(repoKey)}}async function fetchAndResolveBaseSha(deps,baseBranch){let validationError2=validateBranchName(baseBranch);if(validationError2)return{ok:!1,error:`Invalid base branch '${baseBranch}': ${validationError2}`};let repoKey=normalizeRepoKey(deps.cwd);return withRepoFetchLock(repoKey,async()=>{let fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`};let resolve2=await deps.runCommand("git",["rev-parse","--verify",`origin/${baseBranch}^{commit}`],{cwd:deps.cwd});return commandSucceeded(resolve2)?{ok:!0,base_sha:resolve2.stdout.trim()}:{ok:!1,error:`Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`}})}var repoFetchLocks,init_base_ref=__esm({"src/base-ref.ts"(){"use strict";init_start_tickets_prereqs();repoFetchLocks=new Map}});import{execFile}from"child_process";import{readFile as readFile5,writeFile as writeFile3,mkdir as mkdir3,mkdtemp,stat as stat3,readdir as readdir2,rm}from"fs/promises";import os4 from"node:os";import path15 from"path";import{existsSync as existsSync2}from"node:fs";function appendSummaryRowWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function getStartTicketsUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]","","Flags:"," --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)"," --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."," --tier cheap|basic|premium Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. It is still mapped to a model through the agent registry and any configured difficulty_model_tier_overrides, then validated \u2014 it is NOT a raw --model alias, and never carries an API key or credential. A malformed value fails open to premium routing."," --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement"," --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only"," --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"," --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)"," --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)"," --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility"," --max-parallel N Max worktrees to create concurrently (default: 3)"," --conductor Enable the Conductor system: per-worker hook injection + BAPI_CONDUCTOR_* env, a supervisor peer tab, and check_messages message-relay polling (default: off \u2014 a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>')"," -h, --help Show this help","","Environment:",` ${WORKTRUNK_BINARY_OVERRIDE_ENV} Override the Worktrunk executable name/path for nonstandard installs`,` ${TMUX_SESSION_OVERRIDE_ENV} Override the tmux session-name prefix on Linux (default: ${DEFAULT_TMUX_SESSION_PREFIX})`," BAPI_CONDUCTOR_GATE_NAME Conductor gate name for this run (default: implement-ticket)"," BAPI_CONDUCTOR_SUPERVISOR_MODE Conductor supervisor mode (default: auto when --auto, else interactive)"," BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE Set 1/true to also register a PreToolUse conductor hook","","Conductor observability (opt-in via --conductor):"," With --conductor, real Claude Code workers launched by start-tickets receive"," per-worktree conductor hook injection (into .claude/settings.local.json) and emit"," local lifecycle events into the conductor ledger. Each such run mints one run_id"," and attributes worker events by worker_id, ticket key, and worktree path, and a"," supervisor peer tab is opened. Without --conductor none of this happens. Inspect"," the ledger with the `conductor` CLI. The BAPI_CONDUCTOR_* env vars above apply"," only when --conductor is set.","","Prerequisites:"," macOS wt, git, osascript"," Windows git-wt, Git for Windows / Git Bash, Windows Terminal or PowerShell"," Linux wt, git, tmux","","Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248)."].join(`
108
108
  `)}function parseStartTicketsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getStartTicketsUsage()};let terminal,dryRun=!1,autoApprove=!1,refreshMain=!0,maxParallelRaw,agentName=DEFAULT_AGENT_NAME,baseBranch="main",conductorEnabled=!1,workflow="implement",reviewRoundsRaw,injectedTier,branchEntries=[],keys=[];for(let i=0;i<argv.length;i++){let arg=argv[i],takeValue4=()=>{if(!(i+1>=argv.length))return i+=1,argv[i]};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--agent requires a value (an agent name)."};if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}if(arg==="--workflow"||arg.startsWith("--workflow=")){let value;if(arg.startsWith("--workflow="))value=arg.slice(11);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--workflow requires a value (allowed values: implement, review-and-implement)."};if(value!=="implement"&&value!=="review-and-implement")return{status:"error",message:`Invalid --workflow value: '${value}' (allowed values: implement, review-and-implement).`};workflow=value;continue}if(arg==="--rounds"||arg.startsWith("--rounds=")){let value;if(arg.startsWith("--rounds="))value=arg.slice(9);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--rounds requires a value (allowed values: 1, 2)."};if(value!=="1"&&value!=="2")return{status:"error",message:`Invalid --rounds value: '${value}' (allowed values: 1, 2).`};reviewRoundsRaw=value;continue}if(arg==="--tier"||arg.startsWith("--tier=")){let value;if(arg.startsWith("--tier="))value=arg.slice(7);else{let next=i+1<argv.length?argv[i+1]:void 0;next!==void 0&&!next.startsWith("-")&&!TICKET_KEY_PATTERN.test(next)&&(value=takeValue4())}injectedTier=isModelTier(value)?value:INJECTED_TIER_UNRESOLVED;continue}if(arg==="--terminal"||arg.startsWith("--terminal=")){let value;if(arg.startsWith("--terminal="))value=arg.slice(11);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--terminal requires a value (terminal or iterm)."};if(value!=="terminal"&&value!=="iterm")return{status:"error",message:`Invalid --terminal value: '${value}' (allowed values: terminal, iterm).`};terminal=value;continue}if(arg==="--max-parallel"||arg.startsWith("--max-parallel=")){if(arg.startsWith("--max-parallel="))maxParallelRaw=arg.slice(15);else{let value=takeValue4();if(value===void 0)return{status:"error",message:"--max-parallel requires a positive integer value."};maxParallelRaw=value}continue}if(arg==="--branch"||arg.startsWith("--branch=")){let value;if(arg.startsWith("--branch="))value=arg.slice(9);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--branch requires a KEY=BRANCH value."};branchEntries.push(value);continue}if(arg==="--base-branch"||arg.startsWith("--base-branch=")){let value;if(arg.startsWith("--base-branch="))value=arg.slice(14);else{let next=i+1<argv.length?argv[i+1]:void 0;if(next===void 0||next.startsWith("-"))return{status:"error",message:"--base-branch requires a value (a branch name)."};value=takeValue4()}let trimmed=(value??"").trim(),error=validateBranchName(trimmed);if(error)return{status:"error",message:`Invalid --base-branch value: ${error}`};baseBranch=trimmed;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--auto"){autoApprove=!0;continue}if(arg==="--conductor"){conductorEnabled=!0;continue}if(arg==="--no-refresh-main"){refreshMain=!1;continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};keys.push(arg)}if(keys.length===0)return{status:"error",message:"At least one ticket key is required (e.g., BAPI-248)."};let seen=new Set;for(let key of keys){if(!TICKET_KEY_PATTERN.test(key))return{status:"error",message:`Invalid ticket key: '${key}' (keys must match [A-Z]+-[0-9]+, e.g., BAPI-248).`};if(seen.has(key))return{status:"error",message:`Duplicate ticket key: '${key}'.`};seen.add(key)}let maxParallel=DEFAULT_MAX_PARALLEL;if(maxParallelRaw!==void 0){if(!/^[0-9]+$/.test(maxParallelRaw)||Number(maxParallelRaw)<1)return{status:"error",message:`Invalid --max-parallel value: '${maxParallelRaw}' (must be a positive integer).`};maxParallel=Number(maxParallelRaw)}let branchOverrides={};for(let entry of branchEntries){let sepIndex=entry.indexOf("=");if(sepIndex<=0)return{status:"error",message:`Invalid --branch override: '${entry}' (expected KEY=BRANCH).`};let overrideKey=entry.slice(0,sepIndex),branchName=entry.slice(sepIndex+1);if(!TICKET_KEY_PATTERN.test(overrideKey))return{status:"error",message:`Invalid --branch override key: '${overrideKey}' (keys must match [A-Z]+-[0-9]+).`};if(!seen.has(overrideKey))return{status:"error",message:`--branch override key '${overrideKey}' is not one of the requested tickets.`};let branchError=validateBranchName(branchName);if(branchError)return{status:"error",message:`Invalid branch name for ${overrideKey}: ${branchError}`};branchOverrides[overrideKey]=branchName}let reviewRounds;if(reviewRoundsRaw!==void 0){if(workflow!=="review-and-implement")return{status:"error",message:"--rounds is only valid with --workflow review-and-implement."};reviewRounds=reviewRoundsRaw==="1"?1:2}return{status:"ok",options:{keys,terminal,dryRun,autoApprove,refreshMain,maxParallel,branchOverrides,agentName,baseBranch,conductorEnabled,workflow,reviewRounds,...injectedTier!==void 0?{injectedTier}:{}}}}function detectTerminal(explicit,env){return explicit||((env.TERM_PROGRAM??"").toLowerCase().includes("iterm")?"iterm":"terminal")}function getDefaultSpawnTerminalTabForPlatform(platform){switch(platform){case"darwin":return spawnMacOSTerminalTab;case"win32":return spawnWindowsTerminalTab;case"linux":return spawnLinuxTmuxTerminalTab;default:return spawnUnsupportedPlatformTerminalTab}}function resolveStartTicketsPlatformConfig(deps,agent,autoApprove=!1,conductorEnabled=!1,repoName=null,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){if(!isSupportedStartTicketsPlatform(deps.platform))return{ok:!1,error:unsupportedPlatformMessage(deps.platform)};let platform=deps.platform,prBaseBranch=conductorEnabled?baseBranch:null;return{ok:!0,config:{platform,worktrunkBinary:resolveWorktrunkBinary(platform,deps.env),buildAgentShellCommand:(key,worktreePath,modelAlias)=>prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch),repoName,platform),prBaseBranch,platform),spawnTerminalTab:deps.spawnTerminalTab}}}function prependRepoNameEnvAssignment(command,repoName,platform="darwin"){return repoName?platform==="win32"?`$env:BAPI_REPO_NAME = ${powershellSquote(repoName)}; ${command}`:`export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`:command}function prependBaseBranchEnvAssignment(command,baseBranch,platform="darwin"){return baseBranch?platform==="win32"?`$env:${PR_BASE_BRANCH_ENV_VAR} = ${powershellSquote(baseBranch)}; ${command}`:`export ${PR_BASE_BRANCH_ENV_VAR}='${shSquoteInner(baseBranch)}' && ${command}`:command}function shSquoteInner(value){return value.replace(/'/g,"'\\''")}function applescriptDquoteInner(value){return value.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function powershellSquoteInner(value){return value.replace(/'/g,"''")}function powershellSquote(value){return`'${powershellSquoteInner(value)}'`}function createDefaultStartTicketsDeps(){return{runCommand:(file,args,options)=>new Promise(resolve2=>{execFile(file,args,{cwd:options?.cwd,maxBuffer:67108864,encoding:"utf-8",timeout:options?.timeoutMs},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})}),platform:process.platform,env:process.env,cwd:process.cwd(),spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),writeWorkerLaunchScript:defaultWriteWorkerLaunchScript}}function combineCommandOutput(result){return[result.stderr,result.stdout].map(s=>s.trim()).filter(Boolean).join(" ")}async function runPreflight(deps,options,warn=message=>console.warn(message)){if(options.dryRun)return{ok:!0};let enforceLiveSourceGuard=options.nonMutatingBase===!0||options.epic!==void 0,result=await enforcePreflightPrerequisites(deps,{enforceLiveSourceGuard});return result.ok?(result.warning&&warn(result.warning),{ok:!0}):result.reason==="unsupported-platform"?{ok:!1,error:result.error}:{ok:!1,error:appendDoctorHint(result.error)}}function parseGitWorktreeList(output){let entries=[],current=null;for(let rawLine of output.split(`
109
- `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&&current){let ref=line.slice(7);current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}function findBaseWorktreePath(entries,baseBranch){for(let entry of entries)if(entry.branch===baseBranch)return entry.path;return null}async function refreshBaseBranch(deps,options){if(!options.refreshMain)return{ok:!0};let baseBranch=options.baseBranch,originRef=`origin/${baseBranch}`,fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`};let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return{ok:!1,error:`git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`};let basePath=findBaseWorktreePath(parseGitWorktreeList(list.stdout),baseBranch);if(basePath){let merge=await deps.runCommand("git",["merge","--ff-only",originRef],{cwd:basePath});return commandSucceeded(merge)?{ok:!0}:{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`}}if(await branchExists(deps,baseBranch)){let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",baseBranch,originRef],{cwd:deps.cwd});if(!commandSucceeded(ancestor))return{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`}}let update=await deps.runCommand("git",["branch","--force",baseBranch,originRef],{cwd:deps.cwd});return commandSucceeded(update)?{ok:!0}:{ok:!1,error:`Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`}}async function runWithConcurrency(items,limit,worker){let results=new Array(items.length),effectiveLimit=Math.max(1,Math.floor(limit)),nextIndex=0;async function runner(){for(;;){let index=nextIndex;if(index>=items.length)return;nextIndex+=1,results[index]=await worker(items[index],index)}}let runners=[],poolSize=Math.min(effectiveLimit,items.length);for(let i=0;i<poolSize;i++)runners.push(runner());return await Promise.all(runners),results}async function createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{};return runWithConcurrency(options.keys,options.maxParallel,key=>createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior))}async function resumeWorktrees(deps,options){let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return options.keys.map(key=>({key,branch:resolveBranchForTicket(key,options.branchOverrides),status:"create-failed",error:"resume mode: git worktree list --porcelain failed; cannot locate existing worktree."}));let entries=parseGitWorktreeList(list.stdout);return options.keys.map(key=>{let branch=resolveBranchForTicket(key,options.branchOverrides),entry=entries.find(e=>e.branch===branch);if(!entry){let needle=key.toLowerCase();entry=entries.find(e=>(e.branch??"").toLowerCase().includes(needle))}return entry?{key,branch:entry.branch??branch,status:"created",path:entry.path}:{key,branch,status:"create-failed",error:`resume mode: no existing worktree found for ticket ${key} (branch '${branch}').`}})}function buildConductorMessageRelayLaunchInstruction(){return"Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing."}function buildResumeModeRemediationFinalizeInstruction(){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."}function buildAgentPrompt(key,opts={}){let workflow=opts.workflow??"implement",command=`${workflow==="review-and-implement"?"/review-and-implement":"/implement-ticket"} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`));let parts=[command];return opts.conductorEnabled&&(parts.push(buildConductorMessageRelayLaunchInstruction()),parts.push(buildPrBaseContractLaunchInstruction())),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias){let argv=[agent.command];return agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(...agent.interactiveLaunchArgs??[]),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias),quotedRest=rest.map(quote);return[command,...quotedRest].join(" ")}else{let exhaustive=agent.promptArgStyle;throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`)}}function buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(worktreePath)}' && ${invocation}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch)}function buildGenericAgentShellCommand(agent,prompt,cwd,platform="darwin",modelAlias){if(platform==="win32"){let invocation2=buildAgentInvocation(agent,prompt,powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(cwd)}; ${invocation2}`}let invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function terminalTitleForTicket(key){return`${key} Implementation`}function buildTerminalAppleScript(shellCommand,title){let esc=applescriptDquoteInner(shellCommand),titleEsc=applescriptDquoteInner(title);return['tell application "Terminal"'," activate"," if (count of windows) is 0 then",` set spawnedTab to do script "${esc}"`," else",' tell application "System Events" to keystroke "t" using command down'," delay 0.2",` set spawnedTab to do script "${esc}" in selected tab of front window`," end if",` set custom title of spawnedTab to "${titleEsc}"`,"end tell"].join(`
109
+ `)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&&current){let ref=line.slice(7);current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}function findBaseWorktreePath(entries,baseBranch){for(let entry of entries)if(entry.branch===baseBranch)return entry.path;return null}async function refreshBaseBranch(deps,options){if(!options.refreshMain)return{ok:!0};let baseBranch=options.baseBranch,originRef=`origin/${baseBranch}`,fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`};let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return{ok:!1,error:`git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`};let basePath=findBaseWorktreePath(parseGitWorktreeList(list.stdout),baseBranch);if(basePath){let merge=await deps.runCommand("git",["merge","--ff-only",originRef],{cwd:basePath});return commandSucceeded(merge)?{ok:!0}:{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`}}if(await branchExists(deps,baseBranch)){let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",baseBranch,originRef],{cwd:deps.cwd});if(!commandSucceeded(ancestor))return{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`}}let update=await deps.runCommand("git",["branch","--force",baseBranch,originRef],{cwd:deps.cwd});return commandSucceeded(update)?{ok:!0}:{ok:!1,error:`Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`}}async function runWithConcurrency(items,limit,worker){let results=new Array(items.length),effectiveLimit=Math.max(1,Math.floor(limit)),nextIndex=0;async function runner(){for(;;){let index=nextIndex;if(index>=items.length)return;nextIndex+=1,results[index]=await worker(items[index],index)}}let runners=[],poolSize=Math.min(effectiveLimit,items.length);for(let i=0;i<poolSize;i++)runners.push(runner());return await Promise.all(runners),results}async function createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{};return runWithConcurrency(options.keys,options.maxParallel,key=>createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior))}async function resumeWorktrees(deps,options){let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return options.keys.map(key=>({key,branch:resolveBranchForTicket(key,options.branchOverrides),status:"create-failed",error:"resume mode: git worktree list --porcelain failed; cannot locate existing worktree."}));let entries=parseGitWorktreeList(list.stdout);return options.keys.map(key=>{let branch=resolveBranchForTicket(key,options.branchOverrides),entry=entries.find(e=>e.branch===branch);if(!entry){let needle=key.toLowerCase();entry=entries.find(e=>(e.branch??"").toLowerCase().includes(needle))}return entry?{key,branch:entry.branch??branch,status:"created",path:entry.path}:{key,branch,status:"create-failed",error:`resume mode: no existing worktree found for ticket ${key} (branch '${branch}').`}})}function buildConductorMessageRelayLaunchInstruction(){return"Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing."}function buildResumeModeRemediationFinalizeInstruction(){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."}function buildAgentPrompt(key,opts={}){let workflow=opts.workflow??"implement",command=`${workflow==="review-and-implement"?"/review-and-implement":"/implement-ticket"} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`));let parts=[command];return opts.conductorEnabled&&(parts.push(buildConductorMessageRelayLaunchInstruction()),parts.push(buildPrBaseContractLaunchInstruction())),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias){let argv=[agent.command];return agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias),quotedRest=rest.map(quote);return[command,...quotedRest].join(" ")}else{let exhaustive=agent.promptArgStyle;throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`)}}function buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(worktreePath)}' && ${invocation}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch)}function buildGenericAgentShellCommand(agent,prompt,cwd,platform="darwin",modelAlias){if(platform==="win32"){let invocation2=buildAgentInvocation(agent,prompt,powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(cwd)}; ${invocation2}`}let invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function terminalTitleForTicket(key){return`${key} Implementation`}function buildTerminalAppleScript(shellCommand,title){let esc=applescriptDquoteInner(shellCommand),titleEsc=applescriptDquoteInner(title);return['tell application "Terminal"'," activate"," if (count of windows) is 0 then",` set spawnedTab to do script "${esc}"`," else",' tell application "System Events" to keystroke "t" using command down'," delay 0.2",` set spawnedTab to do script "${esc}" in selected tab of front window`," end if",` set custom title of spawnedTab to "${titleEsc}"`,"end tell"].join(`
110
110
  `)}function itermBadgeShellCommand(badgeText){return`printf '\\033]1337;SetBadgeFormat=%s\\007' '${Buffer.from(badgeText,"utf8").toString("base64")}'`}function buildITermAppleScript(shellCommand,title,badgeText){let esc=applescriptDquoteInner(shellCommand),lines=['tell application "iTerm"'," activate"," if (count of windows) = 0 then"," set spawnedSession to current session of (create window with default profile)"," else"," tell current window to set spawnedSession to (current session of (create tab with default profile))"," end if"," tell spawnedSession",` set name to "${applescriptDquoteInner(title)}"`];if(badgeText){let badgeEsc=applescriptDquoteInner(itermBadgeShellCommand(badgeText));lines.push(` write text "${badgeEsc}"`)}return lines.push(` write text "${esc}"`),lines.push(" end tell"),lines.push("end tell"),lines.join(`
111
111
  `)}async function spawnMacOSTerminalTab(deps,terminal,shellCommand,context){let title=context?.title??terminalTitleForTicket(context?.key??""),badgeText=context?.title??(context?.key||void 0),script=terminal==="iterm"?buildITermAppleScript(shellCommand,title,badgeText):buildTerminalAppleScript(shellCommand,title),result=await deps.runCommand("osascript",["-e",script]);if(commandSucceeded(result))return{ok:!0};let reason=(result.stderr||result.stdout||"").trim();return{ok:!1,error:`osascript failed to open a ${terminal} tab${reason?`: ${reason}`:""}`}}function buildWindowsTerminalArgs(worktreePath,shellCommand,title){let wtEscapedCommand=shellCommand.replace(/;/g,"\\;");return["new-tab","--title",title,"--suppressApplicationTitle","-d",worktreePath,"powershell.exe","-NoExit","-Command",wtEscapedCommand]}function buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title){let titledCommand=`$host.UI.RawUI.WindowTitle = ${powershellSquote(title)}; ${shellCommand}`,argumentList=`@('-NoExit', '-Command', ${powershellSquote(titledCommand)})`;return`Start-Process -FilePath 'powershell.exe' -WorkingDirectory ${powershellSquote(worktreePath)} -ArgumentList ${argumentList}`}async function spawnWindowsTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath;if(!worktreePath)return{ok:!1,error:"Windows spawner requires a worktreePath context to open a tab."};let title=context?.title??terminalTitleForTicket(context?.key??"");if(await isCommandOnPath(deps,WINDOWS_TERMINAL_COMMAND)){let args=buildWindowsTerminalArgs(worktreePath,shellCommand,title),result2=await deps.runCommand(WINDOWS_TERMINAL_COMMAND,args);if(commandSucceeded(result2))return{ok:!0};let reason2=combineCommandOutput(result2);return{ok:!1,error:`wt.exe failed to open a Windows Terminal tab${reason2?`: ${reason2}`:""}`}}let powershell=await resolveFirstCommandOnPath(deps,WINDOWS_POWERSHELL_CANDIDATES);if(!powershell)return{ok:!1,error:"Windows Terminal (wt.exe) or PowerShell is required to open a tab, but neither was found on PATH."};let fallback=buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title),result=await deps.runCommand(powershell,["-NoProfile","-ExecutionPolicy","Bypass","-Command",fallback]);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`PowerShell failed to open a window via Start-Process${reason?`: ${reason}`:""}`}}function sanitizeTmuxName(value){let cleaned=value.replace(/[^A-Za-z0-9_-]+/g,"-").replace(/^-+/,"").replace(/-+$/,"");return cleaned.length>0?cleaned:"ticket"}function tmuxWindowNameForTicket(key){return terminalTitleForTicket(sanitizeTmuxName(key))}function tmuxWindowLabelFromTitle(title){let cleaned=title.replace(/[.:]+/g," ").replace(/\s+/g," ").trim();return cleaned.length>0?cleaned:"session"}function tmuxSessionPrefix(deps){let override=deps.env[TMUX_SESSION_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return DEFAULT_TMUX_SESSION_PREFIX}function tmuxSessionNameForTicket(deps,key){return`${tmuxSessionPrefix(deps)}-${sanitizeTmuxName(key)}`}function buildTmuxPaneCommand(shellCommand){return`${shellCommand}; exec $SHELL`}function buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand){return["new-session","-d","-s",session,"-n",window,"-c",worktreePath,paneCommand]}function buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand){return["new-window","-t",session,"-n",window,"-c",worktreePath,paneCommand]}async function spawnLinuxTmuxTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath,key=context?.key;if(!worktreePath||!key)return{ok:!1,error:"Linux tmux spawner requires a worktreePath context to open a session."};if(!await isCommandOnPath(deps,TMUX_COMMAND))return{ok:!1,error:"tmux is required to spawn Linux sessions but was not found on PATH. Install tmux and retry."};let session=tmuxSessionNameForTicket(deps,key),window=context?.title?tmuxWindowLabelFromTitle(context.title):tmuxWindowNameForTicket(key),paneCommand=buildTmuxPaneCommand(shellCommand),hasSession=await deps.runCommand(TMUX_COMMAND,["has-session","-t",session]),args=commandSucceeded(hasSession)?buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand):buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand),result=await deps.runCommand(TMUX_COMMAND,args);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`tmux failed to create a session/window${reason?`: ${reason}`:""}`}}async function spawnUnsupportedPlatformTerminalTab(deps,_terminal,_shellCommand,_context){return{ok:!1,error:unsupportedPlatformMessage(deps.platform)}}function sanitizeKeyForLaunchScript(key){let cleaned=key.replace(/[^A-Za-z0-9._-]/g,"_");return cleaned.length>0?cleaned:"worker"}function buildLaunchScriptContent(platform,fullCommand){return platform==="win32"?`${fullCommand}
112
112
  `:`#!/usr/bin/env bash
@@ -2361,7 +2361,7 @@ active \u2014 the server-side reconciler will pick it up within ~30s."
2361
2361
  ## Return
2362
2362
 
2363
2363
  Confirm the overview was written to \`{docs_dir}/epic-plans/{epic_slug}/overview.md\` and report the total sub-task count along with a one-line summary of the epic plan. State whether the goals/NFRs + recommended order were posted as a comment on \`{epic_key}\` or skipped because no epic key was provided.
2364
- `};init_version_generated();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](./docs/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\nRun bare like that in a terminal and it starts by asking\n**`Do you have a Bridge API key or invite? [Y/n]`**:\n\n- **Yes** (or just press Enter) \u2014 the existing-key flow. It asks for your **API key**\n (generate one on the Bridge API web UI **Security** page) and a **repo name**\n matching your server-side registration; everything else is derived. A\n `bapi_inv_\u2026` credential entered here instead of a full API key is automatically\n detected and redeemed as a **bootstrap invite** \u2014 it creates a brand-new project\n and mints your admin API key rather than looking up an existing repository.\n- **No** \u2014 the **self-serve** flow. It asks for an **email**, then a name for your new\n Bridge project, and creates the workspace and your own admin API key for you. No\n account, no key, and no invite needed beforehand. Same as passing\n `--email you@example.com` (see below).\n\nThat question is asked only for a *bare interactive* run. Passing any flag, setting\n`BAPI_API_KEY`, or running without an interactive terminal skips it and keeps the\nexisting deterministic behavior.\n\nFrom there `install-bridge` scaffolds the project, writes your editor\'s MCP config\nwith real values, verifies connectivity, persists your API key to the user-scoped\ncredential store, and opens a fresh agent session that runs `/install-bridge` to\nderive and apply the remaining config, presents a **capability report** (what you can\nuse now and what you\'ll unlock), and closes by asking whether to index the\nrepository. It does **not** automatically run `/learn-repository` or index without\nyour consent \u2014 both remain available as separate steps. Add `--dry-run` to preview\nevery 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 and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). 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)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically and redeemed the same way `--invite` is, skipping\n repository lookup entirely. `--invite` and `--email` remain the preferred,\n explicit entry points for a new project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. If the server is older,\n the key can\'t be uniquely resolved, or the lookup fails, `install-bridge` falls\n back to an inferred default you confirm interactively (and requires `--repo`\n when stdin is non-interactive). Whatever name is used MUST match the\n server-side repository registration.\n\n#### Self-serve email onboarding (`--email`) \u2014 no account, no key, no invite\n\nThe primary path for a **first-time user with nothing yet** \u2014 no Bridge account,\nno API key, and no pre-issued invite. Run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --email you@example.com\n```\n\n`install-bridge` requests a brand-new Bridge workspace for that email, receives an\ninvite token, and then feeds it into the **exact same** persist-before-exchange\nbootstrap protocol as `--invite` below \u2014 so the project is created and your first\nadmin key is minted in one command. The minted token is used internally and **never\nshown**.\n\nThe email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a\n**visible** interactive prompt (email is not a secret, so it is echoed as you type \u2014\nunlike the API key and the invite token, which use a hidden prompt). That prompt is\nwhat answering **no** to `Do you have a Bridge API key or invite? [Y/n]` on a bare run reaches,\nso `install-bridge --email you@example.com` and a bare `install-bridge` + "no" land\nin the same place. The email is still **never written to a log line**. No email\nverification is performed and no message is sent to the address \u2014 it only labels your\nnew workspace. `--email` is mutually exclusive with `--api-key` and `--invite`.\n\nBecause this flow *creates* the project, it asks you to **name a new project**\n(`Name your new Bridge project [<inferred>]: `) rather than to match an existing\nserver-side registration. The name must be globally unique; if it\'s taken, you\'re\nasked for another one and the invite is not consumed. The same applies to `--invite`.\n\n#### Bootstrap-invite onboarding (`--invite`)\n\nWith a **bootstrap invite** you were already given, there is no pre-existing key and\nno web UI: like `--email` above, this mode **creates** the project and its first\nadmin key instead 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- `--email <addr>` \u2014 self-serve signup: create a new workspace from just an email\n (mutually exclusive with `--api-key` and `--invite`). Falls back to\n `BAPI_SIGNUP_EMAIL`, then a visible prompt. Visible input, not a secret; still\n never logged.\n- `--invite [token]` \u2014 redeem a bootstrap invite (mutually exclusive with\n `--api-key` and `--email`). 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` inline then, after a per-ticket halt gate, hands off to a **fresh** `/implement-ticket` session reusing the same worktree) \xB7 `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override (see [CLI Subcommands](#cli-subcommands)).\n\n**3. Council**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of four modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), or **`general`** (brief-driven ideation from your task description and concerns alone). `technical` and `discovery` are codebase-grounded \u2014 they retrieve from the repository index and need a successfully indexed repo. `general` needs no code index at all, so it works immediately after install, before `/parse-repository` has ever run. 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, `general` for a quick brief-driven council before the repository is indexed.\n- **How to use it:** ask your agent to convene a council \u2014 *"Convene a council on approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design council for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery council \u2014 `request_council` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."* For a fresh, unindexed repo: *"Run a general council \u2014 `request_council` with `mode: "general"` \u2014 on launch options for this idea."*\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**10. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket human proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree \u2014 review and implementation run in two separate agent contexts, not one shared session.\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 review\u2192gate\u2192fresh-implementation-handoff logic lives in the spawned `/review-and-implement` session, never in this command or the CLI. On approval, that session reuses its review-time model tier by passing it to the fresh implementation launcher via `--tier`.\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` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\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, councils, 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\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\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, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\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**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-dx-mcp`** (`logs_list_files`, configured from `dw.json`) \u2014 it is vendor-maintained and reads log files over WebDAV, so it is strictly less work than shelling the B2C CLI (`b2c logs get --since <window> --search <q> --json`). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted `b2c-dx-mcp` or CLI output to an LLM.\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-3--now-and-then)) 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| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\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 default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\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](./docs/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](./docs/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| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\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## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\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_council`/`get_council`, `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';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
2364
+ `};init_version_generated();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](./docs/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\nRun bare like that in a terminal and it starts by asking\n**`Do you have a Bridge API key or invite? [Y/n]`**:\n\n- **Yes** (or just press Enter) \u2014 the existing-key flow. It asks for your **API key**\n (generate one on the Bridge API web UI **Security** page) and a **repo name**\n matching your server-side registration; everything else is derived. A\n `bapi_inv_\u2026` credential entered here instead of a full API key is automatically\n detected and redeemed as a **bootstrap invite** \u2014 it creates a brand-new project\n and mints your admin API key rather than looking up an existing repository.\n- **No** \u2014 the **self-serve** flow. It asks for an **email**, then a name for your new\n Bridge project, and creates the workspace and your own admin API key for you. No\n account, no key, and no invite needed beforehand. Same as passing\n `--email you@example.com` (see below).\n\nThat question is asked only for a *bare interactive* run. Passing any flag, setting\n`BAPI_API_KEY`, or running without an interactive terminal skips it and keeps the\nexisting deterministic behavior.\n\nFrom there `install-bridge` scaffolds the project, writes your editor\'s MCP config\nwith real values, verifies connectivity, persists your API key to the user-scoped\ncredential store, and opens a fresh agent session that runs `/install-bridge` to\nderive and apply the remaining config, presents a **capability report** (what you can\nuse now and what you\'ll unlock), and closes by asking whether to index the\nrepository. It does **not** automatically run `/learn-repository` or index without\nyour consent \u2014 both remain available as separate steps. Add `--dry-run` to preview\nevery 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 and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). 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)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically and redeemed the same way `--invite` is, skipping\n repository lookup entirely. `--invite` and `--email` remain the preferred,\n explicit entry points for a new project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. If the server is older,\n the key can\'t be uniquely resolved, or the lookup fails, `install-bridge` falls\n back to an inferred default you confirm interactively (and requires `--repo`\n when stdin is non-interactive). Whatever name is used MUST match the\n server-side repository registration.\n\n#### Self-serve email onboarding (`--email`) \u2014 no account, no key, no invite\n\nThe primary path for a **first-time user with nothing yet** \u2014 no Bridge account,\nno API key, and no pre-issued invite. Run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --email you@example.com\n```\n\n`install-bridge` requests a brand-new Bridge workspace for that email, receives an\ninvite token, and then feeds it into the **exact same** persist-before-exchange\nbootstrap protocol as `--invite` below \u2014 so the project is created and your first\nadmin key is minted in one command. The minted token is used internally and **never\nshown**.\n\nThe email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a\n**visible** interactive prompt (email is not a secret, so it is echoed as you type \u2014\nunlike the API key and the invite token, which use a hidden prompt). That prompt is\nwhat answering **no** to `Do you have a Bridge API key or invite? [Y/n]` on a bare run reaches,\nso `install-bridge --email you@example.com` and a bare `install-bridge` + "no" land\nin the same place. The email is still **never written to a log line**. No email\nverification is performed and no message is sent to the address \u2014 it only labels your\nnew workspace. `--email` is mutually exclusive with `--api-key` and `--invite`.\n\nBecause this flow *creates* the project, it asks you to **name a new project**\n(`Name your new Bridge project [<inferred>]: `) rather than to match an existing\nserver-side registration. The name must be globally unique; if it\'s taken, you\'re\nasked for another one and the invite is not consumed. The same applies to `--invite`.\n\n#### Bootstrap-invite onboarding (`--invite`)\n\nWith a **bootstrap invite** you were already given, there is no pre-existing key and\nno web UI: like `--email` above, this mode **creates** the project and its first\nadmin key instead 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- `--email <addr>` \u2014 self-serve signup: create a new workspace from just an email\n (mutually exclusive with `--api-key` and `--invite`). Falls back to\n `BAPI_SIGNUP_EMAIL`, then a visible prompt. Visible input, not a secret; still\n never logged.\n- `--invite [token]` \u2014 redeem a bootstrap invite (mutually exclusive with\n `--api-key` and `--email`). 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` inline then, after a per-ticket halt gate, hands off to a **fresh** `/implement-ticket` session reusing the same worktree) \xB7 `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override (see [CLI Subcommands](#cli-subcommands)).\n\n**3. Council**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of four modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), or **`general`** (brief-driven ideation from your task description and concerns alone). `technical` and `discovery` are codebase-grounded \u2014 they retrieve from the repository index and need a successfully indexed repo. `general` needs no code index at all, so it works immediately after install, before `/parse-repository` has ever run. 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, `general` for a quick brief-driven council before the repository is indexed.\n- **How to use it:** ask your agent to convene a council \u2014 *"Convene a council on approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design council for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery council \u2014 `request_council` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."* For a fresh, unindexed repo: *"Run a general council \u2014 `request_council` with `mode: "general"` \u2014 on launch options for this idea."*\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**10. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket human proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree \u2014 review and implementation run in two separate agent contexts, not one shared session.\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 review\u2192gate\u2192fresh-implementation-handoff logic lives in the spawned `/review-and-implement` session, never in this command or the CLI. On approval, that session reuses its review-time model tier by passing it to the fresh implementation launcher via `--tier`.\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` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\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, councils, 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\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\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, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\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**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-dx-mcp`** (`logs_list_files`, configured from `dw.json`) \u2014 it is vendor-maintained and reads log files over WebDAV, so it is strictly less work than shelling the B2C CLI (`b2c logs get --since <window> --search <q> --json`). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted `b2c-dx-mcp` or CLI output to an LLM.\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-3--now-and-then)) 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| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\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 default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\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](./docs/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](./docs/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| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\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## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\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 **60 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- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\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_council`/`get_council`, `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';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
2365
2365
  ${errors.join(`
2366
2366
  `)}`);continue}let pipeline=parsed,key=file.replace(/\.json$/,""),hasInvalidRef=!1;for(let step of pipeline.steps)if(step.type==="agent_task"&&step.instruction_file){let content=mergedInstructions[step.instruction_file];if(content===void 0){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" not found.`),hasInvalidRef=!0;break}if(!hasTerminalReturnSection(content)){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" is missing a terminal "## Return" section (required by BAPI-275 agent_result contract).`),hasInvalidRef=!0;break}}hasInvalidRef||(userPipelines[key]=pipeline,userPipelineKeys2.add(key))}}catch(err){return err.code!=="ENOENT"&&console.error(`Warning: could not read pipelines directory "${pipelinesDir}": ${err.message}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}return userPipelineKeys2.size>0&&console.error(`Loaded ${userPipelineKeys2.size} user pipeline(s) from ${pipelinesDir}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}import{writeFile as writeFile2,mkdir as mkdir2,readFile as readFile3,stat}from"fs/promises";import path5 from"path";var COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
2367
2367
 
@@ -3292,7 +3292,7 @@ After the final pipeline step completes, cleanly end your worker session (for ex
3292
3292
  - you have unpushed local commits.
3293
3293
 
3294
3294
  Exit only after your final branch state is pushed, the done-gate / CI-monitoring workflow required by the recipe has completed, and no CI/review follow-up remains. A clean \`SessionEnd\` is both the correct terminal lifecycle signal and the point at which the worker should exit.
3295
- `,"install-bridge.md":'Bootstrap Bridge API project configuration from the local codebase via the easy-install manifest.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes no arguments; any `$ARGUMENTS` text is ignored.\n\nCommand contract version: **5**. The install manifest returns a `command_contract_version`; Stage 2\ncompares it to this number to detect a stale scaffolded copy of this command.\n\nThis command performs a one-time "easy install" of Bridge API project configuration. It reads the\nserver-owned install manifest once, derives values for the **unset** bootstrap fields from the local\ncodebase, asks for human approval of the project description, applies everything in a single atomic\ncall, and then presents a **concise capability report** derived from a fresh read-after-write manifest\nread. The server owns all skip-if-set, conflict, and confirmation semantics \u2014 this command never makes\nits own skip-if-set decisions \u2014 and the server owns the complete tool catalog and the bounded concise\nprojection over it, their grouping and ordering, and every gate and dependency relationship; this\ncommand formats the server\'s contract and never recomputes it from prose. Indexing is never a decision\nthis command makes or asks about: it starts automatically, gated entirely by server-side readiness (see\nStage 8).\n\nThis command is portable across Claude Code, Cursor, and Copilot: it only drives the Bridge API MCP\ntools (`get_my_role`, `get_install_manifest`, `apply_install_manifest`).\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n**Install-spawn context.** When the spawning instruction explicitly says this `/install-bridge` run is\nin the "install-spawn context" (it was launched by the `install-bridge` CLI\'s fresh agent session),\nStage 8, Stage 9, and Stage 10 are SKIPPED and the single closing interaction is the concise capability\nreport plus a `/learn-repository` recommendation that the spawn prompt owns. When you invoke\n`/install-bridge` directly (manual invocation), Stages 8, 9, and 10 run normally.\n\n## Stage 1 \u2014 Admin preflight\n\n1. Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `source` is `"legacy"`: proceed (legacy keys are permitted).\n - Else if `role` is `"admin"`: proceed.\n - Otherwise (a non-admin `user_access` key): stop immediately and display:\n ```\n Admin role required to apply install configuration. Ask a project admin to run /install-bridge,\n or use an admin API key.\n ```\n\n## Stage 2 \u2014 Read the manifest (once, pre-apply)\n\n1. Call the `get_install_manifest` MCP tool exactly once here.\n2. Keep the returned `snapshot_token` verbatim \u2014 you must pass the SAME token to the apply call in\n Stage 5. Do not call `get_install_manifest` again before applying; a fresh read would invalidate\n the token you are about to use. (Stage 7 performs a SEPARATE, later read-after-write manifest read\n for the current capability status \u2014 that is deliberate and does not reuse this token.)\n3. The manifest contains ordered `groups` of bootstrap fields (each with `field_name`, `is_set`,\n `current_value`, `guidance`, `examples`, `validation_summary`, `requires_confirmation`), a list of\n `deferred_fields`, a `next_step`, `done_criteria`, a `command_contract_version`, and the\n `snapshot_token`. It also carries the additive capability fields (`integrations`, `configured`,\n `learned`, `indexed`, `tool_capabilities`, `concise_tool_capabilities`, `locked_tools`,\n `unlocked_tools`) \u2014 but ignore those here; the accurate capability status is the post-apply read in\n Stage 7. `tool_capabilities` is the COMPLETE catalog-backed report field (one entry per registered\n MCP tool, grouped and ordered by the server); `concise_tool_capabilities` is the ADDITIVE, bounded\n projection Stage 7 actually renders (see Stage 7); `locked_tools` / `unlocked_tools` are LEGACY\n compatibility data covering only the VCS/index policy cases and are NOT the tool inventory.\n4. Compare the manifest\'s `command_contract_version` to this command\'s contract version (5, stated at\n the top of this file). If the manifest\'s version is HIGHER, this scaffolded command copy is stale:\n warn the user to refresh it by running `npx -y @bridge_gpt/mcp-server --init` in the project root,\n then proceed conservatively \u2014 wherever the manifest\'s `guidance`, `next_step`, or `done_criteria`\n differ from these instructions, follow the manifest.\n\n## Stage 3 \u2014 Derive values for UNSET bootstrap fields only\n\n1. Consider ONLY fields whose `is_set` is `false`. Never re-derive or overwrite a field that is\n already set \u2014 the server will also skip already-set fields, but you should not even propose them.\n2. For each unset field, derive a value from the local codebase following that field\'s `guidance`\n (e.g. infer `working_in` from frameworks/manifests, `version_control_system` from the git remote,\n testing stacks from test config and dependencies, directory fields from the repo layout).\n3. Omit any field you are uncertain about or that does not apply \u2014 leaving it unset is always\n acceptable. Do NOT guess.\n4. Specifically, do NOT invent a `version` when the platform has no meaningful version concept; leave\n it unset.\n5. Before deriving `version_control_system` or `base_branch`, confirm the git repository root is THIS\n project\'s root: `git rev-parse --show-toplevel` must resolve to the project directory itself. If\n the git root is an ancestor directory (the project is nested inside another repository), OMIT both\n fields and note why in the final report \u2014 deriving them would describe the parent repository, not\n this project.\n6. If you find real evidence for TWO OR MORE plausible values for a field (e.g. configs for multiple\n test frameworks, multiple frameworks in a monorepo), treat the value as ambiguous and omit it\n rather than picking one.\n7. Do NOT derive `selected_mcp_slugs`, even though it appears as an unset bootstrap-eligible field in\n the manifest. MCP validation manual selection is deferred to `/learn-repository`, which derives and\n confirms it with the codebase already researched. Install neither proposes nor applies this field.\n\n## Stage 4 \u2014 Human approval for confirmation-requiring fields\n\n`project_description` is the ONLY confirmation-requiring field install proposes. It carries\n`requires_confirmation: true` in the manifest, so it is never applied on derivation alone \u2014 it needs\nexplicit human approval. (`selected_mcp_slugs` also requires confirmation, but install does not\nderive it at all; `/learn-repository` asks for it. See Stage 3 step 7.)\n\n1. If `project_description` is unset and you can draft a concise description from the codebase,\n present the proposed `project_description` text to the human and ask for explicit approval before\n including it.\n2. Include `project_description` in the apply payload ONLY as\n `{ "value": <approved value>, "confirmed": true }`, and only after the human approves it. If the\n human does not approve it, omit the field entirely.\n3. If no human response can be obtained (e.g. a non-interactive / headless session), do NOT stall and\n do NOT abandon the install: proceed to Stage 5 with `project_description` omitted, and report it\n as "pending human input" in the final summary. The other derived fields must still be applied \u2014 an\n unapproved description never blocks them.\n\n## Stage 5 \u2014 Apply (one call)\n\n1. Make exactly ONE call to the `apply_install_manifest` MCP tool, passing:\n - `snapshot_token`: the exact token from Stage 2.\n - `fields`: a map of the derived fields. Scalar fields may be passed directly\n (e.g. `"base_branch": "main"`); an approved `project_description` must use the\n `{ "value": ..., "confirmed": true }` object form from Stage 4. Never include\n `selected_mcp_slugs` in this payload \u2014 install does not derive or apply it (Stage 3 step 7).\n2. Do not implement any client-side skip-if-set, conflict, or confirmation logic \u2014 the server owns\n all of that and returns the outcome buckets.\n3. The apply is partial-tolerant: fields that fail server-side validation are returned in the\n `rejected` bucket while the valid fields still commit. A rejected field is therefore reported,\n not fatal \u2014 do NOT retry the whole apply just because one field was rejected.\n\n## Stage 6 \u2014 Persist the routing credential\n\nThe Bash-spawned `start-tickets` CLI runs in a DIFFERENT runtime surface than this MCP server, so a\nkey that lives only in `.mcp.json` / `.cursor/mcp.json` is invisible to it and difficulty\u2192model\nrouting silently degrades. This final stage persists the already-validated key into the user-scoped\ncredential store (`~/.config/bridge/credentials.json`) so shell-spawned CLI commands can resolve it.\n\n1. Call the `persist_routing_credential` MCP tool with `repo_name` set to the repository name for\n this install (the configured `BAPI_REPO_NAME`). Pass `repo_name` ONLY \u2014 this tool resolves the\n API key inside the MCP server process. NEVER pass or display `BAPI_API_KEY` (nor `api_key`,\n `apiKey`, `secret`, or `token`); the key value must not appear in the tool call or in any output.\n2. On success, tell the user the credential was stored, echoing the returned `target` and `path`,\n e.g. `Stored routing credential for bapi:<repo_name> at <path>`.\n3. On failure, do NOT block the install \u2014 show the rest of the install summary anyway, then tell the\n user the routing credential could not be stored. They can rerun `/install-bridge` after setting\n `BAPI_API_KEY` in the environment, or run\n `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate an\n existing key. Until then, `start-tickets` model routing fails open (assumes a hard ticket and\n defaults to premium/Opus when available) \u2014 this is the MOST EXPENSIVE model tier, so unexplained\n cost is the symptom of leaving this unfixed. Recommend verifying credential resolution afterwards\n with `npx -y @bridge_gpt/mcp-server doctor`.\n\n## Stage 7 \u2014 Summarize the outcome, then present the concise capability report\n\nFirst, begin with an explicit applied count: "Applied N of M derivable fields" (M = the unset\nbootstrap fields you derived values for). An install that applied 0 fields must say so loudly \u2014 the\ninstall is NOT complete until the apply call reports applied fields. This applied-count line and the\nsix-bucket summary below remain the PRIMARY install result \u2014 the capability report that follows is a\nsecondary close, not a replacement for it.\n\nThen summarize the six buckets from the apply response for the user:\n- `applied` \u2014 fields written this run.\n- `skipped` \u2014 fields already set (left untouched).\n- `conflict` \u2014 fields that changed since the manifest was read (re-run /install-bridge to retry).\n- `needs_confirmation` \u2014 fields awaiting explicit confirmation (e.g. project_description).\n- `rejected` \u2014 fields that failed validation or were not bootstrap-eligible (the other fields still\n applied; fix or drop the rejected values before any retry).\n- `deferred` \u2014 fields intentionally not auto-written by install (owned by /learn-repository or set\n deliberately). `selected_mcp_slugs` belongs here: report it as deferred to `/learn-repository`,\n never as approved, declined, or pending an install-time decision.\n\n### Read-after-write: fetch the concise capability report\n\nAfter the bucket summary, call the `get_install_manifest` MCP tool ONCE MORE. This post-apply read\nreflects the configuration you just wrote (the Stage-2 read was pre-apply and is stale for this\npurpose). This read does not need the snapshot token. Use ONLY this post-write response for the\nreport below.\n\nThe response carries `concise_tool_capabilities` \u2014 an ADDITIVE, bounded projection over the complete\n`tool_capabilities` catalog (which the response still carries unchanged; this stage simply does not\nrender it). It is a server-ordered array of at most two tiers, each\n`{id, name, tools: [{tool, display_name}], more_count}`, covering only "Regularly useful" and\n"Occasionally useful", available-now tools only, with everything else in those two tiers collapsed\ninto that tier\'s `more_count`.\n\nServer authority: the server computed this projection\'s tier selection, availability filter, and\n`more_count` arithmetic. Never recompute, re-filter, re-count, or re-derive it from `tool_capabilities`,\n`docs/mcp-tool-integrations.md`, or any other documentation \u2014 render exactly what the server sent.\n\nIf the post-write response has no `concise_tool_capabilities` field at all, or it is present but\nmalformed (not the `{id, name, tools, more_count}` tier shape described above), print exactly:\n`No capability status is available yet; configuration can still continue.` and skip the section below\nentirely \u2014 never fall back to rendering the complete `tool_capabilities` catalog or a remembered/\nhallucinated capability list.\n\nOtherwise, render exactly one section, with this exact heading:\n\n**What Bridge can help with**\n\n- Render each tier from `concise_tool_capabilities` in the server\'s given order: "Regularly useful"\n first, then "Occasionally useful". Do not reorder, filter, re-tier, or drop a tier the server\n included, even if its `tools` array is empty.\n- Within a tier, list each tool\'s `display_name` only, in server order \u2014 no description,\n `availability_text`, effect, dependency explanation, or variant detail; those live on the complete\n `tool_capabilities` field, which this section does not touch.\n- Render the tier\'s `more_count` as plain, muted-style summary text ("+N more") \u2014 never as an\n expansion prompt, a link, or something requiring further action. Omit the "+N more" line entirely\n when `more_count` is `0`.\n- Do not locally filter, count, regroup, infer availability, or fall back to the complete\n `tool_capabilities` collection for this section under any circumstance.\n\n## Stage 8 \u2014 Offer the next step\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it); the\nspawn prompt owns the single closing concise-report-plus-learn-recommendation there. On direct manual\n`/install-bridge` invocation, run it normally:\n\n1. Offer to continue with `/learn-repository` to populate the deeper instruction-tier configuration\n (architecture, review, testing, and correctness standards), matching the manifest\'s `next_step`.\n Mention that it also derives and confirms the applicable MCP validation manuals\n (`selected_mcp_slugs`) \u2014 it researches the codebase first, so it can propose them with evidence\n and explain what they do, which is why install leaves that field alone.\n2. Do NOT ask about repository indexing in any form. There is no consent question, no\n `parse_repository` tool call, and no `/parse-repository` continuation here \u2014 indexing starts\n automatically once the repository reaches full parse readiness (VCS credentials, the Pinecone\n index, `working_in` / `project_description`, and SFCC prerequisites where applicable), via the\n same readiness-gated funnel the GitHub connection-confirm endpoints and the scheduled sweep already\n use. Do not claim indexing has already started \u2014 this command has no visibility into that funnel\'s\n outcome.\n\n## Stage 9 \u2014 Offer CI follow-up configuration (only when CI is detected)\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the\n`config_field` MCP tool. `ci_followup_config` is REPLACE-NOT-AUGMENT \u2014 leaving it NULL already means\nsafe poll-only defaults, so "skip" is always a correct answer.\n\n1. Gate: only offer this when Stage 3 found real CI evidence (e.g. `.github/workflows/*.yml`,\n `bitbucket-pipelines.yml`). With no CI detected, skip this stage and note "no CI detected \u2014 CI\n follow-up not offered" in the final report.\n2. Ask ONE question, referencing the discovered CI by name, with exactly these preset profiles\n (default: skip):\n - **observe** \u2014 poll CI results only, never attempt fixes:\n `{"strategy": "poll_only", "max_iterations": 1, "max_minutes": 10, "instructions": ""}`\n - **self-heal** \u2014 bounded fix-and-iterate loop on the automation\'s own PRs:\n `{"strategy": "fix_and_iterate", "max_iterations": 3, "max_minutes": 45, "instructions": ""}`\n - **skip** (default) \u2014 leave `ci_followup_config` unset (NULL = baseline poll-only behavior).\n3. Do NOT offer or compose a `custom` strategy during install \u2014 free-form CI follow-up instructions\n are a deliberate later act (setup UI or a direct `config_field` update).\n4. When the human picks a profile, call the `config_field` MCP tool once (operation `"update"`,\n `field_name: "ci_followup_config"`, `value`: the profile\'s JSON object). If the server rejects the\n value, retry ONCE with a corrected payload; if it is rejected again, stop, show the proposed JSON\n to the human, and leave the field unset.\n5. Never select a profile in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never enable fix-and-iterate behavior.\n\n## Stage 10 \u2014 Offer the speed-vs-quality repository preference\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the `config_field`\nMCP tool, `field_name: "speed_vs_quality"`. The column defaults to `5` (max quality) for every\nrepository, so "skip" always leaves a safe, valid value in place.\n\n1. Never ask this question in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never block on or silently answer this preference.\n2. Read the current value first via the `config_field` MCP tool (operation `"get"`,\n `field_name: "speed_vs_quality"`) so a reinstall can show the stored value \u2014 not always `5` \u2014 as\n the displayed default rather than silently re-asking from scratch.\n3. Ask ONE question with exactly these five numbered presets (default: the value from step 2, or `5`\n if this is the first install):\n - **1** \u2014 Max speed\n - **2** \u2014 Prefer speed\n - **3** \u2014 Balanced\n - **4** \u2014 Prefer quality\n - **5** \u2014 Max quality (default)\n4. Persist ONLY on an explicit answer \u2014 including an explicitly accepted default \u2014 by calling the\n `config_field` MCP tool once (operation `"update"`, `field_name: "speed_vs_quality"`,\n `value`: the selected integer 1-5). Do not overwrite an existing value when the human gives no\n answer at all (e.g. the session cannot obtain one) \u2014 leave the stored/default value untouched in\n that case, distinct from an explicit accepted-default selection of `5`.\n5. After a successful persist, emit one additive structured log line (mirroring the codebase\'s\n `logging.info(msg, extra={...})` convention for non-response-body observability signals) with\n fields: `event="install.speed_vs_quality"`, `repo_name`, `field_name="speed_vs_quality"`,\n `selected_preset` (the persisted integer), `outcome="persisted"`. Never log this event before the\n `config_field` tool call has confirmed the write.\n6. When skipped, emit the corresponding structured event with `outcome` set to one of\n `"skipped_non_interactive"` (non-interactive session) or `"skipped_no_answer"` (interactive session,\n no explicit answer obtained) \u2014 omit `selected_preset` and any prompt text from this event.\n7. If the `config_field` call is rejected, retry ONCE with a corrected payload; if it is rejected\n again, stop, show the proposed value to the human, and leave the field at its current stored value.\n Do not log a persistence-success event for a rejected or failed write.\n\n## Return\n\nReport the admin check result, the "Applied N of M" count, the\napplied/skipped/conflict/needs_confirmation/rejected field names, any fields omitted for\nnested-repository or ambiguity reasons, the approval outcome for `project_description` \u2014 install\'s\nonly confirmation-requiring field \u2014 (approved / declined / pending human input), the fact that\n`selected_mcp_slugs` is deferred to `/learn-repository` rather than decided here, whether a\nstale-command warning was raised (manifest\n`command_contract_version` higher than this command\'s), whether the routing credential was persisted\n(the returned `target` and `path`, or the non-blocking failure remediation), the "What Bridge can help\nwith" concise capability report from the post-apply read-after-write manifest read, the CI follow-up\noutcome (profile written / skipped / no CI detected / pending / skipped in install-spawn context), the\nspeed-vs-quality preference outcome (persisted with its preset / skipped_non_interactive /\nskipped_no_answer / pending / skipped in install-spawn context), and the `/learn-repository`\nrecommendation.\n',"learn-repository.md":`Learn and document all configuration fields for the repository by running parallel research agents.
3295
+ `,"install-bridge.md":'Bootstrap Bridge API project configuration from the local codebase via the easy-install manifest.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes no arguments; any `$ARGUMENTS` text is ignored.\n\nCommand contract version: **6**. The install manifest returns a `command_contract_version`; Stage 2\ncompares it to this number to detect a stale scaffolded copy of this command.\n\nThis command has two modes, chosen by the project\'s state (Stage 2 decides from the manifest\'s\n`configured` flag), never by the caller\'s role:\n\n- **Fresh configuration** (`configured == false`): the full derive \u2192 approve \u2192 apply \u2192 report flow\n below, for an admin or legacy caller. This is the original, unchanged install path.\n- **JOIN MODE** (`configured == true`): the project is already set up, so this run proposes and\n applies ZERO configuration changes for ANY caller. A new teammate \u2014 including a non-admin "member"\n key \u2014 gets a graceful welcome and the concise capability report instead of an error. An eligible\n b2b admin is additionally offered the teammate-invite stage (Stage 11).\n\nThis command performs a one-time "easy install" of Bridge API project configuration. It reads the\nserver-owned install manifest once, derives values for the **unset** bootstrap fields from the local\ncodebase, asks for human approval of the project description, applies everything in a single atomic\ncall, and then presents a **concise capability report** derived from a fresh read-after-write manifest\nread. The server owns all skip-if-set, conflict, and confirmation semantics \u2014 this command never makes\nits own skip-if-set decisions \u2014 and the server owns the complete tool catalog and the bounded concise\nprojection over it, their grouping and ordering, and every gate and dependency relationship; this\ncommand formats the server\'s contract and never recomputes it from prose. Indexing is never a decision\nthis command makes or asks about: it starts automatically, gated entirely by server-side readiness (see\nStage 8).\n\nThis command is portable across Claude Code, Cursor, and Copilot: it only drives the Bridge API MCP\ntools (`get_my_role`, `get_install_manifest`, `apply_install_manifest`, and \u2014 in the gated Stage 11\nonly \u2014 `invite_member`).\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n**Install-spawn context.** When the spawning instruction explicitly says this `/install-bridge` run is\nin the "install-spawn context" (it was launched by the `install-bridge` CLI\'s fresh agent session),\nStage 8, Stage 9, and Stage 10 are SKIPPED and the single closing interaction is the concise capability\nreport plus a `/learn-repository` recommendation that the spawn prompt owns. When you invoke\n`/install-bridge` directly (manual invocation), Stages 8, 9, and 10 run normally. Stage 11 is NOT part\nof that install-spawn skip set \u2014 it is independently gated (admin + b2b + interactive) and best-effort,\nso it may still run in the install-spawn context for an eligible admin.\n\n## Stage 1 \u2014 Admin preflight (defer the permission decision until the manifest is read)\n\n1. Call the `get_my_role` MCP tool (no parameters). Retain its `role`, `source`, and `customer_type`\n values \u2014 later stages branch on all three (Stage 2\'s mode decision uses `role`/`source`; Stage 11\'s\n invite gate uses `role` and `customer_type`).\n2. Classify the caller, but do NOT stop here \u2014 the member permission decision is DEFERRED until Stage 2\n has read the manifest and determined whether the project is already `configured`. A member must be\n allowed to continue at least far enough to read the manifest, because a member CAN join an\n already-configured project even though a member cannot configure a fresh one:\n - If `source` is `"legacy"`, or `role` is `"admin"`: the caller is configuration-capable (it may run\n the fresh-configuration flow when the project is unconfigured).\n - Otherwise (a non-admin `user_access` "member" key): the caller is join-only. It may proceed into\n JOIN MODE for a configured project, but must be refused if Stage 2 proves the project is not yet\n configured (it cannot configure a fresh repo).\n3. Preserve this exact refusal text for later use \u2014 it is emitted in Stage 2 ONLY when a non-admin\n member reaches a `configured == false` project:\n ```\n Admin role required to apply install configuration. Ask a project admin to run /install-bridge,\n or use an admin API key.\n ```\n\n## Stage 2 \u2014 Read the manifest (once, pre-apply)\n\n1. Call the `get_install_manifest` MCP tool exactly once here.\n2. Keep the returned `snapshot_token` verbatim \u2014 you must pass the SAME token to the apply call in\n Stage 5. Do not call `get_install_manifest` again before applying; a fresh read would invalidate\n the token you are about to use. (Stage 7 performs a SEPARATE, later read-after-write manifest read\n for the current capability status \u2014 that is deliberate and does not reuse this token.)\n3. The manifest contains ordered `groups` of bootstrap fields (each with `field_name`, `is_set`,\n `current_value`, `guidance`, `examples`, `validation_summary`, `requires_confirmation`), a list of\n `deferred_fields`, a `next_step`, `done_criteria`, a `command_contract_version`, and the\n `snapshot_token`. It also carries the additive capability fields (`integrations`, `configured`,\n `learned`, `indexed`, `tool_capabilities`, `concise_tool_capabilities`, `locked_tools`,\n `unlocked_tools`) \u2014 but ignore those here; the accurate capability status is the post-apply read in\n Stage 7. `tool_capabilities` is the COMPLETE catalog-backed report field (one entry per registered\n MCP tool, grouped and ordered by the server); `concise_tool_capabilities` is the ADDITIVE, bounded\n projection Stage 7 actually renders (see Stage 7); `locked_tools` / `unlocked_tools` are LEGACY\n compatibility data covering only the VCS/index policy cases and are NOT the tool inventory.\n4. Compare the manifest\'s `command_contract_version` to this command\'s contract version (6, stated at\n the top of this file). If the manifest\'s version is HIGHER, this scaffolded command copy is stale:\n warn the user to refresh it by running `npx -y @bridge_gpt/mcp-server --init` in the project root,\n then proceed conservatively \u2014 wherever the manifest\'s `guidance`, `next_step`, or `done_criteria`\n differ from these instructions, follow the manifest.\n5. **Decide the install mode from `configured`.** Read the manifest\'s `configured` readiness flag (use\n ONLY `configured` for this decision \u2014 not `learned` or `indexed`) and branch:\n - **`configured == true` \u2192 JOIN MODE, for EVERY caller** (admin, legacy, or member). The project is\n already set up, so this run makes ZERO configuration changes. Emit a concise welcome \u2014 for\n example: "This Bridge project is already configured. You\'re joining it as a new teammate; no\n configuration changes will be proposed or applied." Then SKIP Stages 3, 4, and 5 entirely (no\n field derivation, no project-description approval, no `apply_install_manifest` call, no\n `config_field` writes), run Stage 6 (persist the routing credential), and render the JOIN MODE\n branch of Stage 7 (the concise capability report, drawn directly from THIS Stage-2 manifest \u2014 no\n read-after-write). Then SKIP Stages 8, 9, and 10 for every caller. A member STOPS after the Stage 7\n report; only an eligible admin continues to Stage 11.\n - **`configured == false` \u2192 apply the deferred Stage 1 role decision:**\n - `source == "legacy"` or `role == "admin"`: run the fresh-configuration flow (Stages 3 \u2192 4 \u2192 5 \u2192\n 6 \u2192 7 \u2192 8 \u2192 9 \u2192 10) exactly as written, unchanged.\n - a non-admin `user_access` "member" key: stop immediately and display the exact refusal text\n preserved in Stage 1 ("Admin role required to apply install configuration\u2026"). Do not derive,\n apply, or persist anything.\n - **`configured` absent / indeterminate (neither `true` nor `false`) \u2192 do NOT treat it as `false`.**\n `configured` comes from a best-effort capability enrichment that can silently omit the key on a\n transient server-side probe failure, so a missing value is "unknown", not "unconfigured". Re-read\n the manifest ONCE (a fresh `get_install_manifest` call) to try to resolve it, and branch on the\n refreshed value if it is now definitive. If it is STILL absent:\n - `source == "legacy"` or `role == "admin"`: proceed with the fresh-configuration flow, but NOT\n silently \u2014 first tell the user that configuration status could not be confirmed and that the run\n will attempt configuration anyway (the server owns skip-if-set, so an apply against an\n already-configured repo is a safe no-op).\n - a non-admin `user_access` "member" key: take the JOIN-MODE-safe path \u2014 render the welcome and the\n Stage 7 capability report (no config writes, no offers) and note that configuration status could\n not be confirmed. Do NOT emit the hard "Admin role required" STOP: that refusal is reserved for a\n *definitive* `configured == false`, because treating an unknown state as unconfigured would\n re-introduce the very member hard-refusal this flow removes.\n\n## Stage 3 \u2014 Derive values for UNSET bootstrap fields only\n\n**Skip this entire stage in JOIN MODE** (Stage 2 selected JOIN MODE because the manifest reported\n`configured == true`). JOIN MODE derives nothing \u2014 it proposes and applies zero configuration for every\ncaller. Run this stage only on the `configured == false` fresh-configuration path.\n\n1. Consider ONLY fields whose `is_set` is `false`. Never re-derive or overwrite a field that is\n already set \u2014 the server will also skip already-set fields, but you should not even propose them.\n2. For each unset field, derive a value from the local codebase following that field\'s `guidance`\n (e.g. infer `working_in` from frameworks/manifests, `version_control_system` from the git remote,\n testing stacks from test config and dependencies, directory fields from the repo layout).\n3. Omit any field you are uncertain about or that does not apply \u2014 leaving it unset is always\n acceptable. Do NOT guess.\n4. Specifically, do NOT invent a `version` when the platform has no meaningful version concept; leave\n it unset.\n5. Before deriving `version_control_system` or `base_branch`, confirm the git repository root is THIS\n project\'s root: `git rev-parse --show-toplevel` must resolve to the project directory itself. If\n the git root is an ancestor directory (the project is nested inside another repository), OMIT both\n fields and note why in the final report \u2014 deriving them would describe the parent repository, not\n this project.\n6. If you find real evidence for TWO OR MORE plausible values for a field (e.g. configs for multiple\n test frameworks, multiple frameworks in a monorepo), treat the value as ambiguous and omit it\n rather than picking one.\n7. Do NOT derive `selected_mcp_slugs`, even though it appears as an unset bootstrap-eligible field in\n the manifest. MCP validation manual selection is deferred to `/learn-repository`, which derives and\n confirms it with the codebase already researched. Install neither proposes nor applies this field.\n\n## Stage 4 \u2014 Human approval for confirmation-requiring fields\n\n**Skip this entire stage in JOIN MODE** \u2014 there is nothing to derive, so there is nothing to approve.\nRun it only on the `configured == false` fresh-configuration path.\n\n`project_description` is the ONLY confirmation-requiring field install proposes. It carries\n`requires_confirmation: true` in the manifest, so it is never applied on derivation alone \u2014 it needs\nexplicit human approval. (`selected_mcp_slugs` also requires confirmation, but install does not\nderive it at all; `/learn-repository` asks for it. See Stage 3 step 7.)\n\n1. If `project_description` is unset and you can draft a concise description from the codebase,\n present the proposed `project_description` text to the human and ask for explicit approval before\n including it.\n2. Include `project_description` in the apply payload ONLY as\n `{ "value": <approved value>, "confirmed": true }`, and only after the human approves it. If the\n human does not approve it, omit the field entirely.\n3. If no human response can be obtained (e.g. a non-interactive / headless session), do NOT stall and\n do NOT abandon the install: proceed to Stage 5 with `project_description` omitted, and report it\n as "pending human input" in the final summary. The other derived fields must still be applied \u2014 an\n unapproved description never blocks them.\n\n## Stage 5 \u2014 Apply (one call)\n\n**Skip this entire stage in JOIN MODE** \u2014 JOIN MODE makes NO `apply_install_manifest` call and writes\nzero fields for every caller. Run it only on the `configured == false` fresh-configuration path.\n\n1. Make exactly ONE call to the `apply_install_manifest` MCP tool, passing:\n - `snapshot_token`: the exact token from Stage 2.\n - `fields`: a map of the derived fields. Scalar fields may be passed directly\n (e.g. `"base_branch": "main"`); an approved `project_description` must use the\n `{ "value": ..., "confirmed": true }` object form from Stage 4. Never include\n `selected_mcp_slugs` in this payload \u2014 install does not derive or apply it (Stage 3 step 7).\n2. Do not implement any client-side skip-if-set, conflict, or confirmation logic \u2014 the server owns\n all of that and returns the outcome buckets.\n3. The apply is partial-tolerant: fields that fail server-side validation are returned in the\n `rejected` bucket while the valid fields still commit. A rejected field is therefore reported,\n not fatal \u2014 do NOT retry the whole apply just because one field was rejected.\n\n## Stage 6 \u2014 Persist the routing credential\n\n**This stage runs in BOTH modes** \u2014 JOIN MODE persists the routing credential too, so a joining\nteammate\'s shell-spawned CLI features (`start-tickets`) can resolve the key. Its fail-open behavior\nbelow is unchanged in either mode.\n\nThe Bash-spawned `start-tickets` CLI runs in a DIFFERENT runtime surface than this MCP server, so a\nkey that lives only in `.mcp.json` / `.cursor/mcp.json` is invisible to it and difficulty\u2192model\nrouting silently degrades. This final stage persists the already-validated key into the user-scoped\ncredential store (`~/.config/bridge/credentials.json`) so shell-spawned CLI commands can resolve it.\n\n1. Call the `persist_routing_credential` MCP tool with `repo_name` set to the repository name for\n this install (the configured `BAPI_REPO_NAME`). Pass `repo_name` ONLY \u2014 this tool resolves the\n API key inside the MCP server process. NEVER pass or display `BAPI_API_KEY` (nor `api_key`,\n `apiKey`, `secret`, or `token`); the key value must not appear in the tool call or in any output.\n2. On success, tell the user the credential was stored, echoing the returned `target` and `path`,\n e.g. `Stored routing credential for bapi:<repo_name> at <path>`.\n3. On failure, do NOT block the install \u2014 show the rest of the install summary anyway, then tell the\n user the routing credential could not be stored. They can rerun `/install-bridge` after setting\n `BAPI_API_KEY` in the environment, or run\n `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate an\n existing key. Until then, `start-tickets` model routing fails open (assumes a hard ticket and\n defaults to premium/Opus when available) \u2014 this is the MOST EXPENSIVE model tier, so unexplained\n cost is the symptom of leaving this unfixed. Recommend verifying credential resolution afterwards\n with `npx -y @bridge_gpt/mcp-server doctor`.\n\n## Stage 7 \u2014 Summarize the outcome, then present the concise capability report\n\n**JOIN MODE branch (`configured == true`).** Do NOT print an applied count and do NOT fabricate apply\nbuckets \u2014 no apply happened. Instead state plainly that the project was already configured and that\nzero configuration changes were proposed or applied (the welcome from Stage 2). Then render the concise\ncapability report described in "### Read-after-write" below, with ONE difference: source it directly\nfrom the `concise_tool_capabilities` field of the Stage-2 manifest you already read \u2014 do NOT perform a\nread-after-write `get_install_manifest` call, because no write occurred and there is nothing to\nrefresh. Apply the same server-authority rendering rules (server order, `more_count` handling,\nmalformed/missing fallback) verbatim. After the report, a member is DONE; an eligible admin proceeds to\nStage 11. The rest of this stage (the applied-count line and six-bucket summary) applies ONLY to the\n`configured == false` fresh-configuration path.\n\n**Fresh-configuration branch (`configured == false`).**\nFirst, begin with an explicit applied count: "Applied N of M derivable fields" (M = the unset\nbootstrap fields you derived values for). An install that applied 0 fields must say so loudly \u2014 the\ninstall is NOT complete until the apply call reports applied fields. This applied-count line and the\nsix-bucket summary below remain the PRIMARY install result \u2014 the capability report that follows is a\nsecondary close, not a replacement for it.\n\nThen summarize the six buckets from the apply response for the user:\n- `applied` \u2014 fields written this run.\n- `skipped` \u2014 fields already set (left untouched).\n- `conflict` \u2014 fields that changed since the manifest was read (re-run /install-bridge to retry).\n- `needs_confirmation` \u2014 fields awaiting explicit confirmation (e.g. project_description).\n- `rejected` \u2014 fields that failed validation or were not bootstrap-eligible (the other fields still\n applied; fix or drop the rejected values before any retry).\n- `deferred` \u2014 fields intentionally not auto-written by install (owned by /learn-repository or set\n deliberately). `selected_mcp_slugs` belongs here: report it as deferred to `/learn-repository`,\n never as approved, declined, or pending an install-time decision.\n\n### Read-after-write: fetch the concise capability report\n\nAfter the bucket summary, call the `get_install_manifest` MCP tool ONCE MORE. This post-apply read\nreflects the configuration you just wrote (the Stage-2 read was pre-apply and is stale for this\npurpose). This read does not need the snapshot token. Use ONLY this post-write response for the\nreport below.\n\nThe response carries `concise_tool_capabilities` \u2014 an ADDITIVE, bounded projection over the complete\n`tool_capabilities` catalog (which the response still carries unchanged; this stage simply does not\nrender it). It is a server-ordered array of at most two tiers, each\n`{id, name, tools: [{tool, display_name}], more_count}`, covering only "Regularly useful" and\n"Occasionally useful", available-now tools only, with everything else in those two tiers collapsed\ninto that tier\'s `more_count`.\n\nServer authority: the server computed this projection\'s tier selection, availability filter, and\n`more_count` arithmetic. Never recompute, re-filter, re-count, or re-derive it from `tool_capabilities`,\n`docs/mcp-tool-integrations.md`, or any other documentation \u2014 render exactly what the server sent.\n\nIf the post-write response has no `concise_tool_capabilities` field at all, or it is present but\nmalformed (not the `{id, name, tools, more_count}` tier shape described above), print exactly:\n`No capability status is available yet; configuration can still continue.` and skip the section below\nentirely \u2014 never fall back to rendering the complete `tool_capabilities` catalog or a remembered/\nhallucinated capability list.\n\nOtherwise, render exactly one section, with this exact heading:\n\n**What Bridge can help with**\n\n- Render each tier from `concise_tool_capabilities` in the server\'s given order: "Regularly useful"\n first, then "Occasionally useful". Do not reorder, filter, re-tier, or drop a tier the server\n included, even if its `tools` array is empty.\n- Within a tier, list each tool\'s `display_name` only, in server order \u2014 no description,\n `availability_text`, effect, dependency explanation, or variant detail; those live on the complete\n `tool_capabilities` field, which this section does not touch.\n- Render the tier\'s `more_count` as plain, muted-style summary text ("+N more") \u2014 never as an\n expansion prompt, a link, or something requiring further action. Omit the "+N more" line entirely\n when `more_count` is `0`.\n- Do not locally filter, count, regroup, infer availability, or fall back to the complete\n `tool_capabilities` collection for this section under any circumstance.\n\n## Stage 8 \u2014 Offer the next step\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it); the\nspawn prompt owns the single closing concise-report-plus-learn-recommendation there. On direct manual\n`/install-bridge` invocation, run it normally:\n\n**Also skip this stage in JOIN MODE** (`configured == true`): a joining teammate is offered no\nconfiguration follow-up; they stop after the Stage 7 capability report (an eligible admin continues to\nStage 11).\n\n1. Offer to continue with `/learn-repository` to populate the deeper instruction-tier configuration\n (architecture, review, testing, and correctness standards), matching the manifest\'s `next_step`.\n Mention that it also derives and confirms the applicable MCP validation manuals\n (`selected_mcp_slugs`) \u2014 it researches the codebase first, so it can propose them with evidence\n and explain what they do, which is why install leaves that field alone.\n2. Do NOT ask about repository indexing in any form. There is no consent question, no\n `parse_repository` tool call, and no `/parse-repository` continuation here \u2014 indexing starts\n automatically once the repository reaches full parse readiness (VCS credentials, the Pinecone\n index, `working_in` / `project_description`, and SFCC prerequisites where applicable), via the\n same readiness-gated funnel the GitHub connection-confirm endpoints and the scheduled sweep already\n use. Do not claim indexing has already started \u2014 this command has no visibility into that funnel\'s\n outcome.\n\n## Stage 9 \u2014 Offer CI follow-up configuration (only when CI is detected)\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\n**Also skip this stage in JOIN MODE** (`configured == true`): a joining teammate is offered no\nconfiguration follow-up; they stop after the Stage 7 capability report (an eligible admin continues to\nStage 11).\n\nThis stage is separate from the single apply call: it writes at most ONE field via the\n`config_field` MCP tool. `ci_followup_config` is REPLACE-NOT-AUGMENT \u2014 leaving it NULL already means\nsafe poll-only defaults, so "skip" is always a correct answer.\n\n1. Gate: only offer this when Stage 3 found real CI evidence (e.g. `.github/workflows/*.yml`,\n `bitbucket-pipelines.yml`). With no CI detected, skip this stage and note "no CI detected \u2014 CI\n follow-up not offered" in the final report.\n2. Ask ONE question, referencing the discovered CI by name, with exactly these preset profiles\n (default: skip):\n - **observe** \u2014 poll CI results only, never attempt fixes:\n `{"strategy": "poll_only", "max_iterations": 1, "max_minutes": 10, "instructions": ""}`\n - **self-heal** \u2014 bounded fix-and-iterate loop on the automation\'s own PRs:\n `{"strategy": "fix_and_iterate", "max_iterations": 3, "max_minutes": 45, "instructions": ""}`\n - **skip** (default) \u2014 leave `ci_followup_config` unset (NULL = baseline poll-only behavior).\n3. Do NOT offer or compose a `custom` strategy during install \u2014 free-form CI follow-up instructions\n are a deliberate later act (setup UI or a direct `config_field` update).\n4. When the human picks a profile, call the `config_field` MCP tool once (operation `"update"`,\n `field_name: "ci_followup_config"`, `value`: the profile\'s JSON object). If the server rejects the\n value, retry ONCE with a corrected payload; if it is rejected again, stop, show the proposed JSON\n to the human, and leave the field unset.\n5. Never select a profile in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never enable fix-and-iterate behavior.\n\n## Stage 10 \u2014 Offer the speed-vs-quality repository preference\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\n**Also skip this stage in JOIN MODE** (`configured == true`): a joining teammate is offered no\nconfiguration follow-up; they stop after the Stage 7 capability report (an eligible admin continues to\nStage 11).\n\nThis stage is separate from the single apply call: it writes at most ONE field via the `config_field`\nMCP tool, `field_name: "speed_vs_quality"`. The column defaults to `5` (max quality) for every\nrepository, so "skip" always leaves a safe, valid value in place.\n\n1. Never ask this question in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never block on or silently answer this preference.\n2. Read the current value first via the `config_field` MCP tool (operation `"get"`,\n `field_name: "speed_vs_quality"`) so a reinstall can show the stored value \u2014 not always `5` \u2014 as\n the displayed default rather than silently re-asking from scratch.\n3. Ask ONE question with exactly these five numbered presets (default: the value from step 2, or `5`\n if this is the first install):\n - **1** \u2014 Max speed\n - **2** \u2014 Prefer speed\n - **3** \u2014 Balanced\n - **4** \u2014 Prefer quality\n - **5** \u2014 Max quality (default)\n4. Persist ONLY on an explicit answer \u2014 including an explicitly accepted default \u2014 by calling the\n `config_field` MCP tool once (operation `"update"`, `field_name: "speed_vs_quality"`,\n `value`: the selected integer 1-5). Do not overwrite an existing value when the human gives no\n answer at all (e.g. the session cannot obtain one) \u2014 leave the stored/default value untouched in\n that case, distinct from an explicit accepted-default selection of `5`.\n5. After a successful persist, emit one additive structured log line (mirroring the codebase\'s\n `logging.info(msg, extra={...})` convention for non-response-body observability signals) with\n fields: `event="install.speed_vs_quality"`, `repo_name`, `field_name="speed_vs_quality"`,\n `selected_preset` (the persisted integer), `outcome="persisted"`. Never log this event before the\n `config_field` tool call has confirmed the write.\n6. When skipped, emit the corresponding structured event with `outcome` set to one of\n `"skipped_non_interactive"` (non-interactive session) or `"skipped_no_answer"` (interactive session,\n no explicit answer obtained) \u2014 omit `selected_preset` and any prompt text from this event.\n7. If the `config_field` call is rejected, retry ONCE with a corrected payload; if it is rejected\n again, stop, show the proposed value to the human, and leave the field at its current stored value.\n Do not log a persistence-success event for a rejected or failed write.\n\n## Stage 11 \u2014 Invite teammates (gated: b2b admins only; best-effort)\n\nThis is a best-effort final stage that lets an eligible admin mint teammate keys after configuration or\nJOIN MODE. It is **independently gated** and is NOT part of the install-spawn skip set (Stages 8\u201310) \u2014\nit may run in the install-spawn context for an eligible admin. The entire stage is **fail-open**: a\nprompt failure, a declined offer, a non-interactive context, a malformed tool response, or an\n`invite_member` failure must NEVER cause this command to report the (already-completed) install as\nfailed.\n\n1. **Eligibility gate (AND).** Offer this stage ONLY when BOTH hold, using the values retained in\n Stage 1:\n - `role == "admin"`, AND\n - `customer_type == "b2b"`.\n Otherwise skip the stage silently with NO prompt: a member, a legacy-source caller whose role is not\n explicitly `"admin"`, and a b2c admin all skip. Reachable from BOTH the fresh-admin close (after\n Stage 10) and the JOIN MODE admin path (after the Stage 7 report), including an admin re-run against\n an already-configured b2b project.\n2. **Interactive surface required.** This stage needs a human response. If no interactive response can\n be obtained (a non-TTY / headless / spawn context that cannot prompt), skip the stage silently\n without changing the completed install result \u2014 do not stall.\n3. **Offer prompt.** Ask exactly: `Invite teammates to this project? (y/N)`. Treat a blank answer, `n`,\n `no`, an unavailable response, or any prompt failure as a non-fatal decline \u2014 skip the rest of the\n stage and report it as declined.\n4. **Collect invitees.** On an affirmative answer, collect one or more teammate email entries using\n normal **echoed** input (email is PII, not a secret \u2014 never use a muted/hidden secret prompt).\n Optionally collect a display name per entry.\n5. **Per-invite role.** Default each invitation to role `member`. Only set a specific request\'s role to\n `admin` after an explicit per-invite opt-up for that entry; never opt up by default.\n6. **Mint.** For each invitee, call the `invite_member` MCP tool exactly once with `{email, name?,\n role}`. Do NOT pass `repo_name` \u2014 the tool resolves the repository from the current session.\n7. **Show each key once.** After each successful call, display that response\'s plaintext `api_key`\n exactly once, associated with its intended recipient, followed by the exact warning:\n `Distribute securely; this key is shown once.` Do NOT repeat a minted key anywhere else \u2014 not in the\n final summary, not in retry guidance, not in diagnostics, not in a later stage.\n8. **Per-invite failure isolation.** Treat each mint failure as local to that invitee: report a\n sanitized failure for it, continue to any remaining invitees, and never change the already-completed\n install outcome. Do not surface raw error text, headers, or the caller\'s key.\n\n## Return\n\nThe Return contract depends on which mode Stage 2 selected.\n\n**Fresh-configuration return (`configured == false` admin/legacy path).**\nReport the admin check result, the "Applied N of M" count, the\napplied/skipped/conflict/needs_confirmation/rejected field names, any fields omitted for\nnested-repository or ambiguity reasons, the approval outcome for `project_description` \u2014 install\'s\nonly confirmation-requiring field \u2014 (approved / declined / pending human input), the fact that\n`selected_mcp_slugs` is deferred to `/learn-repository` rather than decided here, whether a\nstale-command warning was raised (manifest\n`command_contract_version` higher than this command\'s), whether the routing credential was persisted\n(the returned `target` and `path`, or the non-blocking failure remediation), the "What Bridge can help\nwith" concise capability report from the post-apply read-after-write manifest read, the CI follow-up\noutcome (profile written / skipped / no CI detected / pending / skipped in install-spawn context), the\nspeed-vs-quality preference outcome (persisted with its preset / skipped_non_interactive /\nskipped_no_answer / pending / skipped in install-spawn context), and the `/learn-repository`\nrecommendation.\n\n**JOIN MODE return (`configured == true` path).**\nReport the caller\'s role, the "you\'re joining an already-configured project" welcome and the explicit\nno-change status (zero configuration proposed or applied \u2014 do NOT report an applied count or apply\nbuckets), whether the routing credential was persisted (the returned `target` and `path`, or the\nnon-blocking failure remediation), and the "What Bridge can help with" concise capability report drawn\nfrom the Stage-2 manifest (no read-after-write). A member ends here.\n\n**Teammate-invitation outcome (Stage 11, both modes).**\nReport the Stage 11 outcome as counts/statuses only \u2014 one of offered, declined, skipped (not eligible,\nor non-interactive context), partially completed, or completed, plus how many keys were minted. Never\nrepeat teammate email addresses or minted key values in this summary.\n',"learn-repository.md":`Learn and document all configuration fields for the repository by running parallel research agents.
3296
3296
 
3297
3297
  $ARGUMENTS
3298
3298
 
@@ -3300,7 +3300,29 @@ $ARGUMENTS
3300
3300
 
3301
3301
  # Instructions
3302
3302
 
3303
- This command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.
3303
+ This command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters. There are exactly two narrow exceptions, both invoked directly by this command and never through the recipe: the \`get_my_role\` admin preflight at entry (immediately below) and the closing \`get_install_manifest\` capability report (step 6).
3304
+
3305
+ ## Admin preflight (UX guard \u2014 run this first)
3306
+
3307
+ Before the onboarding guidance below and before fetching the recipe, call the \`get_my_role\` MCP tool
3308
+ once (the first of the two narrow exceptions above). Retain its \`role\` and \`source\`:
3309
+
3310
+ - If \`source\` is \`"legacy"\`, or \`role\` is \`"admin"\`: continue to the guidance and recipe below.
3311
+ - Otherwise (any non-admin \`user_access\` result \u2014 e.g. a "member" key): stop immediately, run nothing
3312
+ else (no recipe fetch, no research, no configuration write), and display exactly one concise message:
3313
+ \`\`\`
3314
+ Admin role required to learn this repository. Learning writes shared Bridge project configuration,
3315
+ which only an admin may change. Ask a project admin to run /learn-repository, or use an admin API key.
3316
+ \`\`\`
3317
+ - If the \`get_my_role\` call fails or returns a malformed / unrecognized response, treat it as a
3318
+ preflight failure: stop with the same admin-required message rather than proceeding into
3319
+ configuration writes.
3320
+
3321
+ This preflight is a UX guard ONLY \u2014 it fails fast with one clear message instead of the cascade of
3322
+ per-field denials a non-admin would otherwise hit. It is NOT the security control: the authoritative
3323
+ enforcement is the existing server-side admin gate on the \`config_field\` update and
3324
+ \`apply_install_manifest\` routes, which already reject non-admin keys regardless of this client-side
3325
+ check.
3304
3326
 
3305
3327
  1. This command takes no arguments.
3306
3328
 
@@ -4755,17 +4777,16 @@ Options:
4755
4777
  when omitted; you will be asked to confirm).
4756
4778
  --help Show this message.`;function parseConnectGithubArgs(argv){let out={help:!1};for(let i=0;i<argv.length;i+=1){let arg=argv[i];if(arg==="--help"||arg==="-h"){out.help=!0;continue}if(arg==="--repo"){let value=argv[i+1];if(!value||value.startsWith("-"))return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value,i+=1;continue}if(arg.startsWith("--repo=")){let value=arg.slice(7);if(!value)return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value;continue}return arg==="--yes"||arg==="-y"?{ok:!1,error:"connect-github does not support --yes: connecting a repository always requires an explicit confirmation."}:arg==="--installation-id"||arg.startsWith("--installation-id=")?{ok:!1,error:"connect-github does not accept --installation-id: the installation is verified by Bridge from your browser install, not supplied by the caller."}:arg.startsWith("-")?{ok:!1,error:`Unknown option: ${arg}`}:{ok:!1,error:`Unexpected argument: ${arg}`}}return{ok:!0,value:out}}function defaultPromptLine2(promptText){return new Promise(resolve2=>{let rl=readline2.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function defaultOpenBrowser(platform,url){let[command,args]=platform==="darwin"?["open",[url]]:platform==="win32"?["cmd",["/c","start","",url]]:["xdg-open",[url]];return new Promise(resolve2=>{try{let child=spawn6(command,args,{stdio:"ignore",detached:!1,shell:!1});child.on("error",()=>resolve2(!1)),child.on("spawn",()=>resolve2(!0))}catch{resolve2(!1)}})}function createDefaultConnectGithubDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os13.homedir,isTTY:!!process.stdin.isTTY,readFile:filePath=>readFile10(filePath,"utf-8"),stat:async filePath=>({mode:(await stat7(filePath)).mode}),fetch:globalThis.fetch,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),jitter:()=>Math.random(),promptLine:defaultPromptLine2,openBrowser:url=>defaultOpenBrowser(process.platform,url),stdout:message=>process.stdout.write(`${message}
4757
4779
  `),stderr:message=>process.stderr.write(`${message}
4758
- `)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path25.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers \u2014 this session's only closing interaction is the concise capability report and learn recommendation below). Complete the command's read-after-write concise capability report first: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. End with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com",DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool to derive the remaining config, present a","concise capability report, and recommend /learn-repository. Indexing is never","asked about \u2014 it starts automatically once the repository reaches full parse","readiness.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,"existing-key flow below; answer no and it asks for an email and creates a new","Bridge workspace for you (the self-serve flow). That question is asked ONLY for a","bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without","an interactive terminal keeps the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt."," Generate a key in the Bridge API web UI Security page \u2014 this"," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or a negative answer to the key question above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what a negative answer to"," the bare-run key question above reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag the"," checklist starts EMPTY \u2014 no tool is pre-selected"," (not even Claude Code) and you must select at"," least one. A non-interactive (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`,"," Cursor opens `cursor-agent`; if several launchable"," tools are selected the wizard asks which single one"," to open; and a selection whose tools have no"," agentic CLI (e.g. Copilot) opens nothing and prints"," how to finish configuring later."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
4780
+ `)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path25.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com",DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,"existing-key flow below; answer no and it asks for an email and creates a new","Bridge workspace for you (the self-serve flow). That question is asked ONLY for a","bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without","an interactive terminal keeps the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt."," Generate a key in the Bridge API web UI Security page \u2014 this"," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or a negative answer to the key question above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what a negative answer to"," the bare-run key question above reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`,"," Cursor opens `cursor-agent`; if several launchable"," tools are selected the wizard asks which single one"," to open; and a selection whose tools have no"," agentic CLI (e.g. Copilot) opens nothing and prints"," how to finish configuring later."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
4759
4781
  `)}function parseInstallBridgeArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getInstallBridgeUsage()};let apiKey,repo,force=!1,dryRun=!1,agentName,invite,email,tools,inviteSupplied=!1,apiKeySupplied=!1,emailSupplied=!1,readValue=(arg,flag,i)=>arg.startsWith(`${flag}=`)?{value:arg.slice(flag.length+1),nextIndex:i}:i+1>=argv.length?{error:`${flag} requires a value.`}:{value:argv[i+1],nextIndex:i+1};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--force"){force=!0;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--api-key"||arg.startsWith("--api-key=")){let r=readValue(arg,"--api-key",i);if("error"in r)return{status:"error",message:r.error};apiKey=r.value,apiKeySupplied=!0,i=r.nextIndex;continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(inviteSupplied=!0,arg.startsWith("--invite="))invite=arg.slice(9);else{let next=argv[i+1];typeof next=="string"&&!next.startsWith("-")&&(invite=next,i+=1)}continue}if(arg==="--email"||arg.startsWith("--email=")){let r=readValue(arg,"--email",i);if("error"in r)return{status:"error",message:r.error};if(r.value.trim().length===0)return{status:"error",message:"--email requires a non-empty value."};email=r.value.trim(),emailSupplied=!0,i=r.nextIndex;continue}if(arg==="--repo"||arg.startsWith("--repo=")){let r=readValue(arg,"--repo",i);if("error"in r)return{status:"error",message:r.error};repo=r.value,i=r.nextIndex;continue}if(arg==="--tools"||arg.startsWith("--tools=")){let r=readValue(arg,"--tools",i);if("error"in r)return{status:"error",message:r.error};let parsed=parseToolsSelection(r.value);if("error"in parsed)return{status:"error",message:parsed.error};tools=parsed.tools,i=r.nextIndex;continue}if(arg==="--agent"||arg.startsWith("--agent=")){let r=readValue(arg,"--agent",i);if("error"in r)return{status:"error",message:r.error};if(!isAgentName(r.value))return{status:"error",message:`Invalid --agent value: '${r.value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=r.value,i=r.nextIndex;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. install-bridge does not accept positional arguments.`}}return inviteSupplied&&apiKeySupplied?{status:"error",message:"--invite and --api-key are mutually exclusive: a bootstrap invite creates your API key, it does not consume an existing one."}:emailSupplied&&apiKeySupplied?{status:"error",message:"--email and --api-key are mutually exclusive: self-serve signup creates your API key, it does not consume an existing one."}:emailSupplied&&inviteSupplied?{status:"error",message:"--email and --invite are mutually exclusive: use --email for self-serve signup (no pre-issued invite), or --invite to redeem an invite you already have."}:{status:"ok",options:{apiKey,repo,force,dryRun,agentName,invite,inviteMode:inviteSupplied,email,tools}}}function parseToolsSelection(value){let raw=value.split(",").map(s=>s.trim()).filter(s=>s.length>0),seen=new Set;for(let id of raw){if(!isHostPlatformId(id)){let allowed=HOST_PLATFORM_ORDER.join(", ");return{error:`Invalid --tools value: '${id}' (allowed tools: ${allowed}).`}}seen.add(id)}return{tools:HOST_PLATFORM_ORDER.filter(id=>seen.has(id))}}function promptSecretViaReadline(promptText,input=process.stdin,output=process.stderr){return new Promise(resolve2=>{let rl=readline3.createInterface({input,output,terminal:!0}),mutable=rl,muted=!1;mutable._writeToOutput=s=>{muted?s.includes(promptText)&&output.write(promptText):output.write(s)};let answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),output.write(`
4760
- `),resolve2(answer.trim())}),muted=!0})}async function offerGithubConnection(repoName,deps,log){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}let answer=(await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();if(answer==="n"||answer==="no")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn7(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os14.homedir,isTTY,readFile:p=>readFile11(p,"utf-8"),writeFile:(p,data,options)=>writeFile7(p,data,options),mkdir:(p,options)=>mkdir7(p,options),stat:p=>stat8(p),rename:(a,b)=>rename(a,b),chmod:(p,m)=>chmod(p,m),unlink:p=>unlink(p),open:async(p,flags,mode)=>{let handle=await open(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn7),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m)}}async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim()};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No Bridge API key or invite entered. Pass --api-key, set the BAPI_API_KEY environment variable, or try the hidden prompt again."}}return{ok:!1,error:"A Bridge API key or invite is required. Pass --api-key or set the BAPI_API_KEY environment variable (no interactive terminal is available to prompt for it)."}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,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."}}async function resolveSignupEmail(options,deps){if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptLine){let entered=(await deps.promptLine("Email for Bridge workspace setup: ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No email entered."}}return{ok:!1,error:"An email is required to create a Bridge workspace. Pass --email <addr> or set the BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt for it)."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="Do you have a Bridge API key or invite? [Y/n] ";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch};let promptLine=deps.promptLine;try{for(let attempt=0;attempt<5;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();if(answer.length===0||answer==="y"||answer==="yes")return{ok:!0,branch:{kind:"have-key"}};if(answer==="n"||answer==="no")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};deps.log("Please answer y or n (press Enter for yes).")}return{ok:!1,error:"No valid answer to the Bridge API key question. Re-run and answer y or n."}}catch{return{ok:!1,error:"Could not read your answer from the terminal. Re-run with --api-key <key> if you have a Bridge API key, or --email <addr> to create a new Bridge workspace."}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It must match the server-side repository registration."};let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path26.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?";function promptMultiSelectViaReadline(promptText,options,defaults,input=process.stdin,output=process.stderr){return new Promise(resolve2=>{let selected=new Set(defaults),render=()=>{output.write(`
4782
+ `),resolve2(answer.trim())}),muted=!0})}async function offerGithubConnection(repoName,deps,log){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}let answer=(await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();if(answer==="n"||answer==="no")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn7(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os14.homedir,isTTY,readFile:p=>readFile11(p,"utf-8"),writeFile:(p,data,options)=>writeFile7(p,data,options),mkdir:(p,options)=>mkdir7(p,options),stat:p=>stat8(p),rename:(a,b)=>rename(a,b),chmod:(p,m)=>chmod(p,m),unlink:p=>unlink(p),open:async(p,flags,mode)=>{let handle=await open(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn7),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m)}}async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim()};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No Bridge API key or invite entered. Pass --api-key, set the BAPI_API_KEY environment variable, or try the hidden prompt again."}}return{ok:!1,error:"A Bridge API key or invite is required. Pass --api-key or set the BAPI_API_KEY environment variable (no interactive terminal is available to prompt for it)."}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,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."}}async function resolveSignupEmail(options,deps){if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptLine){let entered=(await deps.promptLine("Email for Bridge workspace setup: ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No email entered."}}return{ok:!1,error:"An email is required to create a Bridge workspace. Pass --email <addr> or set the BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt for it)."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="Do you have a Bridge API key or invite? [Y/n] ";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch};let promptLine=deps.promptLine;try{for(let attempt=0;attempt<5;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();if(answer.length===0||answer==="y"||answer==="yes")return{ok:!0,branch:{kind:"have-key"}};if(answer==="n"||answer==="no")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};deps.log("Please answer y or n (press Enter for yes).")}return{ok:!1,error:"No valid answer to the Bridge API key question. Re-run and answer y or n."}}catch{return{ok:!1,error:"Could not read your answer from the terminal. Re-run with --api-key <key> if you have a Bridge API key, or --email <addr> to create a new Bridge workspace."}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It must match the server-side repository registration."};let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path26.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
4761
4783
  ${promptText}
4762
- `),output.write(`[x] = selected \xB7 [ ] = not selected
4763
- `),options.forEach((opt,idx)=>{let mark=selected.has(opt.id)?"[x]":"[ ]";output.write(` ${idx+1}. ${mark} ${opt.label}
4764
- `)}),output.write(`Type numbers to toggle, e.g. 1,3 \u2014 then Enter.
4765
- `),output.write(`Press Enter on an empty line to confirm \u2014 re-typing a number un-selects it.
4766
- `),output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ")},rl=readline3.createInterface({input,output}),answered=!1,finish=()=>{answered=!0,rl.close(),resolve2(options.filter(o=>selected.has(o.id)).map(o=>o.id))};rl.on("close",()=>{answered||resolve2(options.filter(o=>selected.has(o.id)).map(o=>o.id))});let ask=()=>{render(),rl.question("",answer=>{let trimmed=answer.trim();if(trimmed.length===0){if(selected.size===0){output.write(`Select at least one tool.
4767
- `),ask();return}finish();return}let tokens=trimmed.split(",").map(t=>t.trim()),nums=[],bad=!1;for(let tok of tokens){let n=Number(tok);if(!Number.isInteger(n)||n<1||n>options.length){bad=!0;break}nums.push(n)}if(bad){output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.
4768
- `),ask();return}for(let n of nums){let opt=options[n-1];selected.has(opt.id)?selected.delete(opt.id):selected.add(opt.id)}ask()})};ask()})}async function resolveSelectedHostPlatforms(deps,options){if(options.tools!==void 0)return options.tools;let ctx=await buildDetectionContext(deps),detected=new Set(detectDefaultPlatforms(ctx));if(deps.isTTY&&deps.promptMultiSelect){let optionList=allHostTargets().map(t=>({id:t.id,label:t.label})),defaults=[],chosen=await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT,optionList,defaults);return HOST_PLATFORM_ORDER.filter(id=>chosen.includes(id))}let legacy=["claude-code"];return detected.has("cursor")&&legacy.push("cursor"),detected.has("copilot-vscode")&&legacy.push("copilot-vscode"),HOST_PLATFORM_ORDER.filter(id=>legacy.includes(id))}function resolveInstallBridgeLaunchDecision(selectedPlatforms,explicitAgent){if(explicitAgent)return{kind:"spawn",agent:explicitAgent};if(selectedPlatforms.length===0)return{kind:"manual",reason:"empty-selection"};let agents=[];for(let id of HOST_PLATFORM_ORDER){if(!selectedPlatforms.includes(id))continue;let agent=agentForPlatform(id);agent&&!agents.includes(agent)&&agents.push(agent)}return agents.length===0?{kind:"manual",reason:"no-launchable-agent"}:agents.length===1?{kind:"spawn",agent:agents[0]}:{kind:"choose-one",agents}}function toolLabelForLaunchAgent(agent){return allHostTargets().find(t=>t.launchAgent===agent)?.label??agent}async function chooseInstallBridgeLaunchAgent(agents,deps){if(!deps.isTTY||!deps.promptLine)return null;let promptLine=deps.promptLine;try{deps.log(""),deps.log("More than one selected tool can host the configuration session:"),agents.forEach((agent,i)=>{deps.log(` ${String(i+1).padStart(2," ")}. ${toolLabelForLaunchAgent(agent)}`)});let answer=(await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>agents.length?null:agents[index-1]}catch{return null}}function buildManualInstallBridgeContinuation(kind,toolLabels){return kind==="empty-selection"?["No AI coding tools were configured, so nothing was set up for this project.","Re-run install-bridge and select at least one tool to configure it."].join(`
4784
+ `),options.forEach((opt,idx)=>{output.write(` ${idx+1}. ${opt.label}
4785
+ `)}),output.write(`Enter the number(s) of the tools you use (e.g. 1,3), then Enter.
4786
+ `);let prompt=()=>{rl.question("> ",answer=>{let trimmed=answer.trim();if(trimmed.length===0){output.write(`Select at least one tool.
4787
+ `),prompt();return}let tokens=trimmed.split(",").map(t=>t.trim()),indices=[];for(let tok of tokens){if(!SELECTION_TOKEN_PATTERN.test(tok)){output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.
4788
+ `),prompt();return}let n=Number(tok);if(n<1||n>options.length){output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.
4789
+ `),prompt();return}indices.push(n)}let chosen=new Set(indices);finish(options.filter((_,idx)=>chosen.has(idx+1)).map(o=>o.id))})};prompt()})}async function resolveSelectedHostPlatforms(deps,options){if(options.tools!==void 0)return options.tools;let ctx=await buildDetectionContext(deps),detected=new Set(detectDefaultPlatforms(ctx));if(deps.isTTY&&deps.promptMultiSelect){let optionList=allHostTargets().map(t=>({id:t.id,label:t.label})),chosen=await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT,optionList);return HOST_PLATFORM_ORDER.filter(id=>chosen.includes(id))}let legacy=["claude-code"];return detected.has("cursor")&&legacy.push("cursor"),detected.has("copilot-vscode")&&legacy.push("copilot-vscode"),HOST_PLATFORM_ORDER.filter(id=>legacy.includes(id))}function resolveInstallBridgeLaunchDecision(selectedPlatforms,explicitAgent){if(explicitAgent)return{kind:"spawn",agent:explicitAgent};if(selectedPlatforms.length===0)return{kind:"manual",reason:"empty-selection"};let agents=[];for(let id of HOST_PLATFORM_ORDER){if(!selectedPlatforms.includes(id))continue;let agent=agentForPlatform(id);agent&&!agents.includes(agent)&&agents.push(agent)}return agents.length===0?{kind:"manual",reason:"no-launchable-agent"}:agents.length===1?{kind:"spawn",agent:agents[0]}:{kind:"choose-one",agents}}function toolLabelForLaunchAgent(agent){return allHostTargets().find(t=>t.launchAgent===agent)?.label??agent}async function chooseInstallBridgeLaunchAgent(agents,deps){if(!deps.isTTY||!deps.promptLine)return null;let promptLine=deps.promptLine;try{deps.log(""),deps.log("More than one selected tool can host the configuration session:"),agents.forEach((agent,i)=>{deps.log(` ${String(i+1).padStart(2," ")}. ${toolLabelForLaunchAgent(agent)}`)});let answer=(await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>agents.length?null:agents[index-1]}catch{return null}}function buildManualInstallBridgeContinuation(kind,toolLabels){return kind==="empty-selection"?["No AI coding tools were configured, so nothing was set up for this project.","Re-run install-bridge and select at least one tool to configure it."].join(`
4769
4790
  `):[`To finish configuring this project, open it in ${formatToolLabelPhrase(toolLabels)} that has the`,"Bridge MCP server configured and run /install-bridge.","Until the project is configured, your Bridge MCP tools stay limited."].join(`
4770
4791
  `)}function formatToolLabelPhrase(labels){return labels.length===0?"an AI coding tool":labels.length===1?labels[0]:labels.length===2?`${labels[0]} and ${labels[1]}`:`${labels.slice(0,-1).join(", ")}, and ${labels[labels.length-1]}`}var INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX="Bridge can configure and set up this project for you automatically. Open a ";async function requestInstallBridgeLaunchConsent(toolLabel,deps){if(!deps.isTTY||!deps.promptLine)return"no-spawn";try{let answer=(await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `)).trim().toLowerCase();return answer==="n"||answer==="no"?"no-spawn":"spawn"}catch{return"no-spawn"}}async function buildDetectionContext(deps){let cwd=deps.cwd,homedir=deps.homedir(),posixJoin=(base,rel)=>`${base.endsWith("/")?base.slice(0,-1):base}/${rel}`,candidates=[posixJoin(cwd,".cursor"),posixJoin(cwd,".vscode"),posixJoin(cwd,".windsurf"),posixJoin(cwd,".windsurfrules"),posixJoin(homedir,".codex")],present=new Set;return await Promise.all(candidates.map(async p=>{try{await deps.stat(p),present.add(p)}catch{}})),{cwd,homedir,env:deps.env,exists:p=>present.has(p)}}function hostConfigTargetsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id]).filter(t=>t.scope==="project"&&t.format==="json").map(t=>({relPath:t.relPath,topLevelKey:t.topLevelKey}))}function labelsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id].label)}function isPlaceholderApiKey(value){if(typeof value!="string")return!0;let trimmed=value.trim();return trimmed.length===0?!0:trimmed==="YOUR_API_KEY"||trimmed.startsWith("YOUR_")}function buildInstallBridgeServerEntry(cwd,repoName,apiKey,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir,BAPI_API_KEY:apiKey};return{command:entry.command,args:entry.args,env}}async function readHostConfig(deps,fullPath){let raw;try{raw=await deps.readFile(fullPath)}catch{return null}try{let parsed=JSON.parse(raw);return parsed&&typeof parsed=="object"?parsed:null}catch{return null}}async function detectExistingRealKey(deps,targets){for(let target of targets){let entry=(await readHostConfig(deps,path26.join(deps.cwd,target.relPath)))?.[target.topLevelKey]?.["bridge-api"];if(entry?.env&&!isPlaceholderApiKey(entry.env.BAPI_API_KEY))return!0}return!1}async function writeHostConfigs(deps,targets,entry){let written=[];for(let target of targets){let fullPath=path26.join(deps.cwd,target.relPath),parsed=await readHostConfig(deps,fullPath)??{};(!parsed[target.topLevelKey]||typeof parsed[target.topLevelKey]!="object")&&(parsed[target.topLevelKey]={}),parsed[target.topLevelKey]["bridge-api"]=entry,await deps.mkdir(path26.dirname(fullPath),{recursive:!0}),await deps.writeFile(fullPath,JSON.stringify(parsed,null,2)+`
4771
4792
  `,{encoding:"utf-8"}),written.push(target.relPath)}return written}async function provisionSelectedGlobalTargets(deps,platforms,entry){let logLines=[],provisionDeps={fs:{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:async(p,o)=>{await deps.mkdir(p,o)}},vendor:deps.vendor,cwd:deps.cwd,homedir:deps.homedir(),env:deps.env},set=new Set(platforms);for(let id of HOST_PLATFORM_ORDER){if(!set.has(id))continue;let target=MCP_HOST_TARGETS[id];if(target.scope==="project"&&target.format==="json")continue;let outcome2=await provisionHostTarget(target,entry,provisionDeps);switch(outcome2.status){case"vendor-written":case"direct-written":case"created":logLines.push(` configured ${target.label} (${outcome2.displayPath})`);break;case"manual-required":logLines.push(` ${target.label}: add the bridge-api MCP server manually to ${outcome2.displayPath} (the API key is redacted in printed instructions).`);break;case"skipped-invalid":logLines.push(` ${target.label}: skipped ${outcome2.displayPath} \u2014 existing config is not valid; left untouched.`);break;case"failed":logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);break}}return logLines}function buildPingUrl(baseUrl,repoName){let url=new URL(`${baseUrl.replace(/\/+$/,"")}/jira/ping`);return url.searchParams.set("repo_name",repoName),url.toString()}var CONNECTIVITY_ACCESS_DENIED_FALLBACK="The Bridge API denied access to this repository (HTTP 403). Verify the repo_name and that this credential is authorized for it.";async function verifyConnectivity(deps,baseUrl,repoName,apiKey){let url=buildPingUrl(baseUrl,repoName),resp;try{resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{ok:!1,message:`Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`}}if(resp.ok)return{ok:!0};if(resp.status===401)return{ok:!1,message:`The Bridge API rejected the credential (HTTP ${resp.status}). The API key may be invalid or expired \u2014 generate a fresh one in the Bridge API web UI Security page. (An expired token can also surface as a permission error.)`};if(resp.status===403){let detail;try{detail=(await resp.json())?.detail}catch{return{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return typeof detail=="string"&&detail.trim().length>0?{ok:!1,message:detail.trim()}:{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return resp.status===404?{ok:!1,message:`The Bridge API could not find repo '${repoName}' (HTTP 404). Confirm --repo matches the server-side repository registration exactly.`}:{ok:!1,message:`Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`}}function buildResolveRepoUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/resolve-repo`}async function resolveRepoViaServer(fetchImpl,baseUrl,apiKey){let url=buildResolveRepoUrl(baseUrl),resp;try{resp=await fetchImpl(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{status:"error"}}if(resp.status===404)return{status:"not-deployed"};if(resp.status===409)return{status:"unresolved"};if(!resp.ok)return{status:"error"};let body;try{body=await resp.json()}catch{return{status:"error"}}let repoName=body?.repo_name,validated=validateRepoName(repoName);return validated.ok?{status:"resolved",repoName:validated.value}:{status:"error"}}var BOOTSTRAP_KEY_SECRET_BYTES=32;function generateBootstrapKeySecret(randomBytes3){return randomBytes3(BOOTSTRAP_KEY_SECRET_BYTES).toString("base64url")}function fingerprintBootstrapInvite(token){return createHash3("sha256").update(token,"utf-8").digest("hex")}function buildBootstrapExchangeUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap`}async function exchangeBootstrapInvite(deps,baseUrl,token,repoName,keySecret){let url=buildBootstrapExchangeUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,repo_name:repoName,key_secret:keySecret}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,kind:"failed",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.`}}if(resp.ok){let repo;try{repo=(await resp.json())?.repo_name}catch{return{ok:!1,kind:"failed",message:"The Bridge API returned an unreadable response to the bootstrap exchange."}}let validated=validateRepoName(repo);return validated.ok?{ok:!0,repoName:validated.value}:{ok:!1,kind:"failed",message:"The Bridge API returned an unexpected repo name for the bootstrap exchange."}}return resp.status===409?{ok:!1,kind:"repo-name-taken",message:`The repo name '${repoName}' is already taken (HTTP 409). Repo names are globally unique.`}:resp.status===401?{ok:!1,kind:"invalid-invite",message:`The Bridge API rejected the bootstrap invite (HTTP ${resp.status}).`}:{ok:!1,kind:"failed",message:`The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`}}var BOOTSTRAP_INVITE_TOKEN_PREFIX="bapi_inv_";function classifyEnteredCredential(value){return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?"invite":"api-key"}function buildSelfServeMintUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap/self-serve`}async function mintSelfServeInvite(deps,baseUrl,email){let url=buildSelfServeMintUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({invitee_email:email}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"failed"}}if(resp.ok){let token;try{token=(await resp.json())?.token}catch{return{ok:!1,category:"failed"}}return typeof token!="string"||token.trim().length===0||!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?{ok:!1,category:"failed"}:{ok:!0,token}}return resp.status===429?{ok:!1,category:"rate-limited"}:resp.status===400||resp.status===422?{ok:!1,category:"invalid"}:{ok:!1,category:"failed"}}var BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE=["The Bridge API rejected the bootstrap invite (HTTP 401).","","Either the invite is invalid, expired, or revoked \u2014 or it was ALREADY redeemed from this","machine and the local secret has since been lost (e.g. ~/.config/bridge was deleted).","","If it was already redeemed, you cannot recover it yourself:"," \u2022 Re-running will NOT work: each run without the original local secret sends a new one,"," which cannot match what the server stored, so it will keep returning 401."," \u2022 A new bootstrap invite will NOT work either: your repo name is globally unique and is"," now taken by the project you already created, so it cannot be redeemed again.","","Ask your Bridge API operator to recover it for you: they revoke the orphaned key","(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project","(POST /setup/keys), then send you that key. Run install-bridge with --api-key <that key>."].join(`
@@ -4894,7 +4915,7 @@ ${content.slice(0,MAX_INLINE_TEXT_LENGTH)}
4894
4915
 
4895
4916
  [Content truncated. Full content saved to ${resolvedSave}]`:resultText+=`
4896
4917
 
4897
- ${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text}]}}case"delete":{let{ticket_number,file_name}=args,url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an implementation plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 10-15 minutes depending on ticket complexity and number of attachments. The matching get_plan tool retrieves the generated plan later (call get_plan with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns the plan directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 10-15 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("plan",args));registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Estimate a Jira Epic or ticket-key group via the epic estimation orchestrator. Exactly one of epic_key/ticket_keys required (never both). allow_partial allows a partial result on child failures (default: fail-closed). No mode field; source is inferred.",inputSchema:{epic_key:z16.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z16.array(z16.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z16.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("request_architecture",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an architecture plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_architecture tool retrieves the generated architecture plan later (call get_architecture with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("architecture",args));registerTool("request_prd",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("prd",args));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:z16.string().optional().describe("Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."),provider:z16.string().optional().describe("Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence.")}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("request_clarifying_questions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of clarifying questions or debugging guidance for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_clarifying_questions tool retrieves the generated questions later (call get_clarifying_questions with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns them directly. For bug tickets, the result may be debugging guidance instead of clarifying questions. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("clarifying_questions",args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a Jira ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_critique` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));registerTool("request_ticket_critique",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a ticket critique for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_ticket_critique tool retrieves the generated critique later (call get_ticket_critique with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("ticket_critique",args));registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two requests sequentially. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-6 minutes. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until both documents are ready and receive them concatenated.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z16.union([z16.literal(1),z16.literal(2),z16.literal("1"),z16.literal("2"),z16.literal("")]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing.")}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented Jira ticket. Use this for follow-up requests on tickets that have already been through the plan+implement cycle. This triggers an asynchronous background job to process new attachments/images. The matching get_reimplement_context tool retrieves the assembled context later (call get_reimplement_context with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Set wait_for_result to true to block until the context is ready instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a Jira ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z16.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z16.array(z16.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_jira_transitions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List available Jira workflow transitions for a ticket. Returns each transition's id, name, and target status. Use this to discover what status changes are possible for a given ticket. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-transitions`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',inputSchema:{ticket_number:commonFields.ticket_number,target_status:z16.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z16.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("resolve_target_status",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,force_rerun:z16.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")}},async({ticket_number,force_rerun})=>{let payload={repo_name:REPO_NAME,ticket_number};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-target-status"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","ai_automation_level","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", ");registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list.",inputSchema:z16.discriminatedUnion("operation",[z16.object({operation:z16.literal("get"),field_name:z16.string().describe(`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`)}).strict(),z16.object({operation:z16.literal("update"),field_name:z16.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`),value:z16.union([z16.string(),z16.boolean(),z16.array(z16.string()),z16.array(z16.record(z16.string(),z16.unknown())),z16.record(z16.string(),z16.union([z16.string(),z16.null()]))]).optional().describe(`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z16.string().optional().describe("Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."),only_if_null:z16.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}).strict(),z16.object({operation:z16.literal("list")}).strict()])},async args=>{switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("get_my_role",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:'Check the role and auth source for the current API key. Returns JSON with {role: "admin" | "member" | null, source: "user_access" | "legacy"}. Use this to check if the current key has admin permissions before attempting configuration updates via config_field with operation: "update". Non-admin user_access keys will be blocked from config updates.',inputSchema:{}},async()=>{let url=buildGetUrl("/my-role",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. concise_tool_capabilities is an additive projection (tier1/tier2, available_now only, {tools: [{tool, display_name}], more_count}); render as given, never recompute. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text);text=text+note}return{content:[{type:"text",text}]}});registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z16.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z16.record(z16.string(),z16.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud"){let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text+=`
4918
+ ${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text}]}}case"delete":{let{ticket_number,file_name}=args,url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an implementation plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 10-15 minutes depending on ticket complexity and number of attachments. The matching get_plan tool retrieves the generated plan later (call get_plan with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns the plan directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 10-15 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("plan",args));registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Estimate a Jira Epic or ticket-key group via the epic estimation orchestrator. Exactly one of epic_key/ticket_keys required (never both). allow_partial allows a partial result on child failures (default: fail-closed). No mode field; source is inferred.",inputSchema:{epic_key:z16.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z16.array(z16.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z16.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("request_architecture",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an architecture plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_architecture tool retrieves the generated architecture plan later (call get_architecture with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("architecture",args));registerTool("request_prd",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("prd",args));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:z16.string().optional().describe("Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."),provider:z16.string().optional().describe("Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence.")}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("request_clarifying_questions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of clarifying questions or debugging guidance for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_clarifying_questions tool retrieves the generated questions later (call get_clarifying_questions with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns them directly. For bug tickets, the result may be debugging guidance instead of clarifying questions. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("clarifying_questions",args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a Jira ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_critique` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));registerTool("request_ticket_critique",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a ticket critique for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_ticket_critique tool retrieves the generated critique later (call get_ticket_critique with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("ticket_critique",args));registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two requests sequentially. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-6 minutes. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until both documents are ready and receive them concatenated.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z16.union([z16.literal(1),z16.literal(2),z16.literal("1"),z16.literal("2"),z16.literal("")]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing.")}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented Jira ticket. Use this for follow-up requests on tickets that have already been through the plan+implement cycle. This triggers an asynchronous background job to process new attachments/images. The matching get_reimplement_context tool retrieves the assembled context later (call get_reimplement_context with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Set wait_for_result to true to block until the context is ready instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a Jira ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z16.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z16.array(z16.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_jira_transitions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List available Jira workflow transitions for a ticket. Returns each transition's id, name, and target status. Use this to discover what status changes are possible for a given ticket. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-transitions`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',inputSchema:{ticket_number:commonFields.ticket_number,target_status:z16.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z16.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("resolve_target_status",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,force_rerun:z16.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")}},async({ticket_number,force_rerun})=>{let payload={repo_name:REPO_NAME,ticket_number};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-target-status"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","ai_automation_level","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", ");registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list.",inputSchema:z16.discriminatedUnion("operation",[z16.object({operation:z16.literal("get"),field_name:z16.string().describe(`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`)}).strict(),z16.object({operation:z16.literal("update"),field_name:z16.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`),value:z16.union([z16.string(),z16.boolean(),z16.array(z16.string()),z16.array(z16.record(z16.string(),z16.unknown())),z16.record(z16.string(),z16.union([z16.string(),z16.null()]))]).optional().describe(`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z16.string().optional().describe("Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."),only_if_null:z16.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}).strict(),z16.object({operation:z16.literal("list")}).strict()])},async args=>{switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("get_my_role",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:'Check the role, auth source, and account type for the current API key. Returns JSON {role: "admin"|"member"|null, source: "user_access"|"legacy", customer_type: "b2b"|"b2c"}. Use it to check admin permissions before config_field "update" calls (non-admin user_access keys are blocked) and to gate b2b-only onboarding steps.',inputSchema:{}},async()=>{let url=buildGetUrl("/my-role",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("invite_member",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:'Invite a teammate to THIS configured project by minting a scoped user_access key. Admin-only (a member key or a key scoped to another repo is rejected server-side). repo_name is resolved from the session \u2014 do NOT pass it. Args: email (required), name?, role ("member"|"admin", default member). Returns {id, api_key}; the plaintext api_key is shown EXACTLY ONCE \u2014 relay it verbatim so the admin can distribute it out-of-band.',inputSchema:{email:z16.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z16.string().optional().describe("Optional display name for the teammate."),role:z16.enum(["member","admin"]).optional().default("member").describe('Role for the minted key. Defaults to "member"; pass "admin" only to deliberately opt this invite up to admin.')}},async({email,name,role})=>{try{let resp=await fetch(buildApiUrl("/setup/keys/mint"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,email,name,role:role??"member"})});return{content:[{type:"text",text:await handleResponse(resp)}]}}catch{return{content:[{type:"text",text:JSON.stringify({error:"SERVICE_UNAVAILABLE",status:503,message:"The connection to Bridge API failed before the invite could be submitted. Retry inviting the teammate."})}]}}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. concise_tool_capabilities is an additive projection (tier1/tier2, available_now only, {tools: [{tool, display_name}], more_count}); render as given, never recompute. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text);text=text+note}return{content:[{type:"text",text}]}});registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z16.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z16.record(z16.string(),z16.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud"){let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text+=`
4898
4919
 
4899
4920
  \u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`)}}catch{}return{content:[{type:"text",text}]}});registerTool("persist_routing_credential",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",inputSchema:{repo_name:z16.string().describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process.")}},async({repo_name})=>{let repoName=typeof repo_name=="string"?repo_name.trim():"",deps=buildCredentialStoreWriteDeps(),storePath=getPrimaryCredentialStorePath(deps);if(repoName.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,message:"Cannot persist routing credential: repo_name is required. Pass the repo name this install is configuring.",path:storePath})}]};let target=`bapi:${repoName}`,apiKey=await getResolvedApiKeyForRepo(repoName);if(apiKey.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,target,path:storePath,message:`No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`})}]};let result=await upsertBapiCredential(repoName,apiKey,deps);return result.ok?{content:[{type:"text",text:JSON.stringify({ok:!0,action:result.action,target:result.target,path:result.path,migratedFallback:result.migratedFallback,message:`Stored routing credential for ${result.target} at ${result.path}.`})}]}:{content:[{type:"text",text:JSON.stringify({ok:!1,target:result.target,path:result.path,kind:result.kind,message:`Failed to persist routing credential for ${result.target}: ${result.error} You can rerun /install-bridge or migrate manually.`})}]}});function formatDeepResearchProviderReason(meta){if(!meta)return"";let parts=[],reason=meta.incomplete_details?.reason;reason&&parts.push(`provider reason: ${reason}`);let errMsg=meta.error?.message,errCode=meta.error?.code;return(errMsg||errCode)&&(errCode&&errMsg?parts.push(`provider error: ${errCode}: ${errMsg}`):errMsg?parts.push(`provider error: ${errMsg}`):errCode&&parts.push(`provider error: ${errCode}`)),parts.length?` (${parts.join("; ")})`:""}function _safeIsoMs(value){if(!value)return null;let ms=new Date(value).getTime();return Number.isNaN(ms)?null:ms}function formatDeepResearchElapsed(createdAt,lastPollAt){let createdMs=_safeIsoMs(createdAt);if(createdMs===null)return"";let now=Date.now(),startedMs=Math.max(0,now-createdMs),startedMin=Math.floor(startedMs/6e4),lastPollMs=_safeIsoMs(lastPollAt),pollSuffix="";return lastPollMs!==null&&(pollSuffix=`, last poll ${Math.max(0,Math.floor((now-lastPollMs)/1e3))}s ago`),` (running ${startedMin}m${pollSuffix})`}function formatDeepResearchFailure(body){let kind=body.error_kind||body.error_message||"Unknown error",reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`}function formatDeepResearchStatus(body,taskId){let elapsed=formatDeepResearchElapsed(body.created_at,body.last_poll_at),reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`}registerTool("request_deep_research",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",inputSchema:{query:z16.string().describe("The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"),context:z16.string().optional().describe("Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"),ticket_number:commonFields.ticket_number.optional(),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally}},async({query,context,ticket_number,wait_for_result,save_locally})=>{let submitPayload={repo_name:REPO_NAME,query};context&&(submitPayload.context=context),ticket_number&&(submitPayload.ticket_number=ticket_number);let submitResp=await fetch(buildUrl("/deep-research"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let taskId=(await submitResp.json()).task_id;if(!wait_for_result)return{content:[{type:"text",text:`Deep research submitted (task_id: ${taskId}). Processing typically takes 2-10 minutes. Use get_deep_research with task_id ${taskId} to retrieve the result once processing completes.`}]};let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,lastStatus="queued",latestStatusBody=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3);console.error(`Deep research in progress... (elapsed: ${elapsed}s, status: ${lastStatus})`);let statusUrl=buildGetUrl(`/deep-research/${taskId}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok){let errorText=await handleResponse(statusResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error polling deep research status: ${errorText}`})}]}}let statusBody=await statusResp.json();if(lastStatus=statusBody.status,latestStatusBody=statusBody,lastStatus==="completed")break;if(lastStatus==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}if(lastStatus!=="completed"){let statusSuffix=latestStatusBody?` ${formatDeepResearchStatus(latestStatusBody,taskId)}`:"";return{content:[{type:"text",text:`Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`}]}}let resultUrl=buildGetUrl(`/deep-research/${taskId}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok){let errorText=await handleResponse(resultResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error retrieving deep research result: ${errorText}`})}]}}let resultText=await resultResp.text();if(save_locally){let slug=slugify(query),note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${taskId}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});registerTool("get_deep_research",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",inputSchema:{task_id:z16.number().describe("The task ID returned by request_deep_research."),query_slug:z16.string().optional().describe("Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."),save_locally:commonFields.save_locally}},async({task_id,query_slug,save_locally})=>{let statusUrl=buildGetUrl(`/deep-research/${task_id}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(statusBody.status==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};if(statusBody.status!=="completed")return{content:[{type:"text",text:formatDeepResearchStatus(statusBody,task_id)}]};let resultUrl=buildGetUrl(`/deep-research/${task_id}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let resultText=await resultResp.text();if(save_locally){let slug=query_slug||"research",note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${task_id}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});var BRAINSTORM_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isBrainstormTerminalStatus(status){return BRAINSTORM_TERMINAL_STATUSES.has(status)}async function pollBrainstormUntilTerminal(brainstormId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"brainstorm_id",handleValue:brainstormId,recoveryGetUrl:buildGetUrl(`/brainstorms/${brainstormId}/result`,{repo_name:repoName}),retrievalToolName:"get_council"};for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3),statusUrl=buildGetUrl(`/brainstorms/${brainstormId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${brainstormId} status poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation2=`Council ${brainstormId} stopped polling after ${MAX_CONSECUTIVE_POLL_FAILURES} consecutive connection failures.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation2,recovery)}}Date.now()-startTime>6e4&&(pollIntervalMs=3e4);continue}if(consecutiveFetchFailures=0,!statusResp.ok)return{kind:"status",envelope:latest};if(latest=await statusResp.json(),latest.rows.every(row=>isBrainstormTerminalStatus(row.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${brainstormId} timed out after ${Math.round(MAX_TIMEOUT_MS/1e3)} seconds. The task may still be processing on the server.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation,recovery)}}async function saveBrainstormResultsLocally(envelope,subject){let dir=await getDocsPath("brainstorm");return saveBrainstormResultsToDir(envelope,dir,subject)}function formatBrainstormToolResponse(envelope,savedPaths){let lines=[];lines.push(`# Council ${envelope.brainstorm_id}`),lines.push(`Repo: ${envelope.repo_name}`),lines.push("");for(let row of envelope.results)lines.push(`## ${row.provider} \u2014 status: ${row.status}`),lines.push(`error_kind: ${row.error_kind??"null"}`),row.error_message&&lines.push(`error_message: ${row.error_message}`),row.markdown&&(lines.push(""),lines.push(row.markdown)),lines.push("");if(savedPaths.length>0){lines.push("---"),lines.push("Saved files:");for(let p of savedPaths)lines.push(`- ${p}`)}return lines.join(`
4900
4921
  `)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start an async council that fans out a task to multiple opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_council to retrieve. Generates and persists a retrievable artifact.",inputSchema:{task_description:z16.string().describe("Free-form description of the task to brainstorm about. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z16.array(z16.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z16.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_brainstorm_id:z16.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. When provided, the prior brainstorm's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z16.enum(["technical","design","discovery","general"]).optional().describe("Preferred brainstorm-mode selector for new callers. 'technical' (default) is the implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped discovery questions for early/vague tasks. 'general' brainstorms from the supplied brief alone, with no indexed repository context required, unlike 'technical'/'discovery'. Takes precedence over the legacy boolean design field."),design:z16.boolean().optional().describe('Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'),lenses:z16.array(z16.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z16.boolean().optional().describe("Opt-in to trigger a second cross-examination debate round between providers (default off). When true, after round 1 completes each provider critiques the OTHER provider(s)' round-1 output, and the critique is appended to that provider's markdown under a '## Cross-examination' section.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_brainstorm_id,mode,design,lenses,debate})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_brainstorm_id&&(submitPayload.prior_brainstorm_request_id=prior_brainstorm_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let submitResp;try{submitResp=await fetch(buildUrl("/brainstorms"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request_council tool")}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (brainstorm_id: ${submitBody.brainstorm_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`}]};let pollOutcome=await pollBrainstormUntilTerminal(submitBody.brainstorm_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (brainstorm_id: ${submitBody.brainstorm_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope,task_description)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to retrieve the result envelope for a previously submitted council by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new council \u2014 use request_council first if none exists. Returns not-found when still processing.",inputSchema:{brainstorm_id:z16.string().describe("The brainstorm_id (UUID) returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({brainstorm_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/brainstorms/${brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z16.string().describe("The source branch name for the pull request"),base_branch:z16.string().describe("The target/destination branch name for the pull request"),title:z16.string().describe("The title of the pull request"),body:z16.string().optional().describe("The description/body of the pull request")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to discover checks for"),force_rerun:z16.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text=await handleResponse(resp);try{JSON.parse(text).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text=await handleResponse(resp);try{let parsed=JSON.parse(text);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text}]}});async function checkCiConfigAndDisablePoll(){try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&(pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: ci_check_config is null"))}else pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: could not read ci_check_config")}catch(err){pollCiChecksTool.disable(),console.error(`poll_ci_checks disabled: ${err}`)}}await checkCiConfigAndDisablePoll();registerTool("get_docs_dir",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, which requires shell access and may be blocked on some AI coding platforms.",inputSchema:{}},async()=>({content:[{type:"text",text:await getDocsDir()}]}));async function buildPipelineOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}async function buildChainOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,chainRecipes:CHAIN_RECIPES,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z16.array(z16.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z16.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z16.union([z16.literal(1),z16.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",rounds:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea)),(rounds===1||rounds===2)&&(mergedVariables.rounds=String(rounds));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED});return{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os16.tmpdir(),entries;try{entries=await readdir3(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path36.join(tmpDir,entry);try{let info=await stat10(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm3(fullPath,{recursive:!0,force:!0})}catch{}}}registerTool("materialize_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',inputSchema:{base_branch:z16.string().optional().describe(`Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z16.string().optional().describe("Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z16.string().optional().describe('Pass "true" to skip fetch/materialization and fall back to the local project root as-is.')}},async({base_branch,base_sha,no_refresh_base})=>{let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp3(path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path36.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink3(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}});registerTool("cleanup_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:"Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",inputSchema:{fresh_base_root:z16.string().describe("The fresh_base_root path returned by a prior materialize_fresh_base call.")}},async({fresh_base_root})=>{let allowedPrefix=path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path36.resolve(fresh_base_root),resolvedTmpDir=path36.resolve(os16.tmpdir()),isDirectChildOfTmpDir=path36.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path36.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm3(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})});ACTIVE_GROUPS.has("pipeline-authoring")&&(registerTool("list_pipelines",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"List all available pipeline recipes with their names, descriptions, and required variables. No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",inputSchema:{}},async()=>{await ensureCustomPipelinesLoaded();let list=Object.entries(PIPELINES2).map(([key,pipeline])=>({name:key,description:pipeline.description??"",variables:(pipeline.variables??[]).filter(v=>v!=="docs_dir"&&v!=="idea_hash"),source:userPipelineKeys.has(key)?"user":"bundled"}));return{content:[{type:"text",text:JSON.stringify(list,null,2)}]}}),registerTool("run_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional().describe("When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."),ttl_seconds:z16.number().int().positive().optional().describe("Override the default 24-hour idle TTL for this run. Must be a positive integer.")}},async input=>{let result=await runPipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("resume_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",inputSchema:{pipeline_run_id:z16.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),agent_result:z16.string().describe("The string the paused instruction's ## Return section asked you to produce")}},async input=>{let result=await resumePipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("list_pipeline_runs",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",inputSchema:{status:z16.enum(["running","paused","completed","failed","expired"]).optional().describe("Optional status filter")}},async input=>{let result=await listPipelineRuns(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("delete_pipeline_run",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",inputSchema:{pipeline_run_id:z16.string().describe("UUID of the pipeline run to delete.")}},async input=>{let result=await deletePipelineRun(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}));registerTool("run_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",inputSchema:{idea:z16.string().optional(),idea_file:z16.string().optional(),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional(),scheduled_at:z16.string().optional(),max_children:z16.number().int().positive().optional(),allow_duplicate:z16.boolean().optional(),agent:z16.enum(["claude"]).optional(),ttl_seconds:z16.number().int().positive().optional()}},async input=>{let{idea,idea_file,...rest}=input;if(idea!==void 0&&idea_file!==void 0)return{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:"Provide exactly one of `idea` or `idea_file`, not both."})}]};let resolved=await resolveTextOrFile(idea,idea_file,"idea");if(!resolved.ok)return resolved.errorResponse;let result=await runFullAutomation(await buildChainOrchestratorDeps(),{idea:resolved.text,...rest});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("resume_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",inputSchema:{chain_run_id:z16.string(),agent_result:z16.string()}},async input=>{let result=await resumeFullAutomation(await buildChainOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path36.posix.isAbsolute(value)||path36.win32.isAbsolute(value)||path36.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path36.resolve(await getDocsDir()),resolvedTarget=path36.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path36.sep)?{ok:!0,docsPath:path36.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options). Example: {"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z16.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path36.resolve(path36.dirname(fileURLToPath3(import.meta.url)),"../"),assetsDir;try{await stat10(path36.join(projectRootForAssets,"design-assets")),assetsDir=path36.join(projectRootForAssets,"design-assets")}catch{assetsDir=path36.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile14(path36.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile14(path36.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir12(docsPath,{recursive:!0}),await writeFile12(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,gate.handleList),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");var transport=new StdioServerTransport;await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();console.error(`Bridge API MCP server ${VERSION} running on stdio`);(async()=>{await new Promise(r=>setTimeout(r,2e3));let result=await checkForUpdate();result?.updateAvailable&&server.server.sendLoggingMessage({level:"notice",logger:"bridge-api",data:`Update available: running ${result.currentVersion}, latest ${result.latestVersion} \u2014 run npx -y @bridge_gpt/mcp-server@latest --upgrade`})})().catch(()=>{});pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};