@bridge_gpt/mcp-server 0.2.28 → 0.2.29
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/README.md +11 -4
- package/build/agent-registry.js +5 -0
- package/build/conductor-bin.js +1 -1
- package/build/index.js +13 -12
- package/build/install-bridge.js +165 -32
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +19 -9
- package/build/version.generated.js +1 -1
- package/package.json +1 -1
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.
|
|
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(`
|
|
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"}}},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"},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)}
|
|
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 ")&¤t){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(`
|
|
109
|
+
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&¤t){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(`
|
|
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? [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.\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. 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? [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 **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:
|
|
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
|
|
|
@@ -4755,21 +4755,22 @@ Options:
|
|
|
4755
4755
|
when omitted; you will be asked to confirm).
|
|
4756
4756
|
--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
4757
|
`),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. Falls back to the BAPI_API_KEY env var,"," then an interactive (no-echo) prompt. Generate one in the"," Bridge API web UI Security page \u2014 this command consumes a"," key, it does not create one (--email and --invite are the"," exceptions: they CREATE the project and its first admin key)."," 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(`
|
|
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(`
|
|
4759
4759
|
`)}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 (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No API key entered."}}return{ok:!1,error:"
|
|
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(`
|
|
4761
4761
|
${promptText}
|
|
4762
4762
|
`),output.write(`[x] = selected \xB7 [ ] = not selected
|
|
4763
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.
|
|
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.
|
|
4765
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.
|
|
4766
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}.
|
|
4767
|
-
`),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){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(`
|
|
4768
|
-
`):[
|
|
4769
|
-
`)}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 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)+`
|
|
4770
|
-
`,{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()}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.`}}
|
|
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(`
|
|
4769
|
+
`):[`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
|
+
`)}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
|
+
`,{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(`
|
|
4771
4772
|
`),BOOTSTRAP_INVITE_REJECTED_MESSAGE="The Bridge API rejected the bootstrap invite (HTTP 401). The invite is invalid, expired, or revoked \u2014 ask your Bridge API operator for a new one. (Your locally-stored secret was sent unchanged, so this is not a lost-secret problem.)";function buildDryRunPreview(plan){return plan.bootstrapInvite?buildBootstrapDryRunPreview(plan):[plan.attemptedServerResolution?"install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)":"install-bridge --dry-run (no writes, no network, no spawns)",`Repo name: ${plan.repoName}${plan.attemptedServerResolution?" (resolved server-side from your API key)":""}`,`Base URL (ping): ${plan.baseUrl}`,`Docs dir: ${plan.docsDir}`,`Agent: ${describePlannedLaunchAgent(plan.launch)}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",`Step 2 \u2014 connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,"Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned):",...plan.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),...plan.manualEditors.length>0?[` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[],`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY removed): ${plan.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,`Step 4 \u2014 persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,...buildLaunchStepPreview(plan)]}function describePlannedLaunchAgent(launch){return launch.kind==="spawn"?`${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) \u2014 opens only after Y/N consent`:launch.kind==="choose-one"?`choose one of ${launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ")} \u2014 the chosen tool opens only after Y/N consent`:launch.reason==="empty-selection"?"none \u2014 no tools selected":"none \u2014 no launchable tool selected; manual continuation is printed"}function buildLaunchStepPreview(plan){let githubLines=["Step 4b \u2014 optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state"," via the install manifest and, only when it is unconfigured and the terminal is"," interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts."];if(plan.launch.kind==="spawn")return[...githubLines,`Step 5 \u2014 agent session (${toolLabelForLaunchAgent(plan.launch.agent)}): on a TTY the wizard first asks`," 'Open a \u2026 session to do that now? (Y/n)'; only on consent is the full command below"," stored in a restricted launch script (mode 0600, under the system temp dir) and a short"," sourced runner spawned (the script itself is NOT written in --dry-run):",` ${plan.launch.spawnCommand}`];if(plan.launch.kind==="choose-one"){let labels=plan.launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ");return[...githubLines,`Step 5 \u2014 agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`," asks which single tool to open, then asks Y/N consent before spawning that one session."]}return[...githubLines,plan.launch.reason==="empty-selection"?"Step 5 \u2014 no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool.":"Step 5 \u2014 no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured)."]}function buildBootstrapDryRunPreview(plan){let pendingTarget=`bootstrap-pending:${plan.repoName}`,header=plan.selfServeSignup?"install-bridge --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)":"install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)",repoLine=plan.selfServeSignup?`Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`:`Repo name: ${plan.repoName} (created by the exchange; globally unique)`,selfServeStep=plan.selfServeSignup?["Step 2\xB7pre \u2014 self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace"," signup is requested, no mint call is made, and no email is sent or transmitted;"," a real run would request a fresh workspace for your email and receive an invite"," token, which then feeds the SAME redemption protocol below."]:[];return[header,repoLine,`Base URL: ${plan.baseUrl}`,`Docs dir: ${plan.docsDir}`,`Agent: ${describePlannedLaunchAgent(plan.launch)}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",...selfServeStep,`Step 2a \u2014 generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${plan.credentialStorePath}`," BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.","Step 2b \u2014 redeem the bootstrap invite (replaces the pre-flight ping \u2014 there is no key yet):",` POST ${plan.exchangeUrl}`,` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${plan.repoName}", "key_secret": "${REDACTED_API_KEY}"}`,`Step 2c \u2014 connectivity ping with the newly-minted key (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,"Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned):",...plan.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),...plan.manualEditors.length>0?[` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[],`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,`Step 4 \u2014 promote ${pendingTarget} \u2192 ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,...buildLaunchStepPreview(plan)]}async function detectManualEditors(deps){let exists=async p=>{try{return await deps.stat(p),!0}catch{return!1}},windsurf=await exists(path26.join(deps.cwd,".windsurf"))||await exists(path26.join(deps.cwd,".windsurfrules")),codex=await exists(path26.join(deps.homedir(),".codex"));return{windsurf,codex}}function manualEditorNames(editors){let names=[];return editors.windsurf&&names.push("Windsurf"),editors.codex&&names.push("Codex"),names}function buildManualHostInstructions(entry,editors){if(!editors.windsurf&&!editors.codex)return null;let redactedEnv={...entry.env,BAPI_API_KEY:REDACTED_API_KEY},lines=["","Detected an editor whose MCP config is global and cannot be written automatically.","Add the server manually (replace <REDACTED> with your key):"];if(editors.windsurf){let windsurfSnippet=JSON.stringify({mcpServers:{"bridge-api":{command:entry.command,args:entry.args,env:redactedEnv}}},null,2);lines.push(""," Windsurf \u2192 ~/.codeium/windsurf/mcp_config.json:",windsurfSnippet)}return editors.codex&&lines.push(""," Codex \u2192 ~/.codex/config.toml (add an [mcp_servers.bridge-api] table with the"," same command/args/env shown above, BAPI_API_KEY set to your key)."),lines.join(`
|
|
4772
|
-
`)}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage()),1;let options=parsed.options,branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="";if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;inviteToken=inviteResult.value}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;apiKey=keyResult.value}let baseUrl=deps.env.BAPI_BASE_URL??DEFAULT_BAPI_BASE_URL2,docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,attemptedServerResolution=!1;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{attemptedServerResolution=!0,log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(resolution.status==="resolved")repoName=resolution.repoName;else{let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let plan={repoName,baseUrl,docsDir,launch:planLaunch,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),...bootstrapInviteMode?{bootstrapInvite:!0,exchangeUrl:buildBootstrapExchangeUrl(baseUrl)}:{},...selfServeSignupMode?{selfServeSignup:!0}:{},...attemptedServerResolution?{attemptedServerResolution:!0}:{}};if(options.dryRun){for(let line of buildDryRunPreview(plan))log(line);return 0}if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection")),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen,finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;log("Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026"),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){if(selfServeSignupMode){log("Step 2/5 \u2014 requesting Bridge self-serve setup\u2026");let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?errorLog("Error: Self-serve setup is temporarily rate limited. Try again later."):mint.category==="invalid"?errorLog("Error: Self-serve setup could not be requested. Check the email value and try again."):errorLog("Error: Unable to complete self-serve setup. Check connectivity and retry."),1;inviteToken=mint.token}inviteFingerprint=fingerprintBootstrapInvite(inviteToken),log("Step 2/5 \u2014 redeeming the bootstrap invite\u2026");let prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!prepared.ok&&prepared.kind==="credential-conflict")if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0,prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:!0},credentialWriteDeps)}else return errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error} This invite has NOT been used, and re-running will not clear the conflict.`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;let keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused;log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`);let exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);for(;!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(!exchange.ok)return exchange.kind==="invalid-invite"?errorLog(reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE):errorLog(`Error: ${exchange.message}`),1;if(exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`),1;repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||log("Step 2/5 \u2014 verifying connectivity\u2026");let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey);if(!ping.ok)return errorLog(`Error: ${ping.message}`),1;log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir);log("Step 3/5 \u2014 writing per-host MCP config\u2026");let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let written=await writeHostConfigs(deps,targets,entry);for(let relPath of written)log(` wrote ${relPath}`);let globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry);for(let line of globalLogLines)log(line);let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors);manualInstructions&&log(manualInstructions),selectedPlatforms.includes("claude-code")&&selectedPlatforms.includes("copilot-cli")&&log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its global ~/.copilot/mcp-config.json \u2014 the two are configured separately."),selectedPlatforms.includes("claude-code")&&log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; restart or reload an already-running session for it to take effect.");try{await ensureGitignored(deps.cwd,".bridge/install-state.json",gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapInviteMode){log("Step 4/5 \u2014 promoting the bootstrap credential\u2026");let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return errorLog(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record \u2014 re-run install-bridge with the same bootstrap invite to finish (the redemption will replay and return the same key).`),1;log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{log("Step 4/5 \u2014 persisting routing credential\u2026");try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(` stored routing credential for ${result.target} at ${result.path}`):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`)}catch{log(" warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'.")}}if(await offerGithubConnection(repoName,deps,log),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok?(log(" launcher bucket warmed (the first MCP launch will not pay a cold install)."),log(` ${MCP_TIMEOUT_GUIDANCE}`)):errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){log(`Step 5/5 \u2014 opening a ${finalAgentName} session for /install-bridge configuration + concise capability report\u2026`);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});return spawnResult.ok?(log(""),log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log("NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards on the project's Get Started page (install status panel) or via the session's 'Applied N of M' summary."),0):(errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured")),0)}return log(buildManualInstallBridgeContinuation("configured")),0}return log(buildManualInstallBridgeContinuation("configured")),0}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path27 from"path";init_start_tickets();init_agent_registry();async function fetchLatestVersion(){try{let res=await fetch("https://registry.npmjs.org/@bridge_gpt/mcp-server/latest",{signal:AbortSignal.timeout(3e3)});if(res.ok)return(await res.json()).version||null}catch{return null}return null}async function runUpgradeCli(argv){let cwd=process.cwd(),isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes("--internal-reexec"),latestVersion=VERSION,fetched=await fetchLatestVersion();fetched&&(latestVersion=fetched);let isNewer=isNewerVersion(VERSION,latestVersion),npxCmd=process.platform==="win32"?"npx.cmd":"npx";if(isNewer&&!isInternalReexec&&!isDryRun)return console.log(`Update available: ${VERSION} -> ${latestVersion}. Re-executing from @latest...`),new Promise(resolve2=>{let child=spawn8(npxCmd,["-y","@bridge_gpt/mcp-server@latest","upgrade","--internal-reexec","--old-version",VERSION],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})});let oldVersionIdx=argv.indexOf("--old-version"),oldVersion=oldVersionIdx!==-1&&oldVersionIdx+1<argv.length?argv[oldVersionIdx+1]:VERSION,targetVersion=isDryRun&&isNewer?latestVersion:VERSION;if(isDryRun){let configTargets=[".mcp.json",".vscode/mcp.json",".cursor/mcp.json"];console.log(`
|
|
4773
|
+
`)}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage()),1;let options=parsed.options,branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="";if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;inviteToken=inviteResult.value}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;classifyEnteredCredential(keyResult.value)==="invite"?(inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1):apiKey=keyResult.value}let baseUrl=deps.env.BAPI_BASE_URL??DEFAULT_BAPI_BASE_URL2,docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,attemptedServerResolution=!1;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{attemptedServerResolution=!0,log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(resolution.status==="resolved")repoName=resolution.repoName;else{log("Couldn't auto-resolve your repo from the key; falling back to a guessed name \u2014 confirm it matches the setup UI.");let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let plan={repoName,baseUrl,docsDir,launch:planLaunch,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),...bootstrapInviteMode?{bootstrapInvite:!0,exchangeUrl:buildBootstrapExchangeUrl(baseUrl)}:{},...selfServeSignupMode?{selfServeSignup:!0}:{},...attemptedServerResolution?{attemptedServerResolution:!0}:{}};if(options.dryRun){for(let line of buildDryRunPreview(plan))log(line);return 0}if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen,finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;log("Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026"),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){if(selfServeSignupMode){log("Step 2/5 \u2014 requesting Bridge self-serve setup\u2026");let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?errorLog("Error: Self-serve setup is temporarily rate limited. Try again later."):mint.category==="invalid"?errorLog("Error: Self-serve setup could not be requested. Check the email value and try again."):errorLog("Error: Unable to complete self-serve setup. Check connectivity and retry."),1;inviteToken=mint.token}inviteFingerprint=fingerprintBootstrapInvite(inviteToken),log("Step 2/5 \u2014 redeeming the bootstrap invite\u2026");let prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!prepared.ok&&prepared.kind==="credential-conflict")if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0,prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:!0},credentialWriteDeps)}else return errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error} This invite has NOT been used, and re-running will not clear the conflict.`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;let keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused;log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`);let exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);for(;!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(!exchange.ok)return exchange.kind==="invalid-invite"?errorLog(reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE):errorLog(`Error: ${exchange.message}`),1;if(exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`),1;repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||log("Step 2/5 \u2014 verifying connectivity\u2026");let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey);if(!ping.ok)return errorLog(`Error: ${ping.message}`),1;log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir);log("Step 3/5 \u2014 writing per-host MCP config\u2026");let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let written=await writeHostConfigs(deps,targets,entry);for(let relPath of written)log(` wrote ${relPath}`);let globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry);for(let line of globalLogLines)log(line);let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors);manualInstructions&&log(manualInstructions),selectedPlatforms.includes("claude-code")&&selectedPlatforms.includes("copilot-cli")&&log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its global ~/.copilot/mcp-config.json \u2014 the two are configured separately."),selectedPlatforms.includes("claude-code")&&log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; restart or reload an already-running session for it to take effect.");try{await ensureGitignored(deps.cwd,".bridge/install-state.json",gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapInviteMode){log("Step 4/5 \u2014 promoting the bootstrap credential\u2026");let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return errorLog(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record \u2014 re-run install-bridge with the same bootstrap invite to finish (the redemption will replay and return the same key).`),1;log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{log("Step 4/5 \u2014 persisting routing credential\u2026");try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(` stored routing credential for ${result.target} at ${result.path}`):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`)}catch{log(" warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'.")}}if(await offerGithubConnection(repoName,deps,log),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok?(log(" launcher bucket warmed (the first MCP launch will not pay a cold install)."),log(` ${MCP_TIMEOUT_GUIDANCE}`)):errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){log(`Step 5/5 \u2014 opening a ${finalAgentName} session for /install-bridge configuration + concise capability report\u2026`);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});return spawnResult.ok?(log(""),log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log("NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards on the project's Get Started page (install status panel) or via the session's 'Applied N of M' summary."),0):(errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0)}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path27 from"path";init_start_tickets();init_agent_registry();async function fetchLatestVersion(){try{let res=await fetch("https://registry.npmjs.org/@bridge_gpt/mcp-server/latest",{signal:AbortSignal.timeout(3e3)});if(res.ok)return(await res.json()).version||null}catch{return null}return null}async function runUpgradeCli(argv){let cwd=process.cwd(),isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes("--internal-reexec"),latestVersion=VERSION,fetched=await fetchLatestVersion();fetched&&(latestVersion=fetched);let isNewer=isNewerVersion(VERSION,latestVersion),npxCmd=process.platform==="win32"?"npx.cmd":"npx";if(isNewer&&!isInternalReexec&&!isDryRun)return console.log(`Update available: ${VERSION} -> ${latestVersion}. Re-executing from @latest...`),new Promise(resolve2=>{let child=spawn8(npxCmd,["-y","@bridge_gpt/mcp-server@latest","upgrade","--internal-reexec","--old-version",VERSION],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})});let oldVersionIdx=argv.indexOf("--old-version"),oldVersion=oldVersionIdx!==-1&&oldVersionIdx+1<argv.length?argv[oldVersionIdx+1]:VERSION,targetVersion=isDryRun&&isNewer?latestVersion:VERSION;if(isDryRun){let configTargets=[".mcp.json",".vscode/mcp.json",".cursor/mcp.json"];console.log(`
|
|
4773
4774
|
[Dry Run] Upgrade preview:`),isNewer&&!isInternalReexec&&console.log(`- Would re-exec from @latest to upgrade ${VERSION} -> ${latestVersion}`),console.log("- Would detect and remove competing local install in node_modules/@bridge_gpt/mcp-server if present."),console.log(`- Would rewrite launcher pin to @bridge_gpt/mcp-server@${targetVersion} across active per-host configs:`);for(let target of configTargets)try{await stat9(path27.join(cwd,target)),console.log(` - ${target}`)}catch{}console.log("- Would refresh scaffolded artifacts (commands, agents, pipelines).");let spawnCommand=buildGenericAgentShellCommand(AGENT_REGISTRY.claude,"Bridge API MCP server has been upgraded. Please reload/reconnect the MCP server in your environment.",cwd,process.platform);return console.log(`- Would print: ${oldVersion} -> ${targetVersion} and spawn a fresh reconnect session with command:
|
|
4774
4775
|
${spawnCommand}`),0}try{let localModulePath=path27.join(cwd,"node_modules","@bridge_gpt","mcp-server");await stat9(localModulePath),console.log("Found stale local installation in node_modules. Removing to converge on pinned-npx...");let npmCmd=process.platform==="win32"?"npm.cmd":"npm";await new Promise(resolve2=>{let child=spawn8(npmCmd,["uninstall","@bridge_gpt/mcp-server"],{stdio:"inherit",cwd});child.on("close",code=>{code!==0&&console.warn("Warning: npm uninstall failed; you may need to remove node_modules/@bridge_gpt/mcp-server manually."),resolve2()}),child.on("error",()=>resolve2())})}catch{}try{console.log(`
|
|
4775
4776
|
Upgrading @bridge_gpt/mcp-server to ${targetVersion}...
|