@bridge_gpt/mcp-server 0.2.27 → 0.2.28
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 +32 -24
- package/build/commands.generated.js +2 -2
- package/build/conductor/bridge-api-client.js +98 -1
- package/build/conductor/epic-reconcile.js +28 -0
- package/build/conductor/epic-runtime.js +28 -1
- package/build/conductor-bin.js +1 -1
- package/build/connect-github-api.js +9 -0
- package/build/connect-github.js +10 -0
- package/build/doctor.js +2 -2
- package/build/env-flags.js +23 -0
- package/build/index.js +75 -29
- package/build/install-bridge.js +444 -162
- package/build/mcp-host-targets.js +12 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +198 -4
- package/build/start-tickets.js +25 -4
- package/build/tool-surface-gating.js +13 -2
- package/build/version.generated.js +1 -1
- package/package.json +3 -3
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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.28"}});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)}
|
|
@@ -104,11 +104,11 @@ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyN
|
|
|
104
104
|
WHERE run_id = @run_id AND worker_id = @worker_id AND state = 'pending'
|
|
105
105
|
AND julianday(available_at) <= julianday('now')
|
|
106
106
|
ORDER BY seq ASC
|
|
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,getEpicPlan:()=>getEpicPlan,mergePullRequestForGate:()=>mergePullRequestForGate,pollCiChecksForCommit:()=>pollCiChecksForCommit,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)}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)}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{return await fetchConductorJsonPostWithTimeout(url,conductorPostHeaders(access2),body,CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)}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,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"}});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(`
|
|
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
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(`
|
|
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
|
-
`)}async function spawnMacOSTerminalTab(deps,terminal,shellCommand,context){let title=terminalTitleForTicket(context?.key??""),script=terminal==="iterm"?buildITermAppleScript(shellCommand,title,
|
|
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
|
|
113
113
|
${fullCommand}
|
|
114
114
|
`}function buildLaunchScriptRunnerCommand(platform,scriptPath){return platform==="win32"?`. ${powershellSquote(scriptPath)}`:`. '${shSquoteInner(scriptPath)}'`}async function pruneStaleLaunchScripts(deps=defaultPruneStaleLaunchScriptsDeps){try{let parent=path15.join(os4.tmpdir(),"bridge-start-tickets"),entries;try{entries=await deps.readdir(parent)}catch{return}let cutoff=deps.now()-STALE_LAUNCH_SCRIPT_MAX_AGE_MS;for(let entry of entries){if(!entry.startsWith("w-"))continue;let full=path15.join(parent,entry);try{(await deps.stat(full)).mtimeMs<cutoff&&await deps.rm(full,{recursive:!0,force:!0})}catch{}}}catch{}}async function materializeWorkerLaunchCommand(deps,key,fullCommand){if(!deps.writeWorkerLaunchScript)return{ok:!0,command:fullCommand};try{let scriptPath=await deps.writeWorkerLaunchScript({platform:deps.platform,key,content:buildLaunchScriptContent(deps.platform,fullCommand)});return{ok:!0,command:buildLaunchScriptRunnerCommand(deps.platform,scriptPath)}}catch{return Buffer.byteLength(fullCommand,"utf8")<=MAX_TERMINAL_COMMAND_BYTES?{ok:!0,command:fullCommand}:{ok:!1,reason:"launch-script-write-failed-oversized-command",error:"Could not write the temporary launch script, and the full command is too long to send to the terminal directly. Check that the system temporary directory is writable."}}}async function spawnTabsForCreatedWorktrees(deps,rows,terminal,buildShellCommand){let out=[];for(let row of rows){if(row.status!=="created"||!row.path){out.push(row);continue}let baseShellCommand=buildShellCommand(row.key,row.path,row.modelAlias??null),shellCommand=injectConductorEnvIntoShellCommand(deps.platform,baseShellCommand,row.conductorEnv),materialized=await materializeWorkerLaunchCommand(deps,row.key,shellCommand);if(!materialized.ok){out.push({...row,status:"spawn-failed",error:materialized.error});continue}let result=await deps.spawnTerminalTab(deps,terminal,materialized.command,{key:row.key,worktreePath:row.path});result.ok?out.push({...row,status:"spawned"}):out.push({...row,status:"spawn-failed",error:result.error})}return out}function buildDryRunResults(keys,overrides){return keys.map(key=>({key,branch:resolveBranchForTicket(key,overrides),status:"dry-run"}))}function getDryRunPlatformDetails(agent,platform=process.platform,env=process.env,autoApprove=!1,conductorEnabled=!1,repoName=null,workflow="implement",reviewRounds,baseBranch){return{worktrunkBinary:resolveWorktrunkBinary(platform,env),buildAgentShellCommand:(key,worktreePath,modelAlias)=>prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,!1,workflow,reviewRounds,baseBranch),repoName,platform)}}function buildDryRunMcpProvisioningLines(worktreePath,platform=process.platform,mcpServerInvocation){let api=platform==="win32"?path15.win32:path15.posix,mcpJson=api.join(worktreePath,".mcp.json"),cursorJson=api.join(worktreePath,".cursor","mcp.json"),built=buildMcpShimCommand(mcpServerInvocation??{form:"npm-channel",command:"npx",packageSpec:"@bridge_gpt/mcp-server@latest"},"<target>",worktreePath),shim=`${built.command} ${built.args.join(" ")}`;return["DRY-RUN: MCP provisioning (target-driven from .bridge/config \u2014 bapi plus any","DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim","DRY-RUN: entry per target to",`DRY-RUN: ${mcpJson}`,`DRY-RUN: ${cursorJson}`,`DRY-RUN: ${shim}`]}function buildDryRunDetailLines(agent,key,branch,platform=process.platform,env=process.env,baseBranch="main",autoApprove=!1,modelAlias=null,conductorEnabled=!1,repoName=null,mcpServerInvocation,workflow="implement",reviewRounds){let{worktrunkBinary,buildAgentShellCommand:build}=getDryRunPlatformDetails(agent,platform,env,autoApprove,conductorEnabled,repoName,workflow,reviewRounds,baseBranch),wtArgs=buildWtSwitchArgs(branch,!1,baseBranch),agentInvocation=build(key,"<worktree-path>",modelAlias);return[`DRY-RUN: ${key} -> branch=${branch}`,`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,`DRY-RUN: ${agentInvocation}`,...buildDryRunMcpProvisioningLines("<worktree-path>",platform,mcpServerInvocation)]}function formatSummaryReport(rows){let lines=["Summary:"],runId=rows.find(r=>r.runId)?.runId;runId&&lines.push(`run_id=${runId}`);let supervisorStatus=rows.find(r=>r.supervisorStatus)?.supervisorStatus;supervisorStatus&&lines.push(`supervisor=${supervisorStatus}`);for(let row of rows){let line=`${row.key} branch=${row.branch} status=${row.status}`;row.path&&(line+=` path=${row.path}`),row.workerId&&(line+=` worker_id=${row.workerId}`),row.mcpRegistrationForm&&(line+=` mcp_registration=${row.mcpRegistrationForm}`),lines.push(line)}let warningLines=[];for(let row of rows){let messages=[];(row.status==="create-failed"||row.status==="spawn-failed")&&messages.push(row.error??row.status);for(let warning of row.warnings??[])messages.push(warning);let seen=new Set;for(let message of messages)seen.has(message)||(seen.add(message),warningLines.push(` ${row.key}: ${message}`))}return warningLines.length>0&&(lines.push(""),lines.push("Warnings:"),lines.push(...warningLines)),lines.join(`
|
|
@@ -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 the capability report\n (Connected / Not yet connected / Tools you can use now / Tools you\'ll unlock /\n Recommended next step), and closes with one optional `[Y/n] Index repository\n now?` question. It does not chain `/learn-repository` and never indexes without\n consent; run `/learn-repository` and `/parse-repository` yourself when you want\n them.\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**2b. 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**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 brainstorming 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 brainstorm before the repository is indexed.\n- **How to use it:** ask your agent to brainstorm \u2014 *"Brainstorm approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design brainstorm for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery brainstorm \u2014 `request_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 brainstorm \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### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Commit staged changes and open a pull request |\n| `/check-ci [PROJ-123]` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, brainstorms, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\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-1--regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--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| `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. It then re-probes\non a jittered 12\u201318 s poll and emits `notifications/tools/list_changed` whenever\nthe effective visible set actually changes, so a connected client converges to\nthe current surface mid-session.\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? [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:
|
|
2365
2365
|
${errors.join(`
|
|
2366
2366
|
`)}`);continue}let pipeline=parsed,key=file.replace(/\.json$/,""),hasInvalidRef=!1;for(let step of pipeline.steps)if(step.type==="agent_task"&&step.instruction_file){let content=mergedInstructions[step.instruction_file];if(content===void 0){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" not found.`),hasInvalidRef=!0;break}if(!hasTerminalReturnSection(content)){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" is missing a terminal "## Return" section (required by BAPI-275 agent_result contract).`),hasInvalidRef=!0;break}}hasInvalidRef||(userPipelines[key]=pipeline,userPipelineKeys2.add(key))}}catch(err){return err.code!=="ENOENT"&&console.error(`Warning: could not read pipelines directory "${pipelinesDir}": ${err.message}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}return userPipelineKeys2.size>0&&console.error(`Loaded ${userPipelineKeys2.size} user pipeline(s) from ${pipelinesDir}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}import{writeFile as writeFile2,mkdir as mkdir2,readFile as readFile3,stat}from"fs/promises";import path5 from"path";var COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
|
|
2367
2367
|
|
|
@@ -3292,7 +3292,7 @@ After the final pipeline step completes, cleanly end your worker session (for ex
|
|
|
3292
3292
|
- you have unpushed local commits.
|
|
3293
3293
|
|
|
3294
3294
|
Exit only after your final branch state is pushed, the done-gate / CI-monitoring workflow required by the recipe has completed, and no CI/review follow-up remains. A clean \`SessionEnd\` is both the correct terminal lifecycle signal and the point at which the worker should exit.
|
|
3295
|
-
`,"install-bridge.md":'Bootstrap Bridge API project configuration from the local codebase via the easy-install manifest.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes no arguments; any `$ARGUMENTS` text is ignored.\n\nCommand contract version: **4**. The install manifest returns a `command_contract_version`; Stage 2\ncompares it to this number to detect a stale scaffolded copy of this command.\n\nThis command performs a one-time "easy install" of Bridge API project configuration. It reads the\nserver-owned install manifest once, derives values for the **unset** bootstrap fields from the local\ncodebase, asks for human approval of the project description, applies everything in a single atomic\ncall, and then presents a **capability report** derived from a fresh read-after-write manifest read.\nThe server owns all skip-if-set, conflict, and confirmation semantics \u2014 this command never makes its\nown skip-if-set decisions \u2014 and the server owns the complete tool catalog, its grouping and ordering,\nand every gate and dependency relationship; this command formats the server\'s contract and never\nrecomputes it from prose.\n\nThis command is portable across Claude Code, Cursor, and Copilot: it only drives the Bridge API MCP\ntools (`get_my_role`, `get_install_manifest`, `apply_install_manifest`).\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n**Install-spawn context.** When the spawning instruction explicitly says this `/install-bridge` run is\nin the "install-spawn context" (it was launched by the `install-bridge` CLI\'s fresh agent session),\nStage 8, Stage 9, and Stage 10 are SKIPPED and the single closing interaction is the index-consent\nquestion the spawn prompt owns. When you invoke `/install-bridge` directly (manual invocation),\nStages 8, 9, and 10 run normally.\n\n## Stage 1 \u2014 Admin preflight\n\n1. Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `source` is `"legacy"`: proceed (legacy keys are permitted).\n - Else if `role` is `"admin"`: proceed.\n - Otherwise (a non-admin `user_access` key): stop immediately and display:\n ```\n Admin role required to apply install configuration. Ask a project admin to run /install-bridge,\n or use an admin API key.\n ```\n\n## Stage 2 \u2014 Read the manifest (once, pre-apply)\n\n1. Call the `get_install_manifest` MCP tool exactly once here.\n2. Keep the returned `snapshot_token` verbatim \u2014 you must pass the SAME token to the apply call in\n Stage 5. Do not call `get_install_manifest` again before applying; a fresh read would invalidate\n the token you are about to use. (Stage 7 performs a SEPARATE, later read-after-write manifest read\n for the current capability status \u2014 that is deliberate and does not reuse this token.)\n3. The manifest contains ordered `groups` of bootstrap fields (each with `field_name`, `is_set`,\n `current_value`, `guidance`, `examples`, `validation_summary`, `requires_confirmation`), a list of\n `deferred_fields`, a `next_step`, `done_criteria`, a `command_contract_version`, and the\n `snapshot_token`. It also carries the additive capability fields (`integrations`, `configured`,\n `learned`, `indexed`, `tool_capabilities`, `locked_tools`, `unlocked_tools`) \u2014 but ignore those\n here; the accurate capability status is the post-apply read in Stage 7. `tool_capabilities` is the\n COMPLETE catalog-backed report field (one entry per registered MCP tool, grouped and ordered by the\n server); `locked_tools` / `unlocked_tools` are LEGACY compatibility data covering only the VCS/index\n policy cases and are NOT the tool inventory.\n4. Compare the manifest\'s `command_contract_version` to this command\'s contract version (4, stated at\n the top of this file). If the manifest\'s version is HIGHER, this scaffolded command copy is stale:\n warn the user to refresh it by running `npx -y @bridge_gpt/mcp-server --init` in the project root,\n then proceed conservatively \u2014 wherever the manifest\'s `guidance`, `next_step`, or `done_criteria`\n differ from these instructions, follow the manifest.\n\n## Stage 3 \u2014 Derive values for UNSET bootstrap fields only\n\n1. Consider ONLY fields whose `is_set` is `false`. Never re-derive or overwrite a field that is\n already set \u2014 the server will also skip already-set fields, but you should not even propose them.\n2. For each unset field, derive a value from the local codebase following that field\'s `guidance`\n (e.g. infer `working_in` from frameworks/manifests, `version_control_system` from the git remote,\n testing stacks from test config and dependencies, directory fields from the repo layout).\n3. Omit any field you are uncertain about or that does not apply \u2014 leaving it unset is always\n acceptable. Do NOT guess.\n4. Specifically, do NOT invent a `version` when the platform has no meaningful version concept; leave\n it unset.\n5. Before deriving `version_control_system` or `base_branch`, confirm the git repository root is THIS\n project\'s root: `git rev-parse --show-toplevel` must resolve to the project directory itself. If\n the git root is an ancestor directory (the project is nested inside another repository), OMIT both\n fields and note why in the final report \u2014 deriving them would describe the parent repository, not\n this project.\n6. If you find real evidence for TWO OR MORE plausible values for a field (e.g. configs for multiple\n test frameworks, multiple frameworks in a monorepo), treat the value as ambiguous and omit it\n rather than picking one.\n7. Do NOT derive `selected_mcp_slugs`, even though it appears as an unset bootstrap-eligible field in\n the manifest. MCP validation manual selection is deferred to `/learn-repository`, which derives and\n confirms it with the codebase already researched. Install neither proposes nor applies this field.\n\n## Stage 4 \u2014 Human approval for confirmation-requiring fields\n\n`project_description` is the ONLY confirmation-requiring field install proposes. It carries\n`requires_confirmation: true` in the manifest, so it is never applied on derivation alone \u2014 it needs\nexplicit human approval. (`selected_mcp_slugs` also requires confirmation, but install does not\nderive it at all; `/learn-repository` asks for it. See Stage 3 step 7.)\n\n1. If `project_description` is unset and you can draft a concise description from the codebase,\n present the proposed `project_description` text to the human and ask for explicit approval before\n including it.\n2. Include `project_description` in the apply payload ONLY as\n `{ "value": <approved value>, "confirmed": true }`, and only after the human approves it. If the\n human does not approve it, omit the field entirely.\n3. If no human response can be obtained (e.g. a non-interactive / headless session), do NOT stall and\n do NOT abandon the install: proceed to Stage 5 with `project_description` omitted, and report it\n as "pending human input" in the final summary. The other derived fields must still be applied \u2014 an\n unapproved description never blocks them.\n\n## Stage 5 \u2014 Apply (one call)\n\n1. Make exactly ONE call to the `apply_install_manifest` MCP tool, passing:\n - `snapshot_token`: the exact token from Stage 2.\n - `fields`: a map of the derived fields. Scalar fields may be passed directly\n (e.g. `"base_branch": "main"`); an approved `project_description` must use the\n `{ "value": ..., "confirmed": true }` object form from Stage 4. Never include\n `selected_mcp_slugs` in this payload \u2014 install does not derive or apply it (Stage 3 step 7).\n2. Do not implement any client-side skip-if-set, conflict, or confirmation logic \u2014 the server owns\n all of that and returns the outcome buckets.\n3. The apply is partial-tolerant: fields that fail server-side validation are returned in the\n `rejected` bucket while the valid fields still commit. A rejected field is therefore reported,\n not fatal \u2014 do NOT retry the whole apply just because one field was rejected.\n\n## Stage 6 \u2014 Persist the routing credential\n\nThe Bash-spawned `start-tickets` CLI runs in a DIFFERENT runtime surface than this MCP server, so a\nkey that lives only in `.mcp.json` / `.cursor/mcp.json` is invisible to it and difficulty\u2192model\nrouting silently degrades. This final stage persists the already-validated key into the user-scoped\ncredential store (`~/.config/bridge/credentials.json`) so shell-spawned CLI commands can resolve it.\n\n1. Call the `persist_routing_credential` MCP tool with `repo_name` set to the repository name for\n this install (the configured `BAPI_REPO_NAME`). Pass `repo_name` ONLY \u2014 this tool resolves the\n API key inside the MCP server process. NEVER pass or display `BAPI_API_KEY` (nor `api_key`,\n `apiKey`, `secret`, or `token`); the key value must not appear in the tool call or in any output.\n2. On success, tell the user the credential was stored, echoing the returned `target` and `path`,\n e.g. `Stored routing credential for bapi:<repo_name> at <path>`.\n3. On failure, do NOT block the install \u2014 show the rest of the install summary anyway, then tell the\n user the routing credential could not be stored. They can rerun `/install-bridge` after setting\n `BAPI_API_KEY` in the environment, or run\n `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate an\n existing key. Until then, `start-tickets` model routing fails open (assumes a hard ticket and\n defaults to premium/Opus when available) \u2014 this is the MOST EXPENSIVE model tier, so unexplained\n cost is the symptom of leaving this unfixed. Recommend verifying credential resolution afterwards\n with `npx -y @bridge_gpt/mcp-server doctor`.\n\n## Stage 7 \u2014 Summarize the outcome, then present the capability report\n\nFirst, begin with an explicit applied count: "Applied N of M derivable fields" (M = the unset\nbootstrap fields you derived values for). An install that applied 0 fields must say so loudly \u2014 the\ninstall is NOT complete until the apply call reports applied fields.\n\nThen summarize the six buckets from the apply response for the user:\n- `applied` \u2014 fields written this run.\n- `skipped` \u2014 fields already set (left untouched).\n- `conflict` \u2014 fields that changed since the manifest was read (re-run /install-bridge to retry).\n- `needs_confirmation` \u2014 fields awaiting explicit confirmation (e.g. project_description).\n- `rejected` \u2014 fields that failed validation or were not bootstrap-eligible (the other fields still\n applied; fix or drop the rejected values before any retry).\n- `deferred` \u2014 fields intentionally not auto-written by install (owned by /learn-repository or set\n deliberately). `selected_mcp_slugs` belongs here: report it as deferred to `/learn-repository`,\n never as approved, declined, or pending an install-time decision.\n\n### Read-after-write: fetch current capability status\n\nAfter the bucket summary, call the `get_install_manifest` MCP tool ONCE MORE. This post-apply read\nreflects the configuration you just wrote, so its capability fields are current (the Stage-2 read was\npre-apply and is stale for this purpose). This read does not need the snapshot token. Use ONLY this\npost-write response for the capability report below.\n\nThe response carries:\n\n- the `integrations` checklist \u2014 each item has `label`, `is_configured`, `required_for`,\n `configure_in`, and MAY have `optional_components` (a list of optional, non-gating add-ons nested\n under that integration, each with `id`, `label`, `is_configured`, `required_for`, `configure_in`,\n and `gating: false`);\n- the separate `configured` / `learned` / `indexed` readiness values;\n- `tool_capabilities` \u2014 the COMPLETE catalog-backed collection. It is an ordered array of groups, each\n `{id, name, description, tools}`. Each tool is `{tool, display_name, description, group, profile,\n availability, availability_text, effect, missing, semantics, variants}`, one entry per registered MCP\n tool, keyed by physical tool id;\n- `locked_tools` / `unlocked_tools` \u2014 LEGACY compatibility arrays covering only the VCS/index policy\n cases, keyed by policy-case id. They are NOT the tool inventory and you do not render them.\n\nServer authority: the server owns catalog membership, grouping, ordering, gates, and dependency\nrelationships. Never recompute any of them, and never derive them from `docs/mcp-tool-integrations.md`\nor any other documentation \u2014 cite that file only for a human explanation of a gate\'s "why".\n\nIf the post-write response has no capability fields at all (no `integrations` / `tool_capabilities`\nkeys, e.g. the additive enrichment was omitted), print exactly:\n`No capability status is available yet; configuration can still continue.` and skip the five sections.\n\nOtherwise, open with a short plain-language framing before the first section: explain that Bridge\nconnects the user\'s systems (their tracker, their code host, their platform) to their local code\neditor, and that connecting more of those systems unlocks richer capabilities. Then render exactly\nthese five sections, in this order, with these exact headings:\n\n**Connected \u2713**\n\n- List each configured integration\'s `label` from the post-write `integrations` checklist, with a\n restrained `\u2713` marker. (Do not let the `\u2713` markers dominate the report.)\n- If a listed integration has `optional_components`, list each component beneath its parent as a\n sub-item, and describe it as optional and not required for the parent to work. A component with\n `gating: false` NEVER blocks anything: it is not a separate integration, it does not belong in the\n top-level list, and its state must not change how you describe its parent.\n\n**Not yet connected \u2717**\n\n- List each unconfigured integration: its `label`, its `required_for` items, and the exact\n `configure_in` pointer. Do NOT include `setup_instructions` content \u2014 the pointer is the only\n configuration direction you emit. Apply the same `optional_components` rule as above.\n- The pointer is per-integration and is NOT always the setup UI. Emit whatever the server sent,\n VERBATIM: `github_app` points at the terminal command `npx -y @bridge_gpt/mcp-server@latest\n connect-github --repo <repo_name>`; `sfcc` points at the guide `docs/install/sfcc-integration.md`;\n Jira and `vcs_access_token` point at the setup UI. Never rewrite a command pointer into "the setup\n UI", and never replace GitHub\'s command with a guide.\n- STRICT INVARIANT: you DIRECT the human to that pointer; you never ask for, accept, echo, or\n transport an integration credential (Jira, GitHub, VCS, webhook, or SFCC \u2014 API token, access token,\n access key, webhook secret) in any form. This is unchanged for GitHub: the connect-github command\n authenticates the human to GitHub in their own browser, and no GitHub credential ever reaches Bridge\n or an agent. The SFCC pointer is a guide the human follows themselves \u2014 it is never an invitation to\n hand you credentials.\n\n**Tools you can use now**\n\n- Render from `tool_capabilities`, using the server\'s group order and, within each group, the server\'s\n tool order. Show each group\'s `name`, then each tool\'s `display_name`, its one-sentence\n `description`, and its `availability_text` exactly as provided.\n- Include here every tool whose `availability` is `available_now`, `available_with_less_context`,\n `profile_required`, or `varies_by_variant`.\n- Describe an `available_with_less_context` tool as available now with less codebase context \u2014 never\n as failed or unavailable.\n- For a `profile_required` tool, emit the server\'s neutral profile wording as given. Do NOT claim the\n profile is or is not registered locally: the server cannot observe that, so neither can you.\n- A `varies_by_variant` tool belongs here because at least one of its options is usable now. Render\n its variants beneath it and rely on each variant\'s own `availability_text` for accuracy \u2014 do NOT\n describe the whole tool as blocked, and do NOT describe it as fully available. (This is a common\n state, not a corner case: `create_doc` reports it whenever VCS is connected but the repository is\n not yet indexed, since `tdd` needs the index while `fsd`/`prd` do not.)\n- If a tool has a non-empty `variants` array, render the variants as sub-items BENEATH that one tool,\n each with its `label` and its own `availability_text`. Variants are options of a single tool\n (`create_doc`\'s tdd/fsd/prd, `request_council`\'s modes) \u2014 never present them as separately\n registered tools, and never invent an id like `create_doc:tdd`.\n- NEVER emit `BLOCK` or `DEGRADE`. The `effect` field is internal metadata that remains in the\n payload for compatibility; it is not for display. `availability_text` is what a human reads.\n\n**Tools you\'ll unlock**\n\n- Render every tool whose `availability` is `available_after_dependencies`, in the same server-provided\n grouping and order, each with its `display_name`, `description`, and `availability_text`. Where a\n tool\'s `variants` differ, show the variants beneath it so the user can see which options are already\n usable.\n- The `availability_text` already names the connection needed in plain language, including "or"\n relationships (an unknown VCS provider yields "the GitHub App or a VCS access token") and the\n separately-required repository index. Emit it as given rather than re-deriving it from `missing` or\n `semantics`.\n- Cite `docs/mcp-tool-integrations.md` briefly for a gate\'s human "why" \u2014 but never recompute\n membership from it.\n\n**Recommended next step + why**\n\n- Make this section visually strongest through ordering and concise wording. Choose the single most\n valuable next action using this deterministic priority based only on the server output. Jira or SFCC\n never displaces this order unless the server itself reports it as a dependency:\n 1. If any tool\'s `missing` includes a VCS integration (`github_app` / `vcs_access_token`), recommend\n connecting VCS first, using that integration\'s own `configure_in` pointer verbatim (for\n `github_app` that is the `connect-github` terminal command, not the setup UI).\n 2. Else if any tool\'s `missing` includes `code_index`, recommend running repository indexing\n (`/parse-repository`) next.\n 3. Else if `learned` is false, recommend running `/learn-repository` to populate the deeper\n instruction-tier configuration.\n- If `indexed` is `null` (unknown), include this exact warning:\n `Index status could not be confirmed\u2014check again before relying on codebase-grounded tools.`\n\n## Stage 8 \u2014 Offer the next steps\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it); the\nspawn prompt owns the single closing index-consent question there. On direct manual `/install-bridge`\ninvocation, run it normally:\n\n1. Offer to continue with `/learn-repository` to populate the deeper instruction-tier configuration\n (architecture, review, testing, and correctness standards), matching the manifest\'s `next_step`.\n Mention that it also derives and confirms the applicable MCP validation manuals\n (`selected_mcp_slugs`) \u2014 it researches the codebase first, so it can propose them with evidence\n and explain what they do, which is why install leaves that field alone.\n2. After `/learn-repository` completes (or if the human declines it), offer to start **repository\n indexing**: ask ONE consent question ("Start repository indexing now? It runs as a background job\n and makes the codebase searchable for Bridge API\'s agents."). On yes, call the `parse_repository`\n MCP tool once. Report that the job was QUEUED and that progress can be checked with\n `get_parse_status` (or `/check-parse-status`) \u2014 do NOT poll it to completion in this session.\n Indexing is deliberately last so the applied `exclude_directories` / `custom_directories` values\n scope it correctly. If no human response can be obtained (non-interactive session), skip indexing\n and list it as a pending next step in the final report \u2014 never start it without consent.\n\n## Stage 9 \u2014 Offer CI follow-up configuration (only when CI is detected)\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe index-consent question the spawn prompt owns remains the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the\n`config_field` MCP tool. `ci_followup_config` is REPLACE-NOT-AUGMENT \u2014 leaving it NULL already means\nsafe poll-only defaults, so "skip" is always a correct answer.\n\n1. Gate: only offer this when Stage 3 found real CI evidence (e.g. `.github/workflows/*.yml`,\n `bitbucket-pipelines.yml`). With no CI detected, skip this stage and note "no CI detected \u2014 CI\n follow-up not offered" in the final report.\n2. Ask ONE question, referencing the discovered CI by name, with exactly these preset profiles\n (default: skip):\n - **observe** \u2014 poll CI results only, never attempt fixes:\n `{"strategy": "poll_only", "max_iterations": 1, "max_minutes": 10, "instructions": ""}`\n - **self-heal** \u2014 bounded fix-and-iterate loop on the automation\'s own PRs:\n `{"strategy": "fix_and_iterate", "max_iterations": 3, "max_minutes": 45, "instructions": ""}`\n - **skip** (default) \u2014 leave `ci_followup_config` unset (NULL = baseline poll-only behavior).\n3. Do NOT offer or compose a `custom` strategy during install \u2014 free-form CI follow-up instructions\n are a deliberate later act (setup UI or a direct `config_field` update).\n4. When the human picks a profile, call the `config_field` MCP tool once (operation `"update"`,\n `field_name: "ci_followup_config"`, `value`: the profile\'s JSON object). If the server rejects the\n value, retry ONCE with a corrected payload; if it is rejected again, stop, show the proposed JSON\n to the human, and leave the field unset.\n5. Never select a profile in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never enable fix-and-iterate behavior.\n\n## Stage 10 \u2014 Offer the speed-vs-quality repository preference\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe index-consent question the spawn prompt owns remains the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the `config_field`\nMCP tool, `field_name: "speed_vs_quality"`. The column defaults to `5` (max quality) for every\nrepository, so "skip" always leaves a safe, valid value in place.\n\n1. Never ask this question in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never block on or silently answer this preference.\n2. Read the current value first via the `config_field` MCP tool (operation `"get"`,\n `field_name: "speed_vs_quality"`) so a reinstall can show the stored value \u2014 not always `5` \u2014 as\n the displayed default rather than silently re-asking from scratch.\n3. Ask ONE question with exactly these five numbered presets (default: the value from step 2, or `5`\n if this is the first install):\n - **1** \u2014 Max speed\n - **2** \u2014 Prefer speed\n - **3** \u2014 Balanced\n - **4** \u2014 Prefer quality\n - **5** \u2014 Max quality (default)\n4. Persist ONLY on an explicit answer \u2014 including an explicitly accepted default \u2014 by calling the\n `config_field` MCP tool once (operation `"update"`, `field_name: "speed_vs_quality"`,\n `value`: the selected integer 1-5). Do not overwrite an existing value when the human gives no\n answer at all (e.g. the session cannot obtain one) \u2014 leave the stored/default value untouched in\n that case, distinct from an explicit accepted-default selection of `5`.\n5. After a successful persist, emit one additive structured log line (mirroring the codebase\'s\n `logging.info(msg, extra={...})` convention for non-response-body observability signals) with\n fields: `event="install.speed_vs_quality"`, `repo_name`, `field_name="speed_vs_quality"`,\n `selected_preset` (the persisted integer), `outcome="persisted"`. Never log this event before the\n `config_field` tool call has confirmed the write.\n6. When skipped, emit the corresponding structured event with `outcome` set to one of\n `"skipped_non_interactive"` (non-interactive session) or `"skipped_no_answer"` (interactive session,\n no explicit answer obtained) \u2014 omit `selected_preset` and any prompt text from this event.\n7. If the `config_field` call is rejected, retry ONCE with a corrected payload; if it is rejected\n again, stop, show the proposed value to the human, and leave the field at its current stored value.\n Do not log a persistence-success event for a rejected or failed write.\n\n## Return\n\nReport the admin check result, the "Applied N of M" count, the\napplied/skipped/conflict/needs_confirmation/rejected field names, any fields omitted for\nnested-repository or ambiguity reasons, the approval outcome for `project_description` \u2014 install\'s\nonly confirmation-requiring field \u2014 (approved / declined / pending human input), the fact that\n`selected_mcp_slugs` is deferred to `/learn-repository` rather than decided here, whether a\nstale-command warning was raised (manifest\n`command_contract_version` higher than this command\'s), whether the routing credential was persisted\n(the returned `target` and `path`, or the non-blocking failure remediation), the five-section\ncapability report (Connected \u2713 / Not yet connected \u2717 / Tools you can use now / Tools you\'ll unlock /\nRecommended next step + why) from the post-apply read-after-write manifest read, the CI follow-up\noutcome (profile written / skipped / no CI detected / pending / skipped in install-spawn context), the\nspeed-vs-quality preference outcome (persisted with its preset / skipped_non_interactive /\nskipped_no_answer / pending / skipped in install-spawn context), and the recommended next step.\n',"learn-repository.md":`Learn and document all configuration fields for the repository by running parallel research agents.
|
|
3295
|
+
`,"install-bridge.md":'Bootstrap Bridge API project configuration from the local codebase via the easy-install manifest.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes no arguments; any `$ARGUMENTS` text is ignored.\n\nCommand contract version: **5**. The install manifest returns a `command_contract_version`; Stage 2\ncompares it to this number to detect a stale scaffolded copy of this command.\n\nThis command performs a one-time "easy install" of Bridge API project configuration. It reads the\nserver-owned install manifest once, derives values for the **unset** bootstrap fields from the local\ncodebase, asks for human approval of the project description, applies everything in a single atomic\ncall, and then presents a **concise capability report** derived from a fresh read-after-write manifest\nread. The server owns all skip-if-set, conflict, and confirmation semantics \u2014 this command never makes\nits own skip-if-set decisions \u2014 and the server owns the complete tool catalog and the bounded concise\nprojection over it, their grouping and ordering, and every gate and dependency relationship; this\ncommand formats the server\'s contract and never recomputes it from prose. Indexing is never a decision\nthis command makes or asks about: it starts automatically, gated entirely by server-side readiness (see\nStage 8).\n\nThis command is portable across Claude Code, Cursor, and Copilot: it only drives the Bridge API MCP\ntools (`get_my_role`, `get_install_manifest`, `apply_install_manifest`).\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n**Install-spawn context.** When the spawning instruction explicitly says this `/install-bridge` run is\nin the "install-spawn context" (it was launched by the `install-bridge` CLI\'s fresh agent session),\nStage 8, Stage 9, and Stage 10 are SKIPPED and the single closing interaction is the concise capability\nreport plus a `/learn-repository` recommendation that the spawn prompt owns. When you invoke\n`/install-bridge` directly (manual invocation), Stages 8, 9, and 10 run normally.\n\n## Stage 1 \u2014 Admin preflight\n\n1. Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `source` is `"legacy"`: proceed (legacy keys are permitted).\n - Else if `role` is `"admin"`: proceed.\n - Otherwise (a non-admin `user_access` key): stop immediately and display:\n ```\n Admin role required to apply install configuration. Ask a project admin to run /install-bridge,\n or use an admin API key.\n ```\n\n## Stage 2 \u2014 Read the manifest (once, pre-apply)\n\n1. Call the `get_install_manifest` MCP tool exactly once here.\n2. Keep the returned `snapshot_token` verbatim \u2014 you must pass the SAME token to the apply call in\n Stage 5. Do not call `get_install_manifest` again before applying; a fresh read would invalidate\n the token you are about to use. (Stage 7 performs a SEPARATE, later read-after-write manifest read\n for the current capability status \u2014 that is deliberate and does not reuse this token.)\n3. The manifest contains ordered `groups` of bootstrap fields (each with `field_name`, `is_set`,\n `current_value`, `guidance`, `examples`, `validation_summary`, `requires_confirmation`), a list of\n `deferred_fields`, a `next_step`, `done_criteria`, a `command_contract_version`, and the\n `snapshot_token`. It also carries the additive capability fields (`integrations`, `configured`,\n `learned`, `indexed`, `tool_capabilities`, `concise_tool_capabilities`, `locked_tools`,\n `unlocked_tools`) \u2014 but ignore those here; the accurate capability status is the post-apply read in\n Stage 7. `tool_capabilities` is the COMPLETE catalog-backed report field (one entry per registered\n MCP tool, grouped and ordered by the server); `concise_tool_capabilities` is the ADDITIVE, bounded\n projection Stage 7 actually renders (see Stage 7); `locked_tools` / `unlocked_tools` are LEGACY\n compatibility data covering only the VCS/index policy cases and are NOT the tool inventory.\n4. Compare the manifest\'s `command_contract_version` to this command\'s contract version (5, stated at\n the top of this file). If the manifest\'s version is HIGHER, this scaffolded command copy is stale:\n warn the user to refresh it by running `npx -y @bridge_gpt/mcp-server --init` in the project root,\n then proceed conservatively \u2014 wherever the manifest\'s `guidance`, `next_step`, or `done_criteria`\n differ from these instructions, follow the manifest.\n\n## Stage 3 \u2014 Derive values for UNSET bootstrap fields only\n\n1. Consider ONLY fields whose `is_set` is `false`. Never re-derive or overwrite a field that is\n already set \u2014 the server will also skip already-set fields, but you should not even propose them.\n2. For each unset field, derive a value from the local codebase following that field\'s `guidance`\n (e.g. infer `working_in` from frameworks/manifests, `version_control_system` from the git remote,\n testing stacks from test config and dependencies, directory fields from the repo layout).\n3. Omit any field you are uncertain about or that does not apply \u2014 leaving it unset is always\n acceptable. Do NOT guess.\n4. Specifically, do NOT invent a `version` when the platform has no meaningful version concept; leave\n it unset.\n5. Before deriving `version_control_system` or `base_branch`, confirm the git repository root is THIS\n project\'s root: `git rev-parse --show-toplevel` must resolve to the project directory itself. If\n the git root is an ancestor directory (the project is nested inside another repository), OMIT both\n fields and note why in the final report \u2014 deriving them would describe the parent repository, not\n this project.\n6. If you find real evidence for TWO OR MORE plausible values for a field (e.g. configs for multiple\n test frameworks, multiple frameworks in a monorepo), treat the value as ambiguous and omit it\n rather than picking one.\n7. Do NOT derive `selected_mcp_slugs`, even though it appears as an unset bootstrap-eligible field in\n the manifest. MCP validation manual selection is deferred to `/learn-repository`, which derives and\n confirms it with the codebase already researched. Install neither proposes nor applies this field.\n\n## Stage 4 \u2014 Human approval for confirmation-requiring fields\n\n`project_description` is the ONLY confirmation-requiring field install proposes. It carries\n`requires_confirmation: true` in the manifest, so it is never applied on derivation alone \u2014 it needs\nexplicit human approval. (`selected_mcp_slugs` also requires confirmation, but install does not\nderive it at all; `/learn-repository` asks for it. See Stage 3 step 7.)\n\n1. If `project_description` is unset and you can draft a concise description from the codebase,\n present the proposed `project_description` text to the human and ask for explicit approval before\n including it.\n2. Include `project_description` in the apply payload ONLY as\n `{ "value": <approved value>, "confirmed": true }`, and only after the human approves it. If the\n human does not approve it, omit the field entirely.\n3. If no human response can be obtained (e.g. a non-interactive / headless session), do NOT stall and\n do NOT abandon the install: proceed to Stage 5 with `project_description` omitted, and report it\n as "pending human input" in the final summary. The other derived fields must still be applied \u2014 an\n unapproved description never blocks them.\n\n## Stage 5 \u2014 Apply (one call)\n\n1. Make exactly ONE call to the `apply_install_manifest` MCP tool, passing:\n - `snapshot_token`: the exact token from Stage 2.\n - `fields`: a map of the derived fields. Scalar fields may be passed directly\n (e.g. `"base_branch": "main"`); an approved `project_description` must use the\n `{ "value": ..., "confirmed": true }` object form from Stage 4. Never include\n `selected_mcp_slugs` in this payload \u2014 install does not derive or apply it (Stage 3 step 7).\n2. Do not implement any client-side skip-if-set, conflict, or confirmation logic \u2014 the server owns\n all of that and returns the outcome buckets.\n3. The apply is partial-tolerant: fields that fail server-side validation are returned in the\n `rejected` bucket while the valid fields still commit. A rejected field is therefore reported,\n not fatal \u2014 do NOT retry the whole apply just because one field was rejected.\n\n## Stage 6 \u2014 Persist the routing credential\n\nThe Bash-spawned `start-tickets` CLI runs in a DIFFERENT runtime surface than this MCP server, so a\nkey that lives only in `.mcp.json` / `.cursor/mcp.json` is invisible to it and difficulty\u2192model\nrouting silently degrades. This final stage persists the already-validated key into the user-scoped\ncredential store (`~/.config/bridge/credentials.json`) so shell-spawned CLI commands can resolve it.\n\n1. Call the `persist_routing_credential` MCP tool with `repo_name` set to the repository name for\n this install (the configured `BAPI_REPO_NAME`). Pass `repo_name` ONLY \u2014 this tool resolves the\n API key inside the MCP server process. NEVER pass or display `BAPI_API_KEY` (nor `api_key`,\n `apiKey`, `secret`, or `token`); the key value must not appear in the tool call or in any output.\n2. On success, tell the user the credential was stored, echoing the returned `target` and `path`,\n e.g. `Stored routing credential for bapi:<repo_name> at <path>`.\n3. On failure, do NOT block the install \u2014 show the rest of the install summary anyway, then tell the\n user the routing credential could not be stored. They can rerun `/install-bridge` after setting\n `BAPI_API_KEY` in the environment, or run\n `npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials` to migrate an\n existing key. Until then, `start-tickets` model routing fails open (assumes a hard ticket and\n defaults to premium/Opus when available) \u2014 this is the MOST EXPENSIVE model tier, so unexplained\n cost is the symptom of leaving this unfixed. Recommend verifying credential resolution afterwards\n with `npx -y @bridge_gpt/mcp-server doctor`.\n\n## Stage 7 \u2014 Summarize the outcome, then present the concise capability report\n\nFirst, begin with an explicit applied count: "Applied N of M derivable fields" (M = the unset\nbootstrap fields you derived values for). An install that applied 0 fields must say so loudly \u2014 the\ninstall is NOT complete until the apply call reports applied fields. This applied-count line and the\nsix-bucket summary below remain the PRIMARY install result \u2014 the capability report that follows is a\nsecondary close, not a replacement for it.\n\nThen summarize the six buckets from the apply response for the user:\n- `applied` \u2014 fields written this run.\n- `skipped` \u2014 fields already set (left untouched).\n- `conflict` \u2014 fields that changed since the manifest was read (re-run /install-bridge to retry).\n- `needs_confirmation` \u2014 fields awaiting explicit confirmation (e.g. project_description).\n- `rejected` \u2014 fields that failed validation or were not bootstrap-eligible (the other fields still\n applied; fix or drop the rejected values before any retry).\n- `deferred` \u2014 fields intentionally not auto-written by install (owned by /learn-repository or set\n deliberately). `selected_mcp_slugs` belongs here: report it as deferred to `/learn-repository`,\n never as approved, declined, or pending an install-time decision.\n\n### Read-after-write: fetch the concise capability report\n\nAfter the bucket summary, call the `get_install_manifest` MCP tool ONCE MORE. This post-apply read\nreflects the configuration you just wrote (the Stage-2 read was pre-apply and is stale for this\npurpose). This read does not need the snapshot token. Use ONLY this post-write response for the\nreport below.\n\nThe response carries `concise_tool_capabilities` \u2014 an ADDITIVE, bounded projection over the complete\n`tool_capabilities` catalog (which the response still carries unchanged; this stage simply does not\nrender it). It is a server-ordered array of at most two tiers, each\n`{id, name, tools: [{tool, display_name}], more_count}`, covering only "Regularly useful" and\n"Occasionally useful", available-now tools only, with everything else in those two tiers collapsed\ninto that tier\'s `more_count`.\n\nServer authority: the server computed this projection\'s tier selection, availability filter, and\n`more_count` arithmetic. Never recompute, re-filter, re-count, or re-derive it from `tool_capabilities`,\n`docs/mcp-tool-integrations.md`, or any other documentation \u2014 render exactly what the server sent.\n\nIf the post-write response has no `concise_tool_capabilities` field at all, or it is present but\nmalformed (not the `{id, name, tools, more_count}` tier shape described above), print exactly:\n`No capability status is available yet; configuration can still continue.` and skip the section below\nentirely \u2014 never fall back to rendering the complete `tool_capabilities` catalog or a remembered/\nhallucinated capability list.\n\nOtherwise, render exactly one section, with this exact heading:\n\n**What Bridge can help with**\n\n- Render each tier from `concise_tool_capabilities` in the server\'s given order: "Regularly useful"\n first, then "Occasionally useful". Do not reorder, filter, re-tier, or drop a tier the server\n included, even if its `tools` array is empty.\n- Within a tier, list each tool\'s `display_name` only, in server order \u2014 no description,\n `availability_text`, effect, dependency explanation, or variant detail; those live on the complete\n `tool_capabilities` field, which this section does not touch.\n- Render the tier\'s `more_count` as plain, muted-style summary text ("+N more") \u2014 never as an\n expansion prompt, a link, or something requiring further action. Omit the "+N more" line entirely\n when `more_count` is `0`.\n- Do not locally filter, count, regroup, infer availability, or fall back to the complete\n `tool_capabilities` collection for this section under any circumstance.\n\n## Stage 8 \u2014 Offer the next step\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it); the\nspawn prompt owns the single closing concise-report-plus-learn-recommendation there. On direct manual\n`/install-bridge` invocation, run it normally:\n\n1. Offer to continue with `/learn-repository` to populate the deeper instruction-tier configuration\n (architecture, review, testing, and correctness standards), matching the manifest\'s `next_step`.\n Mention that it also derives and confirms the applicable MCP validation manuals\n (`selected_mcp_slugs`) \u2014 it researches the codebase first, so it can propose them with evidence\n and explain what they do, which is why install leaves that field alone.\n2. Do NOT ask about repository indexing in any form. There is no consent question, no\n `parse_repository` tool call, and no `/parse-repository` continuation here \u2014 indexing starts\n automatically once the repository reaches full parse readiness (VCS credentials, the Pinecone\n index, `working_in` / `project_description`, and SFCC prerequisites where applicable), via the\n same readiness-gated funnel the GitHub connection-confirm endpoints and the scheduled sweep already\n use. Do not claim indexing has already started \u2014 this command has no visibility into that funnel\'s\n outcome.\n\n## Stage 9 \u2014 Offer CI follow-up configuration (only when CI is detected)\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the\n`config_field` MCP tool. `ci_followup_config` is REPLACE-NOT-AUGMENT \u2014 leaving it NULL already means\nsafe poll-only defaults, so "skip" is always a correct answer.\n\n1. Gate: only offer this when Stage 3 found real CI evidence (e.g. `.github/workflows/*.yml`,\n `bitbucket-pipelines.yml`). With no CI detected, skip this stage and note "no CI detected \u2014 CI\n follow-up not offered" in the final report.\n2. Ask ONE question, referencing the discovered CI by name, with exactly these preset profiles\n (default: skip):\n - **observe** \u2014 poll CI results only, never attempt fixes:\n `{"strategy": "poll_only", "max_iterations": 1, "max_minutes": 10, "instructions": ""}`\n - **self-heal** \u2014 bounded fix-and-iterate loop on the automation\'s own PRs:\n `{"strategy": "fix_and_iterate", "max_iterations": 3, "max_minutes": 45, "instructions": ""}`\n - **skip** (default) \u2014 leave `ci_followup_config` unset (NULL = baseline poll-only behavior).\n3. Do NOT offer or compose a `custom` strategy during install \u2014 free-form CI follow-up instructions\n are a deliberate later act (setup UI or a direct `config_field` update).\n4. When the human picks a profile, call the `config_field` MCP tool once (operation `"update"`,\n `field_name: "ci_followup_config"`, `value`: the profile\'s JSON object). If the server rejects the\n value, retry ONCE with a corrected payload; if it is rejected again, stop, show the proposed JSON\n to the human, and leave the field unset.\n5. Never select a profile in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never enable fix-and-iterate behavior.\n\n## Stage 10 \u2014 Offer the speed-vs-quality repository preference\n\n**Skip this entire stage in the install-spawn context** (the spawning instruction identifies it), so\nthe concise report and learn recommendation the spawn prompt owns remain the sole closing interaction. On direct\nmanual `/install-bridge` invocation, run it normally.\n\nThis stage is separate from the single apply call: it writes at most ONE field via the `config_field`\nMCP tool, `field_name: "speed_vs_quality"`. The column defaults to `5` (max quality) for every\nrepository, so "skip" always leaves a safe, valid value in place.\n\n1. Never ask this question in a non-interactive session \u2014 skip and list it as a pending next step. An\n unattended install must never block on or silently answer this preference.\n2. Read the current value first via the `config_field` MCP tool (operation `"get"`,\n `field_name: "speed_vs_quality"`) so a reinstall can show the stored value \u2014 not always `5` \u2014 as\n the displayed default rather than silently re-asking from scratch.\n3. Ask ONE question with exactly these five numbered presets (default: the value from step 2, or `5`\n if this is the first install):\n - **1** \u2014 Max speed\n - **2** \u2014 Prefer speed\n - **3** \u2014 Balanced\n - **4** \u2014 Prefer quality\n - **5** \u2014 Max quality (default)\n4. Persist ONLY on an explicit answer \u2014 including an explicitly accepted default \u2014 by calling the\n `config_field` MCP tool once (operation `"update"`, `field_name: "speed_vs_quality"`,\n `value`: the selected integer 1-5). Do not overwrite an existing value when the human gives no\n answer at all (e.g. the session cannot obtain one) \u2014 leave the stored/default value untouched in\n that case, distinct from an explicit accepted-default selection of `5`.\n5. After a successful persist, emit one additive structured log line (mirroring the codebase\'s\n `logging.info(msg, extra={...})` convention for non-response-body observability signals) with\n fields: `event="install.speed_vs_quality"`, `repo_name`, `field_name="speed_vs_quality"`,\n `selected_preset` (the persisted integer), `outcome="persisted"`. Never log this event before the\n `config_field` tool call has confirmed the write.\n6. When skipped, emit the corresponding structured event with `outcome` set to one of\n `"skipped_non_interactive"` (non-interactive session) or `"skipped_no_answer"` (interactive session,\n no explicit answer obtained) \u2014 omit `selected_preset` and any prompt text from this event.\n7. If the `config_field` call is rejected, retry ONCE with a corrected payload; if it is rejected\n again, stop, show the proposed value to the human, and leave the field at its current stored value.\n Do not log a persistence-success event for a rejected or failed write.\n\n## Return\n\nReport the admin check result, the "Applied N of M" count, the\napplied/skipped/conflict/needs_confirmation/rejected field names, any fields omitted for\nnested-repository or ambiguity reasons, the approval outcome for `project_description` \u2014 install\'s\nonly confirmation-requiring field \u2014 (approved / declined / pending human input), the fact that\n`selected_mcp_slugs` is deferred to `/learn-repository` rather than decided here, whether a\nstale-command warning was raised (manifest\n`command_contract_version` higher than this command\'s), whether the routing credential was persisted\n(the returned `target` and `path`, or the non-blocking failure remediation), the "What Bridge can help\nwith" concise capability report from the post-apply read-after-write manifest read, the CI follow-up\noutcome (profile written / skipped / no CI detected / pending / skipped in install-spawn context), the\nspeed-vs-quality preference outcome (persisted with its preset / skipped_non_interactive /\nskipped_no_answer / pending / skipped in install-spawn context), and the `/learn-repository`\nrecommendation.\n',"learn-repository.md":`Learn and document all configuration fields for the repository by running parallel research agents.
|
|
3296
3296
|
|
|
3297
3297
|
$ARGUMENTS
|
|
3298
3298
|
|
|
@@ -3361,6 +3361,44 @@ This command is recipe-driven. Do not call MCP tools directly -- the recipe dete
|
|
|
3361
3361
|
- Report confirmation candidates that could not be presented in a headless session with the exact
|
|
3362
3362
|
phrase \`pending human input\`.
|
|
3363
3363
|
- Every field that was condensed must appear with the reason it was condensed.
|
|
3364
|
+
|
|
3365
|
+
6. **After** the \`## Learn Complete\` summary above is fully displayed, close with the same concise
|
|
3366
|
+
capability report \`/install-bridge\` renders (BAPI-658, AC-9). This is the recipe's ONE exception to
|
|
3367
|
+
"do not call MCP tools directly": call the \`get_install_manifest\` MCP tool EXACTLY ONCE here,
|
|
3368
|
+
directly, with no arguments beyond what it requires \u2014 never through the recipe, never a second time,
|
|
3369
|
+
and never to apply or change any configuration.
|
|
3370
|
+
|
|
3371
|
+
The report is structurally and visually SUBORDINATE to \`## Learn Complete\` above it \u2014 it is a
|
|
3372
|
+
closing addendum, not a replacement for or a distraction from the learn summary's own status,
|
|
3373
|
+
fields, gaps, and confirmation outcome.
|
|
3374
|
+
|
|
3375
|
+
If the \`get_install_manifest\` call errors, or its response has no \`concise_tool_capabilities\` field,
|
|
3376
|
+
or that field is present but malformed (not the server's \`{id, name, tools, more_count}\` tier
|
|
3377
|
+
shape), print exactly this line and stop \u2014 do not attempt the report in any other form:
|
|
3378
|
+
|
|
3379
|
+
\`\`\`
|
|
3380
|
+
capability report unavailable \u2014 run /install-bridge to see it
|
|
3381
|
+
\`\`\`
|
|
3382
|
+
|
|
3383
|
+
Never substitute documentation, the complete \`tool_capabilities\` catalog, a remembered tool list, or
|
|
3384
|
+
an inferred capability category for a missing or malformed concise field \u2014 a hallucinated report
|
|
3385
|
+
during this trust-critical first run is worse than no report at all.
|
|
3386
|
+
|
|
3387
|
+
Otherwise, render exactly one section, with this exact heading, from \`concise_tool_capabilities\`
|
|
3388
|
+
only:
|
|
3389
|
+
|
|
3390
|
+
**What Bridge can help with**
|
|
3391
|
+
|
|
3392
|
+
- Render each tier in the server's given order: "Regularly useful" first, then "Occasionally
|
|
3393
|
+
useful". Do not reorder, filter, re-tier, or drop a tier the server included, even if its \`tools\`
|
|
3394
|
+
array is empty.
|
|
3395
|
+
- Within a tier, list each tool's \`display_name\` only, in server order \u2014 no description,
|
|
3396
|
+
availability text, effect, dependency explanation, or variant detail.
|
|
3397
|
+
- Render the tier's \`more_count\` as plain, muted-style summary text ("+N more") \u2014 never as an
|
|
3398
|
+
expansion prompt, a link, or something requiring further action (it is not interactive or
|
|
3399
|
+
expandable). Omit the "+N more" line entirely when \`more_count\` is \`0\`.
|
|
3400
|
+
- Do not locally filter, count, regroup, infer availability, write configuration, or fall back to
|
|
3401
|
+
the complete \`tool_capabilities\` collection for this section under any circumstance.
|
|
3364
3402
|
`,"parse-repository.md":`Queue a background job to parse and index the repository for Bridge API's AI agents.
|
|
3365
3403
|
|
|
3366
3404
|
$ARGUMENTS
|
|
@@ -4654,12 +4692,12 @@ Agents: scaffolded ${agentTotal} agent${agentTotal===1?"":"s"}`),agentWritten.si
|
|
|
4654
4692
|
.bridge/config: skipped \u2014 already exists`):(await mkdir2(path5.dirname(bridgeConfigPath2),{recursive:!0}),await writeFile2(bridgeConfigPath2,buildBridgeConfigManifest(chooseScaffoldRepoName(cwd)),"utf-8"),console.log(`
|
|
4655
4693
|
.bridge/config: written`)),console.log(" Credentials are resolved at runtime from BAPI_API_KEY or ~/.config/bridge/credentials.json (no secrets are written to .bridge/config)."),anyCreatedOrAdded&&console.log(`
|
|
4656
4694
|
Set BAPI_REPO_NAME in your config files. Do NOT put BAPI_API_KEY in the generated MCP config \u2014 supply it via the BAPI_API_KEY environment variable, or store it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. Get your values from the Bridge API setup UI at https://bridgegpt-api.com`)}init_start_tickets();init_start_tickets();init_review_tickets();init_start_tickets();init_version_generated();import{readFile as readFile6,stat as stat4}from"fs/promises";import{spawn}from"child_process";import os5 from"os";import path17 from"path";init_credential_store();import path16 from"path";var DEFAULT_BASE_URL="https://bridgegpt-api.com",MCP_CONFIG_ENV_TARGETS=[{relPath:".mcp.json",topLevelKey:"mcpServers"},{relPath:".cursor/mcp.json",topLevelKey:"mcpServers"},{relPath:".vscode/mcp.json",topLevelKey:"servers"}],PROBE_TIMEOUT_MS=5e3;async function resolveInstallDoctorTarget(deps){let envRepo=deps.env.BAPI_REPO_NAME?.trim(),envBase=deps.env.BAPI_BASE_URL?.trim(),repoName=envRepo&&envRepo.length>0?envRepo:null,repoSource=repoName?"env":null,baseUrl=envBase&&envBase.length>0?envBase:null;if(!repoName||!baseUrl)for(let{relPath,topLevelKey}of MCP_CONFIG_ENV_TARGETS){let raw;try{raw=await deps.readFile(path16.join(deps.cwd,relPath))}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{continue}let envBlock=parsed&&typeof parsed=="object"?parsed[topLevelKey]?.["bridge-api"]?.env:void 0;if(envBlock&&(!repoName&&typeof envBlock.BAPI_REPO_NAME=="string"&&envBlock.BAPI_REPO_NAME.trim()&&(repoName=envBlock.BAPI_REPO_NAME.trim(),repoSource="config"),!baseUrl&&typeof envBlock.BAPI_BASE_URL=="string"&&envBlock.BAPI_BASE_URL.trim()&&(baseUrl=envBlock.BAPI_BASE_URL.trim()),repoName&&baseUrl))break}return{repoName,repoSource,baseUrl:baseUrl??DEFAULT_BASE_URL}}async function probeGet(deps,url,apiKey){try{let resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(PROBE_TIMEOUT_MS)}),body=null;try{body=await resp.json()}catch{}return{ok:!0,status:resp.status,body}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function summarizeManifestGroups(body){if(!body||typeof body!="object")return null;let groups=body.groups;if(!Array.isArray(groups))return null;let total=0,unset=[];for(let group of groups){let fields=group?.fields;if(Array.isArray(fields))for(let field of fields){let name=field?.field_name;typeof name=="string"&&(total+=1,field.is_set||unset.push(name))}}return{total,unset}}function summarizeIntegrations(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;let total=0,unconfigured=[];for(let item of integrations){let label=item?.label;typeof label=="string"&&(total+=1,item.is_configured||unconfigured.push(label))}return{total,unconfigured}}function readGithubConfiguredFlag(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;for(let item of integrations){if(item?.id!=="github_app")continue;let configured=item.is_configured;return typeof configured=="boolean"?configured:null}return null}async function collectInstallStatusChecks(deps){let checks=[],target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return checks.push({id:"identity",label:"Repository identity",status:"SKIP",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs",remediation:"run install-bridge (or set BAPI_REPO_NAME) to configure this project."}),checks;checks.push({id:"identity",label:"Repository identity",status:"PASS",detail:`${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`});let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok){checks.push({id:"credential",label:"Bridge API credential",status:"WARN",detail:`not resolved (${cred.kind})`,remediation:"set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; until then the remaining install checks are skipped."});for(let[id,label]of[["connectivity","Server connectivity"],["bootstrap","Bootstrap config fields"],["indexing","Repository indexing"]])checks.push({id,label,status:"SKIP",detail:"no credential resolved"});return checks}checks.push({id:"credential",label:"Bridge API credential",status:"PASS",detail:`resolved from ${cred.credentials.source} (value never read into the report)`});let apiKey=cred.credentials.apiKey,repoQuery=`repo_name=${encodeURIComponent(target.repoName)}`,ping=await probeGet(deps,`${target.baseUrl}/jira/ping?${repoQuery}`,apiKey);ping.ok?ping.status===200?checks.push({id:"connectivity",label:"Server connectivity",status:"PASS",detail:"ping OK"}):ping.status===401||ping.status===403?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server rejected the resolved key (HTTP ${ping.status})`,remediation:"the key may have been rotated \u2014 re-run install-bridge with a current key."}):ping.status===404?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server does not know repo '${target.repoName}' (HTTP 404)`,remediation:"create the project in the setup UI (or check BAPI_REPO_NAME spelling)."}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unexpected HTTP ${ping.status} from /jira/ping`}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unreachable (${ping.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});let manifest=await probeGet(deps,`${target.baseUrl}/jira/config/install-manifest?${repoQuery}`,apiKey);if(!manifest.ok)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`manifest unreachable (${manifest.error})`});else if(manifest.status===404)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"no configuration row found for this repository",remediation:"create the project in the setup UI, then run /install-bridge."});else if(manifest.status===200){let summary=summarizeManifestGroups(manifest.body);summary?summary.unset.length===0?checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"PASS",detail:`${summary.total}/${summary.total} bootstrap fields set`}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`${summary.total-summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,remediation:"run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave)."}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"manifest response had an unexpected shape"})}else checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`unexpected HTTP ${manifest.status} from the install manifest`});if(manifest.ok&&manifest.status===200){let integrations=summarizeIntegrations(manifest.body);integrations===null?checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"the manifest response carried no integrations checklist"}):integrations.unconfigured.length===0?checks.push({id:"integrations",label:"Integration credentials",status:"PASS",detail:`${integrations.total}/${integrations.total} configured`}):checks.push({id:"integrations",label:"Integration credentials",status:"WARN",detail:`not configured: ${integrations.unconfigured.join(", ")}`,remediation:"a human configures these in the setup UI (project settings) \u2014 Bridge API never accepts integration secrets through an agent or MCP tool."})}else checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"manifest unavailable"});if(manifest.ok&&manifest.status===200){let github=readGithubConfiguredFlag(manifest.body);github===null?checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"the manifest response carried no GitHub integration entry"}):github?checks.push({id:"github",label:"GitHub connection",status:"PASS",detail:"a GitHub repository is connected to this project"}):checks.push({id:"github",label:"GitHub connection",status:"WARN",detail:"no GitHub repository is connected to this project",remediation:`run 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${target.repoName}'.`})}else checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"manifest unavailable"});let parse=await probeGet(deps,`${target.baseUrl}/jira/parse-status?${repoQuery}`,apiKey);if(!parse.ok||parse.status!==200)checks.push({id:"indexing",label:"Repository indexing",status:"SKIP",detail:parse.ok?`HTTP ${parse.status} from /jira/parse-status`:`unreachable (${parse.error})`});else if(parse.body?.status==="in_progress"){let startedAt=parse.body.started_at;checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:`parse job in progress${typeof startedAt=="string"?` (started ${startedAt})`:""}`})}else checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:"no parse job currently running \u2014 if this repository has never been indexed, queue one with /parse-repository (monitor with get_parse_status)."});return checks}function formatInstallStatusReport(checks){let lines=["","Install status (easy-install done criteria \u2014 advisory)",""],pad={PASS:"PASS ",WARN:"WARN ",INFO:"INFO ",SKIP:"SKIPPED"};for(let check of checks)lines.push(`${pad[check.status]} ${check.label}${check.detail?` \u2014 ${check.detail}`:""}`),check.remediation&&lines.push(` ${check.remediation}`);return lines.push(""),lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only."),lines.join(`
|
|
4657
|
-
`)}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]);function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path37=>`${trimmedBase}/jira${path37}`;return{buildUrl:buildUrl2,buildApiUrl:path37=>`${trimmedBase}${path37}`,buildGetUrl:(path37,params)=>{let url=new URL(buildUrl2(path37));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=
|
|
4695
|
+
`)}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"]);function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path37=>`${trimmedBase}/jira${path37}`;return{buildUrl:buildUrl2,buildApiUrl:path37=>`${trimmedBase}${path37}`,buildGetUrl:(path37,params)=>{let url=new URL(buildUrl2(path37));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=2500,TOOL_SURFACE_POLL_MIN_MS=12e3,TOOL_SURFACE_POLL_MAX_MS=18e3;function timeoutResult(){return{reason:"timeout",blockedTools:new Set}}function malformedResult(subtype){return{reason:"malformed",subtype,blockedTools:new Set}}function validateToolSurfacePayload(body){if(body===null||typeof body!="object"||Array.isArray(body))return malformedResult("invalid-shape");let p=body;if(typeof p.schema_version!="number"||!Number.isInteger(p.schema_version))return malformedResult("invalid-shape");if(!RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS.has(p.schema_version))return malformedResult("unsupported-schema");if(typeof p.complete!="boolean"||typeof p.evaluated_tool_count!="number"||!Number.isInteger(p.evaluated_tool_count)||p.evaluated_tool_count<0||typeof p.catalog_revision!="string"||!Array.isArray(p.blocked_tools)||!p.blocked_tools.every(t=>typeof t=="string"))return malformedResult("invalid-shape");if(!p.complete)return malformedResult("incomplete");if(p.catalog_revision.length===0)return malformedResult("invalid-shape");let blockedTools=new Set(p.blocked_tools);return{reason:"blocked",catalogRevision:p.catalog_revision,evaluatedToolCount:p.evaluated_tool_count,blockedTools}}async function probeToolSurface(options){let deadlineMs=options.deadlineMs??TOOL_SURFACE_PROBE_DEADLINE_MS,controller=new AbortController,onLifecycleAbort=()=>controller.abort();options.abortSignal&&(options.abortSignal.aborted?controller.abort():options.abortSignal.addEventListener("abort",onLifecycleAbort,{once:!0}));let timer,deadlinePromise=new Promise(resolve2=>{timer=setTimeout(()=>{controller.abort(),resolve2(timeoutResult())},deadlineMs)}),abortPromise=new Promise(resolve2=>{if(controller.signal.aborted){resolve2(timeoutResult());return}controller.signal.addEventListener("abort",()=>resolve2(timeoutResult()),{once:!0})}),workPromise=(async()=>{try{let headers=await options.resolveHeaders();if(controller.signal.aborted)return timeoutResult();let resp=await options.fetchFn(options.url,{method:"GET",headers,signal:controller.signal});if(!resp.ok)return malformedResult("non-2xx");let parsed;try{parsed=await resp.json()}catch{return controller.signal.aborted?timeoutResult():malformedResult("invalid-json")}return validateToolSurfacePayload(parsed)}catch{return controller.signal.aborted?timeoutResult():malformedResult("network")}})();try{return await Promise.race([workPromise,deadlinePromise,abortPromise])}finally{timer&&clearTimeout(timer),options.abortSignal&&options.abortSignal.removeEventListener("abort",onLifecycleAbort)}}var defaultScheduler={setTimeout:(callback,ms)=>setTimeout(callback,ms),clearTimeout:handle=>clearTimeout(handle),random:()=>Math.random()};function logDecision(logger,result,hiddenCount,hiddenNames){let revision=result.reason==="blocked"?result.catalogRevision:"n/a",subtype=result.reason==="malformed"?result.subtype:"n/a";logger(`tool-surface gating: reason=${result.reason} subtype=${subtype} hidden=${hiddenCount} revision=${revision} hidden_tools=[${hiddenNames.join(", ")}]`)}function createToolSurfaceGate(options){let{startupProbe,advertised,originalListHandler,freshProbe,notify,logger,lifecycleController}=options,scheduler=options.scheduler??defaultScheduler,advertisedNames=new Set(advertised.map(r=>r.name)),hiddenNames=new Set,lastServedVisible=null,catalogRevision=null,startupApplied=!1,timer,closed=!1;function deriveHidden(result){if(result.reason!=="blocked"||result.blockedTools.size===0)return new Set;let hidden=new Set;for(let id of result.blockedTools)advertisedNames.has(id)&&hidden.add(id);return hidden}function deriveVisible(hidden){let visible=new Set;for(let reg of advertised)reg.isEnabled()&&(hidden.has(reg.name)||visible.add(reg.name));return visible}function applyDecision(result){let nextHidden=deriveHidden(result);hiddenNames=nextHidden,logDecision(logger,result,nextHidden.size,Array.from(nextHidden)),result.reason==="blocked"&&result.catalogRevision!==catalogRevision&&(catalogRevision!==null&&logger(`tool-surface gating: catalog_revision ${catalogRevision} -> ${result.catalogRevision}`),catalogRevision=result.catalogRevision)}function projectList(original){let tools=original.tools.filter(tool=>!hiddenNames.has(tool.name));return{...original,tools}}let handleList=async(request,extra)=>{let startupResult=await startupProbe;startupApplied||(startupApplied=!0,applyDecision(startupResult));let original=await originalListHandler(request,extra),projected=projectList(original);return lastServedVisible=new Set(projected.tools.map(t=>t.name)),projected};async function pollOnce(){let result;try{result=await freshProbe()}catch{result=timeoutResult()}if(closed)return;let previousVisibleServed=lastServedVisible;applyDecision(result);let nextVisible=deriveVisible(hiddenNames);if(previousVisibleServed!==null&&!setsEqual(previousVisibleServed,nextVisible)){lastServedVisible=nextVisible;try{notify()}catch{logger("tool-surface gating: notification failed (suppressed)")}}}function scheduleNext(){if(closed)return;let span=TOOL_SURFACE_POLL_MAX_MS-TOOL_SURFACE_POLL_MIN_MS,delay=Math.round(TOOL_SURFACE_POLL_MIN_MS+scheduler.random()*span);timer=scheduler.setTimeout(()=>{pollOnce().finally(()=>{scheduleNext()})},delay),timer&&typeof timer.unref=="function"&&timer.unref()}function startPolling(){closed||scheduleNext()}function close(){closed||(closed=!0,timer&&(scheduler.clearTimeout(timer),timer=void 0),lifecycleController.signal.aborted||lifecycleController.abort())}return{handleList,startPolling,close}}function setsEqual(a,b){if(a.size!==b.size)return!1;for(let v of a)if(!b.has(v))return!1;return!0}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();function getDoctorUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server doctor [--agent <name>]","","Read-only diagnostics for the start-tickets CLI. It only checks your","environment and prints manual install instructions \u2014 it does not install","anything, modify your system, or start the MCP server.","","Flags:"," --agent claude|cursor-agent Agent to include in the prerequisite check (default: claude)"," -h, --help Show this help","","Checks (for the current OS): the start-tickets preflight prerequisites plus","uv, the selected agent's command, Bridge API credential resolution, and","worktree MCP registration reachability. Credential resolution reports the","source it would use (env vs. store target bapi:<repo>); it never reads or","prints the key value and never writes the credential store. To persist or","migrate a credential, use /install-bridge or the `credentials` subcommand \u2014","doctor stays strictly read-only.","","The report also includes an advisory 'Install status' section (easy-install","done criteria): repo identity, credential resolution, server connectivity,","bootstrap-field completeness, and repository-indexing state. It performs","read-only GETs only and never affects the exit code.","","It also includes an advisory 'MCP tool surface' section (BAPI-641): what","dynamic capability gating would advertise for this repo. It performs at most","one read-only GET to /jira/mcp/tool-surface (none under the kill switch) and","is advisory/fail-open \u2014 a timeout or malformed response is reported as","'fail-open to full surface' and never affects the exit code. Clients that","ignore notifications/tools/list_changed must reconnect or start a new MCP","session to observe surface changes; no project MCP config change is required.","","Conductor ledger / native-module diagnostics (the SQLite ledger's native","binding load status and Node-version skew) live under a separate command:"," conductor doctor","That command is likewise strictly read-only \u2014 it does not install, rebuild,","migrate, or write ledger files.","","Exit code: 0 when all required prerequisites are present, non-zero otherwise."].join(`
|
|
4658
4696
|
`)}function parseDoctorArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getDoctorUsage()};let agentName=DEFAULT_AGENT_NAME;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--fix"||arg.startsWith("--fix="))return{status:"error",message:"--fix is unsupported: doctor is strictly read-only and never installs or modifies anything. Run the printed install commands manually."};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else{if(i+1>=argv.length)return{status:"error",message:"--agent requires a value (an agent name)."};i+=1,value=argv[i]}if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. doctor does not accept positional arguments.`}}return{status:"ok",options:{agentName}}}async function collectDoctorResults(deps,agentName){let agent=resolveAgentSpec(agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),descriptorsResult=getDoctorPrereqDescriptors(deps.platform,deps.env,agent);if(!descriptorsResult.ok)return{ok:!1,unsupported:!0,error:descriptorsResult.error};let injected=deps,probeDeps={...deps,readFile:injected.readFile??(p=>readFile6(p,"utf-8")),stat:injected.stat??(p=>stat4(p)),homedir:injected.homedir??os5.homedir},results=[];for(let descriptor of descriptorsResult.descriptors)results.push(await probePrerequisite(probeDeps,descriptor));return{ok:!0,results}}function formatDoctorReport(platform,agent,collection){let activeGroups=Array.from(resolveProfiles(process.env.BRIDGE_MCP_PROFILE)).join(", "),lines=["start-tickets doctor (read-only diagnostics)",`Platform: ${platform}`,`Selected agent: ${agent.name} (command: ${agent.command})`,`Active MCP Groups: \`${activeGroups}\``,""];if(!collection.ok)return lines.push(`Platform '${platform}' is unsupported. start-tickets supports darwin, win32, and linux.`),lines.join(`
|
|
4659
4697
|
`);for(let result of collection.results){let status=result.found?"FOUND ":"MISSING",detail=result.found&&result.detail?` (${result.detail})`:"";lines.push(`${status} ${result.label}${detail}`),result.found||lines.push(` To install manually: ${result.installHint}`),result.authNote&&lines.push(` Note: ${result.authNote}`)}let anyMissing=collection.results.some(r=>!r.found);return lines.push(""),lines.push(anyMissing?"Some prerequisites are missing \u2014 install the ones above manually, then re-run doctor.":"All required prerequisites are present."),lines.push("For conductor ledger/native-module diagnostics, run: conductor doctor"),lines.join(`
|
|
4660
4698
|
`)}var BRIDGE_PACKAGE_NAME="@bridge_gpt/mcp-server",LAUNCHER_CONFIG_TARGETS=[{relPath:".mcp.json",topLevelKey:"mcpServers"},{relPath:".cursor/mcp.json",topLevelKey:"mcpServers"},{relPath:".vscode/mcp.json",topLevelKey:"servers"}];function parseLauncherPin(args){if(!Array.isArray(args))return null;for(let arg of args)if(typeof arg=="string"){if(arg===BRIDGE_PACKAGE_NAME)return{spec:arg,version:null};if(arg.startsWith(`${BRIDGE_PACKAGE_NAME}@`)){let version=arg.slice(BRIDGE_PACKAGE_NAME.length+1).trim();return{spec:arg,version:version.length>0?version:null}}}return null}function probeNpxNoInstallDefault(spec){return new Promise(resolve2=>{try{let child=spawn("npx",["--no-install",spec,"--version"],{shell:!1,stdio:"ignore",timeout:3e4});child.on("error",()=>resolve2({warmed:!1,indeterminate:!0})),child.on("close",(code,signal)=>{resolve2(signal?{warmed:!1,indeterminate:!0}:code===0?{warmed:!0,indeterminate:!1}:{warmed:!1,indeterminate:!0})})}catch{resolve2({warmed:!1,indeterminate:!0})}})}async function inspectLauncherCache(deps){let inspections=[];for(let{relPath,topLevelKey}of LAUNCHER_CONFIG_TARGETS){let fullPath=path17.join(deps.cwd,relPath),raw;try{raw=await deps.readFile(fullPath)}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{inspections.push({relPath,spec:null,pinnedVersion:null,state:"indeterminate",remediation:"config is not valid JSON \u2014 cannot determine the launcher pin."});continue}let entry=parsed&&typeof parsed=="object"?parsed[topLevelKey]?.["bridge-api"]:void 0;if(!entry)continue;let pin=parseLauncherPin(entry.args);if(!pin){inspections.push({relPath,spec:null,pinnedVersion:null,state:"indeterminate",remediation:`no ${BRIDGE_PACKAGE_NAME} spec found in the launcher args.`});continue}if(pin.version===null||pin.version==="latest"){inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"unpinned",remediation:`pin the launcher to ${BRIDGE_PACKAGE_NAME}@${VERSION} (run /install-bridge or the install-bridge subcommand to rewrite this config).`});continue}if(pin.version!==VERSION){inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"stale-pinned",remediation:`config pins ${pin.version} but this package is ${VERSION}; run upgrade-bridge / install-bridge to repin and re-warm.`});continue}(await deps.probeNpxNoInstall(pin.spec)).warmed?inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"warmed"}):inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"indeterminate",remediation:`pinned-but-unwarmed: the pinned _npx bucket is not a confirmed cache hit, so the first MCP launch may pay a one-time cold install. Warm it with: npx ${pin.spec} --version, or raise MCP_TIMEOUT for the first launch.`})}return inspections}function formatLauncherCacheReport(inspections){let lines=["","Launcher cache (MCP cold-start readiness)",""];if(inspections.length===0)return lines.push("No project-local bridge-api launcher configs found to inspect."),lines.join(`
|
|
4661
4699
|
`);let labels={warmed:"WARMED ",unpinned:"UNPINNED","stale-pinned":"STALE-PINNED",indeterminate:"INDETERMINATE"};for(let i of inspections){let specText=i.spec?` (${i.spec})`:"";lines.push(`${labels[i.state]} ${i.relPath}${specText}`),i.remediation&&lines.push(` ${i.remediation}`)}return lines.join(`
|
|
4662
|
-
`)}async function collectToolSurfaceDiagnostic(deps){if(!parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED))return{enabled:!1,reason:"kill-switch"};let target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return{enabled:!0,reason:"unresolved",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs"};let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok)return{enabled:!0,reason:"unresolved",detail:`no Bridge API credential resolved (${cred.kind})`};let apiKey=cred.credentials.apiKey,url=createBridgeApiUrls(target.baseUrl).buildGetUrl("/mcp/tool-surface",{repo_name:target.repoName}),result=await probeToolSurface({url,resolveHeaders:async()=>({"X-API-Key":apiKey}),fetchFn:deps.fetch});return result.reason==="blocked"?{enabled:!0,reason:"blocked",blockedTools:Array.from(result.blockedTools),catalogRevision:result.catalogRevision,evaluatedToolCount:result.evaluatedToolCount}:result.reason==="timeout"?{enabled:!0,reason:"timeout"}:{enabled:!0,reason:"malformed",subtype:result.subtype}}function formatToolSurfaceDiagnosticReport(diag){let lines=["","MCP tool surface (dynamic capability gating \u2014 advisory)",""];switch(lines.push(`Kill switch: ${diag.enabled?"ENABLED (gating active)":"DISABLED (full surface)"}`),diag.reason){case"kill-switch":lines.push("Reason: kill-switch \u2014 BAPI_MCP_TOOL_SURFACE_GATING_ENABLED is off, so the full profile surface is advertised and no probe is performed.");break;case"unresolved":lines.push(`Reason: unresolved \u2014 ${diag.detail??"repo/credential not resolved"}; the probe was skipped and the full surface is advertised (fail-open to full surface).`);break;case"blocked":{let ids=diag.blockedTools??[];lines.push("Reason: blocked \u2014 the backend returned a valid capability decision."),lines.push("Probe: reachable (HTTP 200, valid response)."),lines.push(`Blocked tools (${ids.length}): [${ids.join(", ")}]`),diag.catalogRevision&&lines.push(`Catalog revision: ${diag.catalogRevision}`),typeof diag.evaluatedToolCount=="number"&&lines.push(`Evaluated tool count: ${diag.evaluatedToolCount}`);break}case"timeout":lines.push("Reason: timeout \u2014 the
|
|
4700
|
+
`)}async function collectToolSurfaceDiagnostic(deps){if(!parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED))return{enabled:!1,reason:"kill-switch"};let target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return{enabled:!0,reason:"unresolved",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs"};let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok)return{enabled:!0,reason:"unresolved",detail:`no Bridge API credential resolved (${cred.kind})`};let apiKey=cred.credentials.apiKey,url=createBridgeApiUrls(target.baseUrl).buildGetUrl("/mcp/tool-surface",{repo_name:target.repoName}),result=await probeToolSurface({url,resolveHeaders:async()=>({"X-API-Key":apiKey}),fetchFn:deps.fetch});return result.reason==="blocked"?{enabled:!0,reason:"blocked",blockedTools:Array.from(result.blockedTools),catalogRevision:result.catalogRevision,evaluatedToolCount:result.evaluatedToolCount}:result.reason==="timeout"?{enabled:!0,reason:"timeout"}:{enabled:!0,reason:"malformed",subtype:result.subtype}}function formatToolSurfaceDiagnosticReport(diag){let lines=["","MCP tool surface (dynamic capability gating \u2014 advisory)",""];switch(lines.push(`Kill switch: ${diag.enabled?"ENABLED (gating active)":"DISABLED (full surface)"}`),diag.reason){case"kill-switch":lines.push("Reason: kill-switch \u2014 BAPI_MCP_TOOL_SURFACE_GATING_ENABLED is off, so the full profile surface is advertised and no probe is performed.");break;case"unresolved":lines.push(`Reason: unresolved \u2014 ${diag.detail??"repo/credential not resolved"}; the probe was skipped and the full surface is advertised (fail-open to full surface).`);break;case"blocked":{let ids=diag.blockedTools??[];lines.push("Reason: blocked \u2014 the backend returned a valid capability decision."),lines.push("Probe: reachable (HTTP 200, valid response)."),lines.push(`Blocked tools (${ids.length}): [${ids.join(", ")}]`),diag.catalogRevision&&lines.push(`Catalog revision: ${diag.catalogRevision}`),typeof diag.evaluatedToolCount=="number"&&lines.push(`Evaluated tool count: ${diag.evaluatedToolCount}`);break}case"timeout":lines.push("Reason: timeout \u2014 the probe deadline elapsed; fail-open to full surface.");break;case"malformed":lines.push(`Reason: malformed (${diag.subtype??"unknown"}) \u2014 fail-open to full surface.`);break}return lines.push(""),lines.push("Blocked IDs are intersected with the locally active MCP profile and the current SDK-enabled"),lines.push("baseline only when an MCP session starts, so IDs unknown to this package or excluded by the"),lines.push("active profile have no effect. Capability-hidden tools remain registered and callable \u2014 the"),lines.push("backend is the enforcement boundary. This section is advisory and never changes the exit code."),lines.push("Clients that ignore notifications/tools/list_changed must reconnect or start a new MCP session"),lines.push("to observe surface changes; no project MCP configuration change is required."),lines.join(`
|
|
4663
4701
|
`)}async function runDoctorCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseDoctorArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getDoctorUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),agent=resolveAgentSpec(parsed.options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),collection=await collectDoctorResults(deps,parsed.options.agentName);log(formatDoctorReport(deps.platform,agent,collection));try{let launcherDeps={cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile6(p,"utf-8")),probeNpxNoInstall:overrides.launcherProbe?.probeNpxNoInstall??probeNpxNoInstallDefault},launcherInspections=await inspectLauncherCache(launcherDeps);log(formatLauncherCacheReport(launcherInspections))}catch{}if(overrides.installStatus!==!1)try{let injectedFs=deps,installDeps={env:overrides.installStatus?.env??deps.env,cwd:overrides.installStatus?.cwd??deps.cwd,platform:overrides.installStatus?.platform??deps.platform,homedir:overrides.installStatus?.homedir??injectedFs.homedir??os5.homedir,readFile:overrides.installStatus?.readFile??injectedFs.readFile??(p=>readFile6(p,"utf-8")),stat:overrides.installStatus?.stat??injectedFs.stat??(p=>stat4(p)),fetch:overrides.installStatus?.fetch??((...args)=>fetch(...args))},checks=await collectInstallStatusChecks(installDeps);log(formatInstallStatusReport(checks))}catch{}if(overrides.toolSurface!==!1)try{let injectedFs=deps,toolSurfaceDeps={env:overrides.toolSurface?.env??deps.env,cwd:overrides.toolSurface?.cwd??deps.cwd,platform:overrides.toolSurface?.platform??deps.platform,homedir:overrides.toolSurface?.homedir??injectedFs.homedir??os5.homedir,readFile:overrides.toolSurface?.readFile??injectedFs.readFile??(p=>readFile6(p,"utf-8")),stat:overrides.toolSurface?.stat??injectedFs.stat??(p=>stat4(p)),fetch:overrides.toolSurface?.fetch??((...args)=>fetch(...args))},diagnostic=await collectToolSurfaceDiagnostic(toolSurfaceDeps);log(formatToolSurfaceDiagnosticReport(diagnostic))}catch{log(formatToolSurfaceDiagnosticReport({enabled:parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),reason:"malformed",subtype:"unexpected"}))}return collection.ok?collection.results.some(r=>!r.found)?1:0:1}function extractBackendWarnings(body){if(body===null||typeof body!="object")return[];let record=body,warnings=[];if(typeof record.warning=="string"&&record.warning&&warnings.push(record.warning),Array.isArray(record.warnings))for(let entry of record.warnings)typeof entry=="string"&&entry?warnings.push(entry):entry!==null&&typeof entry=="object"&&typeof entry.message=="string"&&entry.message&&warnings.push(entry.message);return warnings}function appendBackendWarningsToText(text,warnings){return warnings.length?`${text}
|
|
4664
4702
|
|
|
4665
4703
|
**Warning:** ${warnings.join(" ")}`:text}init_schedule_run();init_bridge_config();init_credential_store();init_third_party_mcp_targets();import{spawn as spawn2,execFile as execFile3}from"child_process";import{stat as stat5,readFile as readFile7}from"fs/promises";import path20 from"path";import os6 from"os";function getMcpInvokeUsage(){return["Usage:"," node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",""," (npm-channel fallback may invoke the same shim through a package spec, e.g."," npx -y @bridge_gpt/mcp-server@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)","","Internal worktree shim: resolves the target's launch command and credentials","from the given project root, then spawns the real MCP server over stdio.","Argument parsing is identical regardless of how this process was launched","(absolute-path node invocation vs npm-channel npx).","","Flags:"," --target <target> MCP target to launch. 'bapi' launches the Bridge"," API server; a configured third-party target from"," .bridge/config (e.g. sfcc) launches that server."," --project-root <ABS_PATH> Absolute path to the worktree (required)"," -h, --help Show this help"].join(`
|
|
@@ -4679,12 +4717,13 @@ ${userPrompt}`}import path22 from"node:path";var SMOKE_EVIDENCE_MAX_BYTES=8e3;va
|
|
|
4679
4717
|
${getExecutorWatchUsage()}`),1;let registryDeps=overrides.registryDeps??createDefaultWatchRegistryDeps(),record=await(overrides.readRecord??readExecutorJobLogRecord)(parsed.jobId,registryDeps);return record.ok?await(overrides.logExists??defaultLogExists)(record.record.log_path)?(overrides.spawnTail??(logPath=>spawnTailFollow(logPath)))(record.record.log_path):(errorLog(`Error: worker log for job ${parsed.jobId} is missing at ${record.record.log_path}`),1):(errorLog(`Error: no worker log registered for job ${parsed.jobId}: ${record.reason}`),1)}var DEFAULT_POLL_INTERVAL_MS=15e3,DEFAULT_HEARTBEAT_INTERVAL_MS=6e4,DEFAULT_DEADMAN_MS=12e4,DEFAULT_TERM_GRACE_MS=1e4,DEFAULT_BASE_BRANCH="main",DEFAULT_JOB_TIMEOUT_SECONDS=1200,DEFAULT_MAX_CONCURRENT=1;function getExecutorUsage(){return["Usage: mcp-server executor --repo <name> [--repo <name> ...] [options]","","Runs the Epic Conductor v2 local executor: poll \u2192 claim \u2192 spawn \u2192 heartbeat.","","Options:"," --repo <name> Repo to serve (repeatable)."," --repos=<a,b> Comma-separated repos."," --executor-id <id> Stable executor id (default: <hostname>-<pid>)."," --max-concurrent <n> Max concurrent jobs (>= 1, default 1)."," --once Run a single preflight/claim cycle and exit."," --poll-interval-ms <n> Poll interval (default 15000)."," --heartbeat-interval-ms <n> Heartbeat interval (default 60000)."," --deadman-ms <n> Dead-man self-kill window (default 120000)."," --base-branch <name> Base branch (default main)."," --no-advisory-parser Disable the advisory stream-json parser."," -h, --help Show this help."].join(`
|
|
4680
4718
|
`)}function parseIntArg(value,flag){if(value===void 0)return{ok:!1,message:`${flag} requires a numeric value`};let n=Number(value);return!Number.isFinite(n)||!Number.isInteger(n)?{ok:!1,message:`${flag} must be an integer`}:{ok:!0,value:n}}function parseExecutorArgs(argv,context){let repos=[],executorId,maxConcurrent=DEFAULT_MAX_CONCURRENT,once=!1,pollIntervalMs=DEFAULT_POLL_INTERVAL_MS,heartbeatIntervalMs=DEFAULT_HEARTBEAT_INTERVAL_MS,deadmanMs=DEFAULT_DEADMAN_MS,baseBranch=DEFAULT_BASE_BRANCH,advisoryParserEnabled=!0;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--help"||arg==="-h")return{kind:"help"};if(arg==="--repo"){let v=argv[++i];if(!v)return{kind:"error",message:"--repo requires a value"};repos.push(v)}else if(arg.startsWith("--repos="))repos.push(...arg.slice(8).split(",").map(s=>s.trim()).filter(Boolean));else if(arg==="--repos"){let v=argv[++i];if(!v)return{kind:"error",message:"--repos requires a value"};repos.push(...v.split(",").map(s=>s.trim()).filter(Boolean))}else if(arg==="--executor-id"){if(executorId=argv[++i],!executorId)return{kind:"error",message:"--executor-id requires a value"}}else if(arg==="--max-concurrent"){let r=parseIntArg(argv[++i],"--max-concurrent");if(!r.ok)return{kind:"error",message:r.message};maxConcurrent=r.value}else if(arg==="--once")once=!0;else if(arg==="--poll-interval-ms"){let r=parseIntArg(argv[++i],"--poll-interval-ms");if(!r.ok)return{kind:"error",message:r.message};pollIntervalMs=r.value}else if(arg==="--heartbeat-interval-ms"){let r=parseIntArg(argv[++i],"--heartbeat-interval-ms");if(!r.ok)return{kind:"error",message:r.message};heartbeatIntervalMs=r.value}else if(arg==="--deadman-ms"){let r=parseIntArg(argv[++i],"--deadman-ms");if(!r.ok)return{kind:"error",message:r.message};deadmanMs=r.value}else if(arg==="--base-branch"){let v=argv[++i];if(!v)return{kind:"error",message:"--base-branch requires a value"};baseBranch=v}else if(arg==="--no-advisory-parser")advisoryParserEnabled=!1;else return{kind:"error",message:`unknown argument: ${arg}`}}return repos.length===0?{kind:"error",message:"at least one --repo (or --repos=a,b) is required"}:maxConcurrent<1?{kind:"error",message:"--max-concurrent must be >= 1"}:{kind:"ok",options:{executorId:executorId&&executorId.trim().length>0?executorId.trim():`${context.hostname}-${context.pid}`,repos,repoName:repos[0],maxConcurrent,pollIntervalMs,heartbeatIntervalMs,deadmanMs,termGraceMs:DEFAULT_TERM_GRACE_MS,once,worktrunkBinary:resolveWorktrunkBinary(context.platform,context.env),baseBranch,advisoryParserEnabled,defaultJobTimeoutSeconds:DEFAULT_JOB_TIMEOUT_SECONDS}}}function hasRepoFlag(argv){return argv.some(a=>a==="--repo"||a==="--repos"||a.startsWith("--repos="))}async function runExecutorCli(argv,overrides={}){let errorLog=overrides.errorLog??(m=>console.error(m));if(argv[0]==="watch")return runExecutorWatchCli(argv.slice(1));let deps=overrides.deps??createDefaultExecutorDeps(),context=overrides.context??{hostname:os11.hostname(),pid:process.pid,platform:process.platform,env:process.env},effectiveArgv=argv;if(!hasRepoFlag(argv)){let fallback=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});fallback&&(effectiveArgv=["--repo",fallback,...argv])}let parsed=parseExecutorArgs(effectiveArgv,context);if(parsed.kind==="help")return errorLog(getExecutorUsage()),0;if(parsed.kind==="error")return errorLog(`Error: ${parsed.message}
|
|
4681
4719
|
|
|
4682
|
-
${getExecutorUsage()}`),1;let options=parsed.options,resolveApi=overrides.resolveApiAccess??resolveExecutorApiAccess,apiKeyByRepo={},baseUrl="";for(let repo of options.repos){let access2=await resolveApi(repo,deps);if(!access2.ok)return errorLog(`Error: ${access2.error}`),1;apiKeyByRepo[repo]=access2.apiKey,baseUrl=access2.baseUrl}let httpClient=(overrides.createHttpClient??createExecutorHttpClient)({baseUrl,apiKey:apiKeyByRepo[options.repoName],apiKeyByRepo,mcpVersion:VERSION,fetch:deps.fetch}),run=overrides.runExecutor??runExecutor;try{return await run(options,deps,httpClient)}catch(err){let message=err instanceof Error?err.message:String(err);return errorLog(`Error: executor exited unexpectedly: ${message.slice(0,200)}`),1}}init_bridge_api_client();init_plan();import{readFile as fsReadFile,stat as fsStat}from"node:fs/promises";import os12 from"node:os";function createDefaultSetupEpicDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os12.homedir,readFile:p=>fsReadFile(p,"utf-8"),stat:p=>fsStat(p),fetch:globalThis.fetch,log:m=>console.log(m),errorLog:m=>console.error(m)}}function getSetupEpicUsage(){return["Usage: mcp-server setup-epic --epic-key <KEY> --plan-file <path> [options]","","Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,","and approves it. Idempotent \u2014 re-running reuses an existing live run.","","Required:"," --epic-key <KEY> Jira epic key (e.g. BAPI-405)"," --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)","","Options:"," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," --plan-version <n> Assert the sidecar's plan_version equals <n>"," --dry-run Validate and preview; make no mutating calls"," --json Emit a single JSON result object on stdout"," -h, --help Show this help","","After setup, the server-side reconciler picks the run up within ~30s.","To execute claimed jobs on this machine, run:"," npx -y @bridge_gpt/mcp-server executor --repo <name>"].join(`
|
|
4683
|
-
`)}function takeValue2(argv,i,flag){let next=argv[i+1];return next===void 0||next.startsWith("-")?null:next}function parseSetupEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getSetupEpicUsage()};let epicKey,planFile,repo,planVersion,dryRun=!1,json=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];switch(arg){case"--epic-key":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--epic-key requires a value."};epicKey=v,i++;break}case"--plan-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-file requires a value."};planFile=v,i++;break}case"--repo":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--repo requires a value."};repo=v,i++;break}case"--plan-version":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-version requires a value."};if(!/^\d+$/.test(v))return{status:"error",message:`--plan-version must be a positive integer, got '${v}'.`};if(planVersion=Number(v),planVersion<1)return{status:"error",message:"--plan-version must be >= 1."};i++;break}case"--dry-run":dryRun=!0;break;case"--json":json=!0;break;default:return{status:"error",message:`Unknown argument '${arg}'. Run "setup-epic --help" for usage.`}}}return epicKey?planFile?{status:"ok",options:{epicKey,planFile,repo,planVersion,dryRun,json}}:{status:"error",message:"setup-epic requires --plan-file <path>."}:{status:"error",message:"setup-epic requires --epic-key <KEY>."}}function validateEpicPlanSidecar(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,error:"Plan sidecar must be a JSON object."};let plan=parsed,version=plan.plan_version;if(typeof version!="number"||!Number.isInteger(version)||version<1)return{ok:!1,error:`plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`};if(!Array.isArray(plan.nodes)||plan.nodes.length===0)return{ok:!1,error:"plan.nodes must be a non-empty array."};if(!Array.isArray(plan.edges))return{ok:!1,error:"plan.edges must be an array (use [] for none)."};let keys=new Set,warnings=[];for(let node of plan.nodes){if(!node||typeof node!="object")return{ok:!1,error:"Every plan node must be an object."};let key=typeof node.ticket_key=="string"?node.ticket_key.trim():"";if(!key)return{ok:!1,error:"Every plan node needs a non-empty ticket_key."};if(keys.has(key))return{ok:!1,error:`Duplicate ticket_key in plan: ${key}.`};keys.add(key),node.touched_files===void 0&&warnings.push(`Node ${key} has no touched_files. File-overlap serialization cannot protect it; if the repo has that flag on, the server will reject this plan.`)}let adjacency=new Map,addEdge=(from,to)=>{let list=adjacency.get(from)??[];list.push(to),adjacency.set(from,list)};for(let node of plan.nodes){let deps=Array.isArray(node.depends_on)?node.depends_on:[];for(let dep of deps){if(!keys.has(dep))return{ok:!1,error:`Node ${node.ticket_key} depends_on unknown ticket '${dep}'.`};addEdge(dep,node.ticket_key)}}for(let edge of plan.edges){if(!edge||typeof edge!="object")return{ok:!1,error:"Every plan edge must be an object."};if(!keys.has(edge.from)||!keys.has(edge.to))return{ok:!1,error:`Edge ${JSON.stringify(edge.from)} -> ${JSON.stringify(edge.to)} references an unknown ticket.`};addEdge(edge.from,edge.to)}let cycle=findCycle(keys,adjacency);return cycle?{ok:!1,error:`Plan DAG has a cycle: ${cycle.join(" -> ")}.`}:{ok:!0,plan:parsed,warnings}}function findCycle(keys,adjacency){let color=new Map;for(let k of keys)color.set(k,0);for(let start of keys){if(color.get(start)!==0)continue;let stack=[{node:start,path:[start]}];for(;stack.length>0;){let{node,path:path37}=stack[stack.length-1];if(color.get(node)===0){color.set(node,1);for(let next of adjacency.get(node)??[]){if(color.get(next)===1)return[...path37,next];color.get(next)===0&&stack.push({node:next,path:[...path37,next]})}}else color.get(node)===1&&color.set(node,2),stack.pop()}}return null}function errorDetail(err){if(err instanceof ConductorBridgeApiError){let status=err.status!==void 0?` (HTTP ${err.status})`:"",preview=err.bodyPreview?`: ${err.bodyPreview}`:"";return`${err.message}${status}${preview}`}return err instanceof Error?err.message:String(err)}async function runSetupEpicCli(argv,overrides={}){let deps={...createDefaultSetupEpicDeps(),...overrides},parsed=parseSetupEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),deps.errorLog(""),deps.errorLog(getSetupEpicUsage()),1;let opts=parsed.options,say=opts.json?deps.errorLog:deps.log,raw;try{raw=await deps.readFile(opts.planFile)}catch(err){return deps.errorLog(`Could not read plan file '${opts.planFile}': ${errorDetail(err)}`),1}let parsedJson;try{parsedJson=JSON.parse(raw)}catch(err){return deps.errorLog(`Plan file '${opts.planFile}' is not valid JSON: ${errorDetail(err)}`),1}let validation=validateEpicPlanSidecar(parsedJson);if(!validation.ok)return deps.errorLog(`Invalid plan DAG: ${validation.error}`),1;let plan=validation.plan,warnings=[...validation.warnings];if(opts.planVersion!==void 0&&opts.planVersion!==plan.plan_version)return deps.errorLog(`--plan-version ${opts.planVersion} does not match the sidecar's plan_version ${plan.plan_version}. Fix the sidecar (or drop the flag) \u2014 setup-epic never rewrites the blob, because that would change its hash.`),1;let localHash=hashPlan(plan),accessResult=await resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,repoName:opts.repo});if(!accessResult.ok)return deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`),1;let access2=accessResult.access;say(`Epic: ${opts.epicKey}`),say(`Repo: ${access2.repoName}`),say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`),say(`Local hash: ${localHash}`);for(let w of warnings)say(` [warn] ${w}`);let existingRunId=null,existingStatus=null;try{let state=await fetchEpicRunState(access2,opts.epicKey,deps.fetch);existingRunId=state.epic_run?.epic_run_id??null,existingStatus=state.epic_run?.status??null}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===404)existingRunId=null;else return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 it is wedged, and every plan call will keep failing. Abandon the duplicate before retrying:
|
|
4720
|
+
${getExecutorUsage()}`),1;let options=parsed.options,resolveApi=overrides.resolveApiAccess??resolveExecutorApiAccess,apiKeyByRepo={},baseUrl="";for(let repo of options.repos){let access2=await resolveApi(repo,deps);if(!access2.ok)return errorLog(`Error: ${access2.error}`),1;apiKeyByRepo[repo]=access2.apiKey,baseUrl=access2.baseUrl}let httpClient=(overrides.createHttpClient??createExecutorHttpClient)({baseUrl,apiKey:apiKeyByRepo[options.repoName],apiKeyByRepo,mcpVersion:VERSION,fetch:deps.fetch}),run=overrides.runExecutor??runExecutor;try{return await run(options,deps,httpClient)}catch(err){let message=err instanceof Error?err.message:String(err);return errorLog(`Error: executor exited unexpectedly: ${message.slice(0,200)}`),1}}init_bridge_api_client();init_base_ref();init_plan();import{readFile as fsReadFile,stat as fsStat}from"node:fs/promises";import os12 from"node:os";import readline from"node:readline";function defaultPromptLine(promptText){return new Promise(resolve2=>{let rl=readline.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 createDefaultSetupEpicDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os12.homedir,readFile:p=>fsReadFile(p,"utf-8"),stat:p=>fsStat(p),fetch:globalThis.fetch,log:m=>console.log(m),errorLog:m=>console.error(m),isTTY:!!process.stdin.isTTY,promptLine:defaultPromptLine}}function getSetupEpicUsage(){return["Usage: mcp-server setup-epic --epic-key <KEY> --plan-file <path> [options]","","Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,","and approves it. Idempotent \u2014 re-running reuses an existing live run.","","Required:"," --epic-key <KEY> Jira epic key (e.g. BAPI-405)"," --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)","","Options:"," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," --plan-version <n> Assert the sidecar's plan_version equals <n>"," --feature-branch <name> Run the epic on a dedicated feature branch."," The branch is created from the repository base branch"," on origin, and every child-ticket PR targets it."," Omit (the default) to continue on the repository base"," branch. Interactive runs are offered a proposal."," --dry-run Validate and preview; make no mutating calls"," --json Emit a single JSON result object on stdout"," -h, --help Show this help","","After setup, the server-side reconciler picks the run up within ~30s.","To execute claimed jobs on this machine, run:"," npx -y @bridge_gpt/mcp-server executor --repo <name>"].join(`
|
|
4721
|
+
`)}function takeValue2(argv,i,flag){let next=argv[i+1];return next===void 0||next.startsWith("-")?null:next}function parseFeatureBranchValue(raw){let trimmed=raw.trim();if(trimmed==="")return{ok:!0,value:void 0};let reason=validateBranchName(trimmed);return reason?{ok:!1,error:`Invalid --feature-branch value: ${reason}`}:{ok:!0,value:trimmed}}function parseSetupEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getSetupEpicUsage()};let epicKey,planFile,repo,planVersion,featureBranch,dryRun=!1,json=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg.startsWith("--feature-branch=")){let parsedFb=parseFeatureBranchValue(arg.slice(17));if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value;continue}switch(arg){case"--feature-branch":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--feature-branch requires a value."};let parsedFb=parseFeatureBranchValue(v);if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value,i++;break}case"--epic-key":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--epic-key requires a value."};epicKey=v,i++;break}case"--plan-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-file requires a value."};planFile=v,i++;break}case"--repo":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--repo requires a value."};repo=v,i++;break}case"--plan-version":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-version requires a value."};if(!/^\d+$/.test(v))return{status:"error",message:`--plan-version must be a positive integer, got '${v}'.`};if(planVersion=Number(v),planVersion<1)return{status:"error",message:"--plan-version must be >= 1."};i++;break}case"--dry-run":dryRun=!0;break;case"--json":json=!0;break;default:return{status:"error",message:`Unknown argument '${arg}'. Run "setup-epic --help" for usage.`}}}return epicKey?planFile?{status:"ok",options:{epicKey,planFile,repo,planVersion,featureBranch,dryRun,json}}:{status:"error",message:"setup-epic requires --plan-file <path>."}:{status:"error",message:"setup-epic requires --epic-key <KEY>."}}function validateEpicPlanSidecar(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,error:"Plan sidecar must be a JSON object."};let plan=parsed,version=plan.plan_version;if(typeof version!="number"||!Number.isInteger(version)||version<1)return{ok:!1,error:`plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`};if(!Array.isArray(plan.nodes)||plan.nodes.length===0)return{ok:!1,error:"plan.nodes must be a non-empty array."};if(!Array.isArray(plan.edges))return{ok:!1,error:"plan.edges must be an array (use [] for none)."};let keys=new Set,warnings=[];for(let node of plan.nodes){if(!node||typeof node!="object")return{ok:!1,error:"Every plan node must be an object."};let key=typeof node.ticket_key=="string"?node.ticket_key.trim():"";if(!key)return{ok:!1,error:"Every plan node needs a non-empty ticket_key."};if(keys.has(key))return{ok:!1,error:`Duplicate ticket_key in plan: ${key}.`};keys.add(key),node.touched_files===void 0&&warnings.push(`Node ${key} has no touched_files. File-overlap serialization cannot protect it; if the repo has that flag on, the server will reject this plan.`)}let adjacency=new Map,addEdge=(from,to)=>{let list=adjacency.get(from)??[];list.push(to),adjacency.set(from,list)};for(let node of plan.nodes){let deps=Array.isArray(node.depends_on)?node.depends_on:[];for(let dep of deps){if(!keys.has(dep))return{ok:!1,error:`Node ${node.ticket_key} depends_on unknown ticket '${dep}'.`};addEdge(dep,node.ticket_key)}}for(let edge of plan.edges){if(!edge||typeof edge!="object")return{ok:!1,error:"Every plan edge must be an object."};if(!keys.has(edge.from)||!keys.has(edge.to))return{ok:!1,error:`Edge ${JSON.stringify(edge.from)} -> ${JSON.stringify(edge.to)} references an unknown ticket.`};addEdge(edge.from,edge.to)}let cycle=findCycle(keys,adjacency);return cycle?{ok:!1,error:`Plan DAG has a cycle: ${cycle.join(" -> ")}.`}:{ok:!0,plan:parsed,warnings}}function findCycle(keys,adjacency){let color=new Map;for(let k of keys)color.set(k,0);for(let start of keys){if(color.get(start)!==0)continue;let stack=[{node:start,path:[start]}];for(;stack.length>0;){let{node,path:path37}=stack[stack.length-1];if(color.get(node)===0){color.set(node,1);for(let next of adjacency.get(node)??[]){if(color.get(next)===1)return[...path37,next];color.get(next)===0&&stack.push({node:next,path:[...path37,next]})}}else color.get(node)===1&&color.set(node,2),stack.pop()}}return null}function proposeFeatureBranchName(epicKey){return`epic/${epicKey}`}async function resolveFeatureBranchSelection(opts,repoName,deps){if(opts.featureBranch!==void 0)return opts.featureBranch;if(!deps.isTTY||opts.json)return;let proposed=proposeFeatureBranchName(opts.epicKey);for(deps.errorLog(""),deps.errorLog(`Feature branch (optional) for epic ${opts.epicKey} on ${repoName}:`),deps.errorLog(` Proposed: ${proposed}`),deps.errorLog(" Strategy: create a new branch from the repository base branch"),deps.errorLog(" Effect: every child-ticket PR will target this branch");;){let answer=(await deps.promptLine(`Use feature branch? 'y' = ${proposed}, a name = custom, Enter = base branch: `)).trim();if(answer==="")return;let lowered=answer.toLowerCase();if(lowered==="n"||lowered==="no")return;if(lowered==="y"||lowered==="yes")return proposed;let reason=validateBranchName(answer);if(reason){deps.errorLog(` Invalid branch name: ${reason} Try again, or press Enter for the base branch.`);continue}return answer}}function errorDetail(err){if(err instanceof ConductorBridgeApiError){let status=err.status!==void 0?` (HTTP ${err.status})`:"",preview=err.bodyPreview?`: ${err.bodyPreview}`:"";return`${err.message}${status}${preview}`}return err instanceof Error?err.message:String(err)}async function runSetupEpicCli(argv,overrides={}){let deps={...createDefaultSetupEpicDeps(),...overrides},parsed=parseSetupEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),deps.errorLog(""),deps.errorLog(getSetupEpicUsage()),1;let opts=parsed.options,say=opts.json?deps.errorLog:deps.log,raw;try{raw=await deps.readFile(opts.planFile)}catch(err){return deps.errorLog(`Could not read plan file '${opts.planFile}': ${errorDetail(err)}`),1}let parsedJson;try{parsedJson=JSON.parse(raw)}catch(err){return deps.errorLog(`Plan file '${opts.planFile}' is not valid JSON: ${errorDetail(err)}`),1}let validation=validateEpicPlanSidecar(parsedJson);if(!validation.ok)return deps.errorLog(`Invalid plan DAG: ${validation.error}`),1;let plan=validation.plan,warnings=[...validation.warnings];if(opts.planVersion!==void 0&&opts.planVersion!==plan.plan_version)return deps.errorLog(`--plan-version ${opts.planVersion} does not match the sidecar's plan_version ${plan.plan_version}. Fix the sidecar (or drop the flag) \u2014 setup-epic never rewrites the blob, because that would change its hash.`),1;let localHash=hashPlan(plan),accessResult=await resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,repoName:opts.repo});if(!accessResult.ok)return deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`),1;let access2=accessResult.access;say(`Epic: ${opts.epicKey}`),say(`Repo: ${access2.repoName}`),say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`),say(`Local hash: ${localHash}`);for(let w of warnings)say(` [warn] ${w}`);let promptedInteractively=opts.featureBranch===void 0&&deps.isTTY&&!opts.json,featureBranch=await resolveFeatureBranchSelection(opts,access2.repoName,deps);featureBranch!==void 0?say(`Feature: ${featureBranch} (create from repository base branch on origin)`):promptedInteractively&&say("Feature: none \u2014 continue using the repository base branch");let existingRunId=null,existingStatus=null,existingBaseBranch=null;try{let state=await fetchEpicRunState(access2,opts.epicKey,deps.fetch);existingRunId=state.epic_run?.epic_run_id??null,existingStatus=state.epic_run?.status??null;let existingPolicy=state.epic_run?.policy_json,existingBase=existingPolicy&&typeof existingPolicy=="object"?existingPolicy.base_branch:void 0;existingBaseBranch=typeof existingBase=="string"&&existingBase.trim()!==""?existingBase:null}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===404)existingRunId=null;else return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 it is wedged, and every plan call will keep failing. Abandon the duplicate before retrying:
|
|
4684
4722
|
PATCH /jira/epic-runs/runs/<epic_run_id> {"status": "abandoned"}
|
|
4685
4723
|
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate (which wedges the epic permanently). Refusing to continue.
|
|
4686
|
-
Detail: ${errorDetail(err)}`),1)}if(opts.dryRun)return say(""),say("[dry-run] No changes made. Would:"),say(existingRunId?` - reuse existing run ${existingRunId} (status: ${existingStatus})`:` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`),opts.json&&deps.log(JSON.stringify({dry_run:!0,epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,local_plan_hash:localHash,existing_run_id:existingRunId,warnings},null,2)),0;let result={epic_run_id:existingRunId??"",epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,plan_hash:null,local_plan_hash:localHash,status:existingStatus,run_created:!1,plan_stored:!1,plan_approved:!1,warnings};if(existingRunId)say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);else try{let
|
|
4687
|
-
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`),1)}let approval=await approveEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version},deps.fetch).catch(err=>(deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`),null));if(approval===null)return 1;if(approval.ok)result.plan_approved=!0,result.plan_hash=approval.plan_hash,result.status="active",say(`Plan: approved v${plan.plan_version}`);else{if(approval.reason==="multiple_active_runs")return deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`),1;{let msg="A later plan version is already approved \u2014 approval skipped.";result.warnings.push(msg),say(`Plan: [warn] ${msg}`)}}return result.plan_hash&&result.plan_hash!==localHash&&result.warnings.push("Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative."),opts.json?deps.log(JSON.stringify(result,null,2)):(say(""),say(`Epic run ${result.epic_run_id} is ${result.status??"unknown"}.`),say("The server-side reconciler will pick it up within ~30s."),say("To execute claimed jobs on this machine, run:"),say(` npx -y @bridge_gpt/mcp-server executor --repo ${access2.repoName}`)),0}import path24 from"node:path";import path23 from"node:path";var JIRA_KEY_RE=/^[A-Z][A-Z0-9]+-\d+$/,EPIC_SLUG_RE=/^[A-Za-z][A-Za-z0-9_-]*$/,PLACEHOLDER_RE=/TBD-\d+/;var SIBLING_TICKET_MANIFEST_SCHEMA_VERSION=1;var fail=error=>({ok:!1,error}),succeed=value=>({ok:!0,value});function normalizeTouchedFiles(input){if(!Array.isArray(input))return fail("touched_files must be an array of repository-relative paths.");let out=new Set;for(let item of input){let normalized=normalizeTouchedFileEntry(item);if(!normalized.ok)return normalized;out.add(normalized.value)}return succeed(Array.from(out).sort())}function normalizeTouchedFileEntry(item){if(typeof item!="string")return fail(`touched_files entries must be strings, got ${JSON.stringify(item)}.`);let trimmed=item.trim();if(trimmed.length===0)return fail("touched_files entries must not be blank.");if(/\s/.test(trimmed))return fail(`touched_files entry ${JSON.stringify(item)} contains whitespace \u2014 declare a concrete repository-relative file path, not prose.`);if(trimmed.startsWith("/")||trimmed.startsWith("\\")||/^[A-Za-z]:[\\/]/.test(trimmed))return fail(`touched_files entry ${JSON.stringify(item)} must be repository-relative, not absolute.`);if(trimmed.startsWith("./"))return fail(`touched_files entry ${JSON.stringify(item)} must not start with './' \u2014 the backend does not normalize it away before comparing paths.`);let posix=trimmed.replace(/\\/g,"/"),segments=posix.split("/");if(segments.some(s=>s===".."))return fail(`touched_files entry ${JSON.stringify(item)} must not contain '..' traversal.`);if(posix.endsWith("/"))return fail(`touched_files entry ${JSON.stringify(item)} looks like a directory. The backend intersects path strings exactly, so a directory protects nothing \u2014 declare each concrete file.`);if(posix.includes("*")||posix.includes("?"))return fail(`touched_files entry ${JSON.stringify(item)} looks like a glob. The backend intersects path strings exactly, so a glob never matches \u2014 declare each concrete file.`);if(/(^|\/)\.worktrees?(\/|$)/.test(posix)||posix.startsWith("tmp/"))return fail(`touched_files entry ${JSON.stringify(item)} points into a temporary worktree.`);let cleaned=segments.filter(s=>s!==""&&s!==".").join("/");return cleaned.length===0?fail(`touched_files entry ${JSON.stringify(item)} is not a usable path.`):succeed(cleaned)}async function validateConductorBundleInputs(args,fs7){let{epic_key,epic_slug,docs_dir,mappings}=args;if(typeof epic_key!="string"||!JIRA_KEY_RE.test(epic_key))return fail(`epic_key must match ${JIRA_KEY_RE.source}, got ${JSON.stringify(epic_key)}.`);if(typeof epic_slug!="string"||!EPIC_SLUG_RE.test(epic_slug))return fail(`epic_slug must match ${EPIC_SLUG_RE.source}, got ${JSON.stringify(epic_slug)}.`);if(!Array.isArray(mappings)||mappings.length===0)return fail("mappings must be a non-empty array of node\u2192ticket entries.");let seenNodes=new Set,seenKeys=new Set;for(let entry of mappings){if(!entry||typeof entry!="object")return fail("Every mapping entry must be an object.");let{plan_node_id,ticket_key}=entry;if(typeof plan_node_id!="string"||plan_node_id.trim().length===0)return fail("Every mapping entry needs a non-empty plan_node_id.");if(typeof ticket_key!="string"||!JIRA_KEY_RE.test(ticket_key))return fail(`Mapping for ${plan_node_id} has an invalid ticket_key ${JSON.stringify(ticket_key)}.`);if(ticket_key===epic_key)return fail(`Mapping for ${plan_node_id} uses the epic key ${epic_key} as a child ticket.`);if(seenNodes.has(plan_node_id))return fail(`Duplicate plan_node_id ${plan_node_id} in mappings.`);if(seenKeys.has(ticket_key))return fail(`Duplicate ticket_key ${ticket_key} in mappings.`);seenNodes.add(plan_node_id),seenKeys.add(ticket_key)}let sidecarNodes=readSidecarNodeKeys(args.sidecar);if(!sidecarNodes.ok)return fail(sidecarNodes.error);let planVersion=readSidecarPlanVersion(args.sidecar);if(!planVersion.ok)return fail(planVersion.error);let coverage=checkMappingCoversSidecar(mappings,sidecarNodes.value);if(!coverage.ok)return fail(coverage.error);if(args.existing_manifest!==void 0&&args.existing_manifest!==null){let agreement=checkManifestAgreement(args.existing_manifest,{epic_key,epic_slug,plan_version:planVersion.value,decomposition_fingerprint:args.decomposition_fingerprint,mappings});if(!agreement.ok)return fail(agreement.error)}let epicDir=path23.resolve(docs_dir,"epic-plans",epic_slug),resolvedEpicDir=await canonicalize2(epicDir,fs7);if(!resolvedEpicDir.ok)return fail(resolvedEpicDir.error);let resolvedMappings=[];for(let entry of mappings){let exploration=await resolveInsideEpicDir(entry.exploration_path,resolvedEpicDir.value,fs7,`${entry.plan_node_id} exploration_path`);if(!exploration.ok)return fail(exploration.error);let draft=await resolveInsideEpicDir(entry.draft_path,resolvedEpicDir.value,fs7,`${entry.plan_node_id} draft_path`);if(!draft.ok)return fail(draft.error);resolvedMappings.push({...entry,resolved_exploration_path:exploration.value,resolved_draft_path:draft.value})}return succeed({epic_key,epic_slug,epic_dir:resolvedEpicDir.value,goals_path:path23.join(resolvedEpicDir.value,"goals-and-nfrs.md"),epic_plan_path:path23.join(resolvedEpicDir.value,"epic-plan.md"),sidecar_path:path23.join(resolvedEpicDir.value,"epic-plan.dag.json"),manifest_path:path23.join(resolvedEpicDir.value,"sibling-ticket-manifest.json"),report_path:path23.join(resolvedEpicDir.value,"harmonization-report.json"),plan_version:planVersion.value,mappings:resolvedMappings})}function readSidecarNodeKeys(sidecar){if(!sidecar||typeof sidecar!="object")return fail("epic-plan.dag.json must parse to an object.");let nodes=sidecar.nodes;if(!Array.isArray(nodes)||nodes.length===0)return fail("epic-plan.dag.json must have a non-empty nodes array.");let keys=[];for(let node of nodes){if(!node||typeof node!="object")return fail("Every epic-plan.dag.json node must be an object.");let key=node.ticket_key;if(typeof key!="string"||key.trim().length===0)return fail("Every epic-plan.dag.json node needs a non-empty ticket_key.");keys.push(key.trim())}return new Set(keys).size!==keys.length?fail("epic-plan.dag.json has duplicate node ticket_key values."):succeed(keys)}function readSidecarPlanVersion(sidecar){let version=sidecar.plan_version;return typeof version!="number"||!Number.isInteger(version)||version<1?fail(`epic-plan.dag.json plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`):succeed(version)}function checkMappingCoversSidecar(mappings,nodeKeys){let nodeSet=new Set(nodeKeys);for(let entry of mappings){let matchesPlaceholder=nodeSet.has(entry.plan_node_id),matchesFinalized=nodeSet.has(entry.ticket_key);if(!matchesPlaceholder&&!matchesFinalized)return fail(`Mapping plan_node_id ${entry.plan_node_id} matches no node in epic-plan.dag.json.`)}let covered=new Set;for(let entry of mappings)nodeSet.has(entry.plan_node_id)?covered.add(entry.plan_node_id):covered.add(entry.ticket_key);let uncovered=nodeKeys.filter(k=>!covered.has(k));return uncovered.length>0?fail(`epic-plan.dag.json node(s) ${uncovered.join(", ")} have no mapping entry. Every node must be mapped before any mutation.`):succeed(!0)}function checkManifestAgreement(manifest,expected){if(!manifest||typeof manifest!="object")return fail("sibling-ticket-manifest.json must parse to an object.");let m=manifest;if(m.epic_key!==expected.epic_key)return fail(`Manifest epic_key ${JSON.stringify(m.epic_key)} disagrees with the supplied ${expected.epic_key}. Refusing to reuse another epic's mapping.`);if(m.epic_slug!==expected.epic_slug)return fail(`Manifest epic_slug ${JSON.stringify(m.epic_slug)} disagrees with the supplied ${expected.epic_slug}.`);if(m.plan_version!==expected.plan_version)return fail(`Manifest plan_version ${JSON.stringify(m.plan_version)} disagrees with the sidecar's ${expected.plan_version}.`);if(m.decomposition_fingerprint!==expected.decomposition_fingerprint)return fail("Manifest decomposition_fingerprint disagrees with this decomposition. Refusing to reuse another decomposition's mapping.");if(!Array.isArray(m.mappings))return fail("Manifest mappings must be an array.");let recorded=new Map;for(let entry of m.mappings){if(!entry||typeof entry!="object")return fail("Every manifest mapping entry must be an object.");recorded.set(entry.plan_node_id,entry.ticket_key)}for(let entry of expected.mappings){let known=recorded.get(entry.plan_node_id);if(known!==void 0&&known!==entry.ticket_key)return fail(`Manifest maps ${entry.plan_node_id} to ${known}, but this invocation maps it to ${entry.ticket_key}. Halting rather than preferring either source.`)}return succeed(!0)}async function canonicalize2(target,fs7){try{return succeed(await fs7.realpath(target))}catch(err){return fail(`Cannot resolve ${target}: ${err instanceof Error?err.message:String(err)}`)}}async function resolveInsideEpicDir(candidate,epicDir,fs7,label){if(typeof candidate!="string"||candidate.trim().length===0)return fail(`${label} must be a non-empty path.`);let raw=candidate.trim();if(path23.isAbsolute(raw)||/^[A-Za-z]:[\\/]/.test(raw))return fail(`${label} must be relative to the epic directory, not absolute.`);if(raw.replace(/\\/g,"/").split("/").some(s=>s===".."))return fail(`${label} must not contain '..' traversal.`);let joined=path23.resolve(epicDir,raw),canonical=await canonicalize2(joined,fs7);return canonical.ok?isInside(canonical.value,epicDir)?succeed(canonical.value):fail(`${label} resolves outside the epic directory (symlink escape): ${canonical.value}`):fail(`${label} is missing or unreadable: ${canonical.error}`)}function isInside(target,dir){return target===dir?!0:target.startsWith(dir.endsWith(path23.sep)?dir:dir+path23.sep)}function finalizeEpicPlanSidecar(args){let{sidecar,node_key_map,touched_files_by_key}=args;if(!sidecar||typeof sidecar!="object"||Array.isArray(sidecar))return fail("epic-plan.dag.json must parse to an object.");if(args.plan_version_already_stored)return fail("This plan_version is already stored. Plan blobs are immutable and post-approval description rewrites are forbidden, so finalizing it now would invalidate the approved hash. This needs an explicit re-plan.");let plan=structuredClone(sidecar),versionCheck=readSidecarPlanVersion(plan);if(!versionCheck.ok)return fail(versionCheck.error);let nodes=plan.nodes;if(!Array.isArray(nodes)||nodes.length===0)return fail("epic-plan.dag.json must have a non-empty nodes array.");let edges=plan.edges;if(!Array.isArray(edges))return fail("epic-plan.dag.json edges must be an array (use [] for none).");for(let node of nodes)if("base_lineage"in node)return fail(`Node ${String(node.ticket_key)} declares base_lineage. It has no consumer and changes the plan hash for no behavioral gain; refusing to emit or silently remove it.`);let resolve2=value=>node_key_map[value]??value;for(let node of nodes){let originalKey=String(node.ticket_key),realKey=resolve2(originalKey);node.ticket_key=realKey;let dependsOn=node.depends_on;if(dependsOn!==void 0&&!Array.isArray(dependsOn))return fail(`Node ${realKey} depends_on must be an array.`);node.depends_on=(Array.isArray(dependsOn)?dependsOn:[]).map(d=>typeof d=="string"?resolve2(d):d);let touched=touched_files_by_key[realKey];if(touched===void 0)return fail(`Node ${realKey} has no touched_files entry. Ownership uncertainty must be escalated, never encoded as an empty array \u2014 an empty array silently disables file-overlap protection.`);let normalized=normalizeTouchedFiles(touched);if(!normalized.ok)return fail(`Node ${realKey}: ${normalized.error}`);node.touched_files=normalized.value}for(let edge of edges){if(!edge||typeof edge!="object")return fail("Every plan edge must be an object.");typeof edge.from=="string"&&(edge.from=resolve2(edge.from)),typeof edge.to=="string"&&(edge.to=resolve2(edge.to))}let residual=findResidualPlaceholder(plan);return residual?fail(`Residual placeholder ${residual} survives finalization. Every TBD- reference must resolve to a real key before the plan is stored.`):validateFinalizedGraph(plan,nodes,edges)}function findResidualPlaceholder(plan){let match=PLACEHOLDER_RE.exec(JSON.stringify(plan));return match?match[0]:null}function validateFinalizedGraph(plan,nodes,edges){let keys=nodes.map(n=>String(n.ticket_key)),keySet=new Set(keys);if(keySet.size!==keys.length)return fail("Finalized plan has duplicate ticket_key values.");let canonicalEdges=new Set;for(let node of nodes){let key=String(node.ticket_key);for(let dep of node.depends_on){if(typeof dep!="string"||!keySet.has(dep))return fail(`Node ${key} depends_on unknown ticket ${JSON.stringify(dep)}.`);if(dep===key)return fail(`Node ${key} depends on itself.`);canonicalEdges.add(`${dep} ${key}`)}}for(let edge of edges){let from=edge.from,to=edge.to;if(typeof from!="string"||!keySet.has(from))return fail(`Edge from ${JSON.stringify(from)} references an unknown ticket.`);if(typeof to!="string"||!keySet.has(to))return fail(`Edge to ${JSON.stringify(to)} references an unknown ticket.`);if(!(typeof edge.kind=="string"&&edge.kind.length>0)&&!canonicalEdges.has(`${from} ${to}`))return fail(`Edge ${from} -> ${to} contradicts the canonical depends_on graph. depends_on is authoritative; ordinary edges must encode the same graph.`)}let cycle=findCycle2(keys,canonicalEdges);return cycle?fail(`Finalized plan has a cycle: ${cycle}.`):succeed(plan)}function findCycle2(keys,edgeKeys){let adjacency=new Map;for(let key of keys)adjacency.set(key,[]);for(let edgeKey of edgeKeys){let[from,to]=edgeKey.split(" ");adjacency.get(from).push(to)}let WHITE=0,GREY=1,BLACK=2,color=new Map(keys.map(k=>[k,WHITE])),stack=[],visit=start=>{let frames=[{node:start,index:0}];for(color.set(start,GREY),stack.push(start);frames.length>0;){let frame=frames[frames.length-1],neighbors=adjacency.get(frame.node)??[];if(frame.index>=neighbors.length){color.set(frame.node,BLACK),stack.pop(),frames.pop();continue}let next=neighbors[frame.index++],state=color.get(next);if(state===GREY){let from=stack.indexOf(next);return[...stack.slice(from),next].join(" -> ")}state===WHITE&&(color.set(next,GREY),stack.push(next),frames.push({node:next,index:0}))}return null};for(let key of keys){if(color.get(key)!==WHITE)continue;let cycle=visit(key);if(cycle)return cycle}return null}function countSpecCharacters(text){let count=0;for(let _ of text)count++;return count}var SECRET_VALUE_PATTERNS=[/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi,/\bghp_[A-Za-z0-9]{20,}/g,/\bgithub_pat_[A-Za-z0-9_]{20,}/g,/\bsk-[A-Za-z0-9]{20,}/g,/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,/\bAKIA[0-9A-Z]{16}\b/g],REDACTED2="[REDACTED]";function sanitizeText(input){let out=input;for(let pattern of SECRET_VALUE_PATTERNS)out=out.replace(pattern,REDACTED2);return out}function bound(input,max){let sanitized=sanitizeText(input);return countSpecCharacters(sanitized)<=max?sanitized:Array.from(sanitized).slice(0,max).join("")+"\u2026[truncated]"}var FINDING_SUMMARY_MAX=600,FINDING_RESOLUTION_MAX=600;function serializeSiblingTicketManifest(candidate){return{schema_version:SIBLING_TICKET_MANIFEST_SCHEMA_VERSION,epic_key:candidate.epic_key,epic_slug:candidate.epic_slug,plan_version:candidate.plan_version,decomposition_fingerprint:candidate.decomposition_fingerprint,finalized_fingerprint:candidate.finalized_fingerprint??null,run_phase:candidate.run_phase,mappings:(candidate.mappings??[]).map(m=>({plan_node_id:m.plan_node_id,ticket_key:m.ticket_key,exploration_path:m.exploration_path,draft_path:m.draft_path})),decisions:(candidate.decisions??[]).map(d=>({finding_id:d.finding_id,chosen_option:bound(d.chosen_option,FINDING_SUMMARY_MAX),rationale:bound(d.rationale,FINDING_RESOLUTION_MAX)})),completed_mutations:(candidate.completed_mutations??[]).map(c=>({kind:c.kind,ticket_key:c.ticket_key,detail:bound(c.detail,FINDING_SUMMARY_MAX)}))}}async function writeJsonAtomically(destination,value,fs7){let dir=path23.dirname(destination),tempPath=path23.join(dir,`.${path23.basename(destination)}.tmp`),serialized=JSON.stringify(value,null,2)+`
|
|
4724
|
+
Detail: ${errorDetail(err)}`),1)}if(existingRunId&&featureBranch!==void 0&&existingBaseBranch!==featureBranch)return deps.errorLog(`Epic ${opts.epicKey} already has a live run (${existingRunId}) whose feature branch is ${existingBaseBranch?`'${existingBaseBranch}'`:"unset (base branch)"}, which conflicts with the requested '${featureBranch}'. setup-epic will not retarget or rebuild an existing run. Re-run without --feature-branch to reuse it unchanged, or abandon the run to start over on a new branch.`),1;if(opts.dryRun)return say(""),say("[dry-run] No changes made. Would:"),say(existingRunId?` - reuse existing run ${existingRunId} (status: ${existingStatus})`:` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`),featureBranch!==void 0&&say(` - feature branch: ${featureBranch} (create from repository base branch; no request made in dry-run)`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`),opts.json&&deps.log(JSON.stringify({dry_run:!0,epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,local_plan_hash:localHash,existing_run_id:existingRunId,...featureBranch!==void 0?{feature_branch:featureBranch}:{},warnings},null,2)),0;let result={epic_run_id:existingRunId??"",epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,plan_hash:null,local_plan_hash:localHash,status:existingStatus,run_created:!1,plan_stored:!1,plan_approved:!1,warnings};if(featureBranch!==void 0&&(result.feature_branch=featureBranch),existingRunId)say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);else try{let createRequest=featureBranch!==void 0?{epicKey:opts.epicKey,policyJson:{base_branch:featureBranch}}:{epicKey:opts.epicKey},run=await createEpicRun(access2,createRequest,deps.fetch);result.epic_run_id=run.epic_run_id,result.status=run.status,result.run_created=!0,say(`Run: created ${run.epic_run_id}`)}catch(err){return deps.errorLog(`Failed to create the epic run: ${errorDetail(err)}`),1}try{let stored=await storeEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version,planBlob:plan,planHash:localHash},deps.fetch);result.plan_stored=!0;let serverHash=stored?.plan_hash;typeof serverHash=="string"&&(result.plan_hash=serverHash),say(`Plan: stored v${plan.plan_version}`)}catch(err){return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Plan v${plan.plan_version} is already stored with a DIFFERENT hash. The stored blob is immutable \u2014 bump plan_version in the sidecar and re-run.
|
|
4725
|
+
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`),1)}featureBranch!==void 0&&say(`Branch: creating or validating ${featureBranch} on origin\u2026`);let approval=await approveEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version},deps.fetch).catch(err=>(featureBranch!==void 0&&err instanceof ConductorBridgeApiError&&err.errorCode==="FEATURE_BRANCH_PROVISIONING"?deps.errorLog(`Failed to provision the feature branch '${featureBranch}' \u2014 child-ticket dispatch has NOT started. Correct repository access or the branch configuration, then re-run setup-epic.
|
|
4726
|
+
Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`),null));if(approval===null)return 1;if(approval.ok){result.plan_approved=!0,result.plan_hash=approval.plan_hash,result.status="active",say(`Plan: approved v${plan.plan_version}`);let prov=approval.featureBranchProvisioning;prov&&(result.feature_branch_provisioning=prov,prov.status==="created"?say(`Branch: ready on origin \u2014 created '${prov.feature_branch}' from '${prov.source_branch}' at ${prov.source_sha}`):say(`Branch: '${prov.feature_branch}' already exists \u2014 validated, unchanged (the remote ref was not moved or reset); head ${prov.remote_head_sha}`))}else{if(approval.reason==="multiple_active_runs")return deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`),1;{let msg="A later plan version is already approved \u2014 approval skipped.";result.warnings.push(msg),say(`Plan: [warn] ${msg}`)}}return result.plan_hash&&result.plan_hash!==localHash&&result.warnings.push("Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative."),opts.json?deps.log(JSON.stringify(result,null,2)):(say(""),say(`Epic run ${result.epic_run_id} is ${result.status??"unknown"}.`),say("The server-side reconciler will pick it up within ~30s."),say("To execute claimed jobs on this machine, run:"),say(` npx -y @bridge_gpt/mcp-server executor --repo ${access2.repoName}`)),0}import path24 from"node:path";import path23 from"node:path";var JIRA_KEY_RE=/^[A-Z][A-Z0-9]+-\d+$/,EPIC_SLUG_RE=/^[A-Za-z][A-Za-z0-9_-]*$/,PLACEHOLDER_RE=/TBD-\d+/;var SIBLING_TICKET_MANIFEST_SCHEMA_VERSION=1;var fail=error=>({ok:!1,error}),succeed=value=>({ok:!0,value});function normalizeTouchedFiles(input){if(!Array.isArray(input))return fail("touched_files must be an array of repository-relative paths.");let out=new Set;for(let item of input){let normalized=normalizeTouchedFileEntry(item);if(!normalized.ok)return normalized;out.add(normalized.value)}return succeed(Array.from(out).sort())}function normalizeTouchedFileEntry(item){if(typeof item!="string")return fail(`touched_files entries must be strings, got ${JSON.stringify(item)}.`);let trimmed=item.trim();if(trimmed.length===0)return fail("touched_files entries must not be blank.");if(/\s/.test(trimmed))return fail(`touched_files entry ${JSON.stringify(item)} contains whitespace \u2014 declare a concrete repository-relative file path, not prose.`);if(trimmed.startsWith("/")||trimmed.startsWith("\\")||/^[A-Za-z]:[\\/]/.test(trimmed))return fail(`touched_files entry ${JSON.stringify(item)} must be repository-relative, not absolute.`);if(trimmed.startsWith("./"))return fail(`touched_files entry ${JSON.stringify(item)} must not start with './' \u2014 the backend does not normalize it away before comparing paths.`);let posix=trimmed.replace(/\\/g,"/"),segments=posix.split("/");if(segments.some(s=>s===".."))return fail(`touched_files entry ${JSON.stringify(item)} must not contain '..' traversal.`);if(posix.endsWith("/"))return fail(`touched_files entry ${JSON.stringify(item)} looks like a directory. The backend intersects path strings exactly, so a directory protects nothing \u2014 declare each concrete file.`);if(posix.includes("*")||posix.includes("?"))return fail(`touched_files entry ${JSON.stringify(item)} looks like a glob. The backend intersects path strings exactly, so a glob never matches \u2014 declare each concrete file.`);if(/(^|\/)\.worktrees?(\/|$)/.test(posix)||posix.startsWith("tmp/"))return fail(`touched_files entry ${JSON.stringify(item)} points into a temporary worktree.`);let cleaned=segments.filter(s=>s!==""&&s!==".").join("/");return cleaned.length===0?fail(`touched_files entry ${JSON.stringify(item)} is not a usable path.`):succeed(cleaned)}async function validateConductorBundleInputs(args,fs7){let{epic_key,epic_slug,docs_dir,mappings}=args;if(typeof epic_key!="string"||!JIRA_KEY_RE.test(epic_key))return fail(`epic_key must match ${JIRA_KEY_RE.source}, got ${JSON.stringify(epic_key)}.`);if(typeof epic_slug!="string"||!EPIC_SLUG_RE.test(epic_slug))return fail(`epic_slug must match ${EPIC_SLUG_RE.source}, got ${JSON.stringify(epic_slug)}.`);if(!Array.isArray(mappings)||mappings.length===0)return fail("mappings must be a non-empty array of node\u2192ticket entries.");let seenNodes=new Set,seenKeys=new Set;for(let entry of mappings){if(!entry||typeof entry!="object")return fail("Every mapping entry must be an object.");let{plan_node_id,ticket_key}=entry;if(typeof plan_node_id!="string"||plan_node_id.trim().length===0)return fail("Every mapping entry needs a non-empty plan_node_id.");if(typeof ticket_key!="string"||!JIRA_KEY_RE.test(ticket_key))return fail(`Mapping for ${plan_node_id} has an invalid ticket_key ${JSON.stringify(ticket_key)}.`);if(ticket_key===epic_key)return fail(`Mapping for ${plan_node_id} uses the epic key ${epic_key} as a child ticket.`);if(seenNodes.has(plan_node_id))return fail(`Duplicate plan_node_id ${plan_node_id} in mappings.`);if(seenKeys.has(ticket_key))return fail(`Duplicate ticket_key ${ticket_key} in mappings.`);seenNodes.add(plan_node_id),seenKeys.add(ticket_key)}let sidecarNodes=readSidecarNodeKeys(args.sidecar);if(!sidecarNodes.ok)return fail(sidecarNodes.error);let planVersion=readSidecarPlanVersion(args.sidecar);if(!planVersion.ok)return fail(planVersion.error);let coverage=checkMappingCoversSidecar(mappings,sidecarNodes.value);if(!coverage.ok)return fail(coverage.error);if(args.existing_manifest!==void 0&&args.existing_manifest!==null){let agreement=checkManifestAgreement(args.existing_manifest,{epic_key,epic_slug,plan_version:planVersion.value,decomposition_fingerprint:args.decomposition_fingerprint,mappings});if(!agreement.ok)return fail(agreement.error)}let epicDir=path23.resolve(docs_dir,"epic-plans",epic_slug),resolvedEpicDir=await canonicalize2(epicDir,fs7);if(!resolvedEpicDir.ok)return fail(resolvedEpicDir.error);let resolvedMappings=[];for(let entry of mappings){let exploration=await resolveInsideEpicDir(entry.exploration_path,resolvedEpicDir.value,fs7,`${entry.plan_node_id} exploration_path`);if(!exploration.ok)return fail(exploration.error);let draft=await resolveInsideEpicDir(entry.draft_path,resolvedEpicDir.value,fs7,`${entry.plan_node_id} draft_path`);if(!draft.ok)return fail(draft.error);resolvedMappings.push({...entry,resolved_exploration_path:exploration.value,resolved_draft_path:draft.value})}return succeed({epic_key,epic_slug,epic_dir:resolvedEpicDir.value,goals_path:path23.join(resolvedEpicDir.value,"goals-and-nfrs.md"),epic_plan_path:path23.join(resolvedEpicDir.value,"epic-plan.md"),sidecar_path:path23.join(resolvedEpicDir.value,"epic-plan.dag.json"),manifest_path:path23.join(resolvedEpicDir.value,"sibling-ticket-manifest.json"),report_path:path23.join(resolvedEpicDir.value,"harmonization-report.json"),plan_version:planVersion.value,mappings:resolvedMappings})}function readSidecarNodeKeys(sidecar){if(!sidecar||typeof sidecar!="object")return fail("epic-plan.dag.json must parse to an object.");let nodes=sidecar.nodes;if(!Array.isArray(nodes)||nodes.length===0)return fail("epic-plan.dag.json must have a non-empty nodes array.");let keys=[];for(let node of nodes){if(!node||typeof node!="object")return fail("Every epic-plan.dag.json node must be an object.");let key=node.ticket_key;if(typeof key!="string"||key.trim().length===0)return fail("Every epic-plan.dag.json node needs a non-empty ticket_key.");keys.push(key.trim())}return new Set(keys).size!==keys.length?fail("epic-plan.dag.json has duplicate node ticket_key values."):succeed(keys)}function readSidecarPlanVersion(sidecar){let version=sidecar.plan_version;return typeof version!="number"||!Number.isInteger(version)||version<1?fail(`epic-plan.dag.json plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`):succeed(version)}function checkMappingCoversSidecar(mappings,nodeKeys){let nodeSet=new Set(nodeKeys);for(let entry of mappings){let matchesPlaceholder=nodeSet.has(entry.plan_node_id),matchesFinalized=nodeSet.has(entry.ticket_key);if(!matchesPlaceholder&&!matchesFinalized)return fail(`Mapping plan_node_id ${entry.plan_node_id} matches no node in epic-plan.dag.json.`)}let covered=new Set;for(let entry of mappings)nodeSet.has(entry.plan_node_id)?covered.add(entry.plan_node_id):covered.add(entry.ticket_key);let uncovered=nodeKeys.filter(k=>!covered.has(k));return uncovered.length>0?fail(`epic-plan.dag.json node(s) ${uncovered.join(", ")} have no mapping entry. Every node must be mapped before any mutation.`):succeed(!0)}function checkManifestAgreement(manifest,expected){if(!manifest||typeof manifest!="object")return fail("sibling-ticket-manifest.json must parse to an object.");let m=manifest;if(m.epic_key!==expected.epic_key)return fail(`Manifest epic_key ${JSON.stringify(m.epic_key)} disagrees with the supplied ${expected.epic_key}. Refusing to reuse another epic's mapping.`);if(m.epic_slug!==expected.epic_slug)return fail(`Manifest epic_slug ${JSON.stringify(m.epic_slug)} disagrees with the supplied ${expected.epic_slug}.`);if(m.plan_version!==expected.plan_version)return fail(`Manifest plan_version ${JSON.stringify(m.plan_version)} disagrees with the sidecar's ${expected.plan_version}.`);if(m.decomposition_fingerprint!==expected.decomposition_fingerprint)return fail("Manifest decomposition_fingerprint disagrees with this decomposition. Refusing to reuse another decomposition's mapping.");if(!Array.isArray(m.mappings))return fail("Manifest mappings must be an array.");let recorded=new Map;for(let entry of m.mappings){if(!entry||typeof entry!="object")return fail("Every manifest mapping entry must be an object.");recorded.set(entry.plan_node_id,entry.ticket_key)}for(let entry of expected.mappings){let known=recorded.get(entry.plan_node_id);if(known!==void 0&&known!==entry.ticket_key)return fail(`Manifest maps ${entry.plan_node_id} to ${known}, but this invocation maps it to ${entry.ticket_key}. Halting rather than preferring either source.`)}return succeed(!0)}async function canonicalize2(target,fs7){try{return succeed(await fs7.realpath(target))}catch(err){return fail(`Cannot resolve ${target}: ${err instanceof Error?err.message:String(err)}`)}}async function resolveInsideEpicDir(candidate,epicDir,fs7,label){if(typeof candidate!="string"||candidate.trim().length===0)return fail(`${label} must be a non-empty path.`);let raw=candidate.trim();if(path23.isAbsolute(raw)||/^[A-Za-z]:[\\/]/.test(raw))return fail(`${label} must be relative to the epic directory, not absolute.`);if(raw.replace(/\\/g,"/").split("/").some(s=>s===".."))return fail(`${label} must not contain '..' traversal.`);let joined=path23.resolve(epicDir,raw),canonical=await canonicalize2(joined,fs7);return canonical.ok?isInside(canonical.value,epicDir)?succeed(canonical.value):fail(`${label} resolves outside the epic directory (symlink escape): ${canonical.value}`):fail(`${label} is missing or unreadable: ${canonical.error}`)}function isInside(target,dir){return target===dir?!0:target.startsWith(dir.endsWith(path23.sep)?dir:dir+path23.sep)}function finalizeEpicPlanSidecar(args){let{sidecar,node_key_map,touched_files_by_key}=args;if(!sidecar||typeof sidecar!="object"||Array.isArray(sidecar))return fail("epic-plan.dag.json must parse to an object.");if(args.plan_version_already_stored)return fail("This plan_version is already stored. Plan blobs are immutable and post-approval description rewrites are forbidden, so finalizing it now would invalidate the approved hash. This needs an explicit re-plan.");let plan=structuredClone(sidecar),versionCheck=readSidecarPlanVersion(plan);if(!versionCheck.ok)return fail(versionCheck.error);let nodes=plan.nodes;if(!Array.isArray(nodes)||nodes.length===0)return fail("epic-plan.dag.json must have a non-empty nodes array.");let edges=plan.edges;if(!Array.isArray(edges))return fail("epic-plan.dag.json edges must be an array (use [] for none).");for(let node of nodes)if("base_lineage"in node)return fail(`Node ${String(node.ticket_key)} declares base_lineage. It has no consumer and changes the plan hash for no behavioral gain; refusing to emit or silently remove it.`);let resolve2=value=>node_key_map[value]??value;for(let node of nodes){let originalKey=String(node.ticket_key),realKey=resolve2(originalKey);node.ticket_key=realKey;let dependsOn=node.depends_on;if(dependsOn!==void 0&&!Array.isArray(dependsOn))return fail(`Node ${realKey} depends_on must be an array.`);node.depends_on=(Array.isArray(dependsOn)?dependsOn:[]).map(d=>typeof d=="string"?resolve2(d):d);let touched=touched_files_by_key[realKey];if(touched===void 0)return fail(`Node ${realKey} has no touched_files entry. Ownership uncertainty must be escalated, never encoded as an empty array \u2014 an empty array silently disables file-overlap protection.`);let normalized=normalizeTouchedFiles(touched);if(!normalized.ok)return fail(`Node ${realKey}: ${normalized.error}`);node.touched_files=normalized.value}for(let edge of edges){if(!edge||typeof edge!="object")return fail("Every plan edge must be an object.");typeof edge.from=="string"&&(edge.from=resolve2(edge.from)),typeof edge.to=="string"&&(edge.to=resolve2(edge.to))}let residual=findResidualPlaceholder(plan);return residual?fail(`Residual placeholder ${residual} survives finalization. Every TBD- reference must resolve to a real key before the plan is stored.`):validateFinalizedGraph(plan,nodes,edges)}function findResidualPlaceholder(plan){let match=PLACEHOLDER_RE.exec(JSON.stringify(plan));return match?match[0]:null}function validateFinalizedGraph(plan,nodes,edges){let keys=nodes.map(n=>String(n.ticket_key)),keySet=new Set(keys);if(keySet.size!==keys.length)return fail("Finalized plan has duplicate ticket_key values.");let canonicalEdges=new Set;for(let node of nodes){let key=String(node.ticket_key);for(let dep of node.depends_on){if(typeof dep!="string"||!keySet.has(dep))return fail(`Node ${key} depends_on unknown ticket ${JSON.stringify(dep)}.`);if(dep===key)return fail(`Node ${key} depends on itself.`);canonicalEdges.add(`${dep} ${key}`)}}for(let edge of edges){let from=edge.from,to=edge.to;if(typeof from!="string"||!keySet.has(from))return fail(`Edge from ${JSON.stringify(from)} references an unknown ticket.`);if(typeof to!="string"||!keySet.has(to))return fail(`Edge to ${JSON.stringify(to)} references an unknown ticket.`);if(!(typeof edge.kind=="string"&&edge.kind.length>0)&&!canonicalEdges.has(`${from} ${to}`))return fail(`Edge ${from} -> ${to} contradicts the canonical depends_on graph. depends_on is authoritative; ordinary edges must encode the same graph.`)}let cycle=findCycle2(keys,canonicalEdges);return cycle?fail(`Finalized plan has a cycle: ${cycle}.`):succeed(plan)}function findCycle2(keys,edgeKeys){let adjacency=new Map;for(let key of keys)adjacency.set(key,[]);for(let edgeKey of edgeKeys){let[from,to]=edgeKey.split(" ");adjacency.get(from).push(to)}let WHITE=0,GREY=1,BLACK=2,color=new Map(keys.map(k=>[k,WHITE])),stack=[],visit=start=>{let frames=[{node:start,index:0}];for(color.set(start,GREY),stack.push(start);frames.length>0;){let frame=frames[frames.length-1],neighbors=adjacency.get(frame.node)??[];if(frame.index>=neighbors.length){color.set(frame.node,BLACK),stack.pop(),frames.pop();continue}let next=neighbors[frame.index++],state=color.get(next);if(state===GREY){let from=stack.indexOf(next);return[...stack.slice(from),next].join(" -> ")}state===WHITE&&(color.set(next,GREY),stack.push(next),frames.push({node:next,index:0}))}return null};for(let key of keys){if(color.get(key)!==WHITE)continue;let cycle=visit(key);if(cycle)return cycle}return null}function countSpecCharacters(text){let count=0;for(let _ of text)count++;return count}var SECRET_VALUE_PATTERNS=[/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi,/\bghp_[A-Za-z0-9]{20,}/g,/\bgithub_pat_[A-Za-z0-9_]{20,}/g,/\bsk-[A-Za-z0-9]{20,}/g,/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,/\bAKIA[0-9A-Z]{16}\b/g],REDACTED2="[REDACTED]";function sanitizeText(input){let out=input;for(let pattern of SECRET_VALUE_PATTERNS)out=out.replace(pattern,REDACTED2);return out}function bound(input,max){let sanitized=sanitizeText(input);return countSpecCharacters(sanitized)<=max?sanitized:Array.from(sanitized).slice(0,max).join("")+"\u2026[truncated]"}var FINDING_SUMMARY_MAX=600,FINDING_RESOLUTION_MAX=600;function serializeSiblingTicketManifest(candidate){return{schema_version:SIBLING_TICKET_MANIFEST_SCHEMA_VERSION,epic_key:candidate.epic_key,epic_slug:candidate.epic_slug,plan_version:candidate.plan_version,decomposition_fingerprint:candidate.decomposition_fingerprint,finalized_fingerprint:candidate.finalized_fingerprint??null,run_phase:candidate.run_phase,mappings:(candidate.mappings??[]).map(m=>({plan_node_id:m.plan_node_id,ticket_key:m.ticket_key,exploration_path:m.exploration_path,draft_path:m.draft_path})),decisions:(candidate.decisions??[]).map(d=>({finding_id:d.finding_id,chosen_option:bound(d.chosen_option,FINDING_SUMMARY_MAX),rationale:bound(d.rationale,FINDING_RESOLUTION_MAX)})),completed_mutations:(candidate.completed_mutations??[]).map(c=>({kind:c.kind,ticket_key:c.ticket_key,detail:bound(c.detail,FINDING_SUMMARY_MAX)}))}}async function writeJsonAtomically(destination,value,fs7){let dir=path23.dirname(destination),tempPath=path23.join(dir,`.${path23.basename(destination)}.tmp`),serialized=JSON.stringify(value,null,2)+`
|
|
4688
4727
|
`;try{await fs7.writeFile(tempPath,serialized)}catch(err){return await cleanupQuietly(tempPath,fs7),fail(`Could not write ${tempPath}: ${err instanceof Error?err.message:String(err)}`)}try{await fs7.rename(tempPath,destination)}catch(err){return await cleanupQuietly(tempPath,fs7),fail(`Could not replace ${destination}: ${err instanceof Error?err.message:String(err)}`)}return succeed(destination)}async function cleanupQuietly(target,fs7){try{await fs7.unlink(target)}catch{}}var USAGE=["Usage: emit-conductor-bundle <validate|finalize> --input <file> [--docs-dir <dir>] [--json]",""," validate Validate identities, the node->ticket mapping, manifest agreement,"," and path containment. Writes nothing."," finalize Validate, then finalize epic-plan.dag.json with real keys and"," per-node touched_files, and write the sibling-ticket manifest.","","Options:"," --input <file> JSON document with epic_key, epic_slug, mappings,"," decomposition_fingerprint, and (for finalize)"," touched_files_by_key."," --docs-dir <dir> Docs directory (default: $BAPI_DOCS_DIR, else docs/tmp)."," --json Emit a machine-readable result on stdout."," -h, --help Show this help."].join(`
|
|
4689
4728
|
`);function takeValue3(argv,index,flag){let value=argv[index+1];if(value===void 0||value.startsWith("-"))throw new Error(`Flag "${flag}" requires a value.`);return value}function parseConductorBundleArgs(argv){let mode=argv[0];if(mode==="-h"||mode==="--help"||mode===void 0)return{mode:"validate",inputFile:"",json:!1,help:!0};if(mode!=="validate"&&mode!=="finalize")throw new Error(`Unknown subcommand "${mode}". Expected "validate" or "finalize".`);let inputFile,docsDir,json=!1,help=!1;for(let i=1;i<argv.length;i++){let arg=argv[i];switch(arg){case"--input":inputFile=takeValue3(argv,i,"--input"),i++;break;case"--docs-dir":docsDir=takeValue3(argv,i,"--docs-dir"),i++;break;case"--json":json=!0;break;case"-h":case"--help":help=!0;break;default:throw new Error(`Unknown argument "${arg}".`)}}if(help)return{mode,inputFile:inputFile??"",docsDir,json,help:!0};if(!inputFile)throw new Error('Flag "--input" is required.');return{mode,inputFile,docsDir,json,help:!1}}async function readJson(filePath,fs7,label){let raw;try{raw=await fs7.readFile(filePath)}catch(err){throw new Error(`Could not read ${label} at ${filePath}: ${err instanceof Error?err.message:String(err)}`)}try{return JSON.parse(raw)}catch(err){throw new Error(`${label} at ${filePath} is not valid JSON: ${err instanceof Error?err.message:String(err)}`)}}async function readOptionalJson(filePath,fs7,label){try{await fs7.readFile(filePath)}catch{return null}return readJson(filePath,fs7,label)}async function runConductorBundleCli(argv,overrides={}){let deps={env:process.env,cwd:process.cwd(),fs:createDefaultBundleFs(),log:m=>console.log(m),errorLog:m=>console.error(m),...overrides},opts;try{opts=parseConductorBundleArgs(argv)}catch(err){return deps.errorLog(err instanceof Error?err.message:String(err)),deps.errorLog(USAGE),1}if(opts.help)return deps.log(USAGE),0;let emitFailure=error=>(opts.json?deps.log(JSON.stringify({ok:!1,error})):deps.errorLog(`Error: ${error}`),1),docsDir=path24.resolve(deps.cwd,opts.docsDir??deps.env.BAPI_DOCS_DIR??path24.join("docs","tmp")),input;try{input=await readJson(path24.resolve(deps.cwd,opts.inputFile),deps.fs,"--input document")}catch(err){return emitFailure(err instanceof Error?err.message:String(err))}if(!input||typeof input!="object")return emitFailure("--input document must be a JSON object.");let epicDir=path24.resolve(docsDir,"epic-plans",String(input.epic_slug)),sidecarPath=path24.join(epicDir,"epic-plan.dag.json"),manifestPath=path24.join(epicDir,"sibling-ticket-manifest.json"),sidecar,existingManifest;try{sidecar=await readJson(sidecarPath,deps.fs,"epic-plan.dag.json"),existingManifest=await readOptionalJson(manifestPath,deps.fs,"sibling-ticket-manifest.json")}catch(err){return emitFailure(err instanceof Error?err.message:String(err))}let validation=await validateConductorBundleInputs({epic_key:input.epic_key,epic_slug:input.epic_slug,docs_dir:docsDir,mappings:input.mappings,sidecar,existing_manifest:existingManifest??void 0,decomposition_fingerprint:input.decomposition_fingerprint},deps.fs);if(!validation.ok)return emitFailure(validation.error);if(opts.mode==="validate"){let result2={ok:!0,mode:"validate",epic_key:validation.value.epic_key,plan_version:validation.value.plan_version,mapped_nodes:validation.value.mappings.length,sidecar_path:validation.value.sidecar_path};return opts.json?deps.log(JSON.stringify(result2)):deps.log(`Validated ${result2.mapped_nodes} node mapping(s) for ${result2.epic_key} (plan v${result2.plan_version}). Nothing was written.`),0}let nodeKeyMap={};for(let m of validation.value.mappings)nodeKeyMap[m.plan_node_id]=m.ticket_key;let finalized=finalizeEpicPlanSidecar({sidecar,node_key_map:nodeKeyMap,touched_files_by_key:input.touched_files_by_key??{},plan_version_already_stored:input.plan_version_already_stored===!0});if(!finalized.ok)return emitFailure(finalized.error);let sidecarWrite=await writeJsonAtomically(validation.value.sidecar_path,finalized.value,deps.fs);if(!sidecarWrite.ok)return emitFailure(sidecarWrite.error);let manifest=serializeSiblingTicketManifest({schema_version:1,epic_key:validation.value.epic_key,epic_slug:validation.value.epic_slug,plan_version:validation.value.plan_version,decomposition_fingerprint:input.decomposition_fingerprint,finalized_fingerprint:null,run_phase:input.run_phase??"staged",mappings:validation.value.mappings.map(m=>({plan_node_id:m.plan_node_id,ticket_key:m.ticket_key,exploration_path:m.exploration_path,draft_path:m.draft_path})),decisions:[],completed_mutations:[]}),manifestWrite=await writeJsonAtomically(validation.value.manifest_path,manifest,deps.fs);if(!manifestWrite.ok)return emitFailure(manifestWrite.error);let result={ok:!0,mode:"finalize",epic_key:validation.value.epic_key,plan_version:validation.value.plan_version,sidecar_path:validation.value.sidecar_path,manifest_path:validation.value.manifest_path,ticket_keys:validation.value.mappings.map(m=>m.ticket_key)};return opts.json?deps.log(JSON.stringify(result)):deps.log(`Finalized ${result.sidecar_path} with ${result.ticket_keys.length} real key(s) and per-node touched_files. Manifest: ${result.manifest_path}`),0}function createDefaultBundleFs(){return{readFile:async p=>(await import("node:fs/promises")).readFile(p,"utf-8"),writeFile:async(p,data)=>(await import("node:fs/promises")).writeFile(p,data,"utf-8"),rename:async(from,to)=>(await import("node:fs/promises")).rename(from,to),unlink:async p=>(await import("node:fs/promises")).unlink(p),realpath:async p=>(await import("node:fs/promises")).realpath(p)}}init_start_tickets();init_start_tickets_prereqs();var VALID_MODES=["lightweight","heavy"],MAX_SYMBOLS_PER_RUN=40,AST_GREP_CANDIDATES=["ast-grep","sg"];function getRegressionCheckUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server regression-check [--mode lightweight|heavy] [--diff <range>] [--symbols a,b,c] [--json]","","Deterministic blast-radius analysis for a proposed code change: extracts the","symbols touched by a git diff (or an explicit --symbols list), then uses","ast-grep to find their REAL structural call-sites and ripgrep for the wider","set of textual mentions (tests/mocks/strings/config). Read-only \u2014 makes no","code changes and no network calls.","","Flags:"," --mode lightweight|heavy Analysis depth (default: lightweight). lightweight"," traces a proposed diff's blast radius; heavy takes"," no diff and scans the whole repo for fragile,"," high-blast-radius locations (fan-in + temporal"," coupling + complexity), seeded from churn/coupling"," hotspots and bounded by a top-N cap."," --diff <range> A git diff range/ref to analyze, lightweight mode only (default: HEAD,"," i.e. the working tree against HEAD)"," --symbols a,b,c Explicit comma-separated symbol names, bypassing"," diff parsing (searches every supported language)"," --json Emit machine-readable JSON instead of a human summary"," -h, --help Show this help","","Missing tools (ast-grep, ripgrep) degrade gracefully: the affected section is","flagged [DEGRADED] and the command still exits 0. Run","`npx -y @bridge_gpt/mcp-server doctor` to check tool availability.","","Exit code: 0 on a completed run (degraded or not); non-zero only for a usage","error or an unreadable diff."].join(`
|
|
4690
4729
|
`)}function parseRegressionCheckArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getRegressionCheckUsage()};let mode="lightweight",diffRange,symbols,json=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--json"){json=!0;continue}if(arg==="--mode"||arg.startsWith("--mode=")){let value;if(arg.startsWith("--mode="))value=arg.slice(7);else{if(i+1>=argv.length)return{status:"error",message:"--mode requires a value (lightweight or heavy)."};value=argv[++i]}if(!VALID_MODES.includes(value))return{status:"error",message:`Invalid --mode value: '${value}' (allowed: ${VALID_MODES.join(", ")}).`};mode=value;continue}if(arg==="--diff"||arg.startsWith("--diff=")){let value;if(arg.startsWith("--diff="))value=arg.slice(7);else{if(i+1>=argv.length)return{status:"error",message:"--diff requires a value (a git diff range/ref)."};value=argv[++i]}diffRange=value;continue}if(arg==="--symbols"||arg.startsWith("--symbols=")){let value;if(arg.startsWith("--symbols="))value=arg.slice(10);else{if(i+1>=argv.length)return{status:"error",message:"--symbols requires a value (comma-separated symbol names)."};value=argv[++i]}if(symbols=value.split(",").map(s=>s.trim()).filter(s=>s.length>0),symbols.length===0)return{status:"error",message:"--symbols requires at least one non-empty symbol name."};continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. regression-check takes only flags.`}}return{status:"ok",options:{mode,diffRange,symbols,json}}}var SECRET_PATTERNS=[/\b[A-Za-z0-9_-]*(?:api[_-]?key|token|secret|password|bearer)[A-Za-z0-9_-]*\s*[:=]\s*['"]?[A-Za-z0-9_\-./+=]{8,}['"]?/gi,/\bBearer\s+[A-Za-z0-9._-]{10,}/g,/\bsk-[A-Za-z0-9_-]{16,}/g];function redactSecrets2(text){let out=text;for(let pattern of SECRET_PATTERNS)out=out.replace(pattern,"[REDACTED_TOKEN]");return out}async function getDiffText(deps,diffRange){let args=diffRange?["diff",diffRange]:["diff","HEAD"],result=await deps.runCommand("git",args,{cwd:deps.cwd});return result.exitCode!==0?{ok:!1,error:redactSecrets2(result.stderr.trim()||`git diff exited ${result.exitCode}`)}:{ok:!0,diff:redactSecrets2(result.stdout)}}var PY_DEF_RE=/^\s*(?:async\s+def|def|class)\s+([A-Za-z_][A-Za-z0-9_]*)/,TS_DEF_RE=/^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?\s+([A-Za-z_$][\w$]*)|class\s+([A-Za-z_$][\w$]*)|(?:const|let)\s+([A-Za-z_$][\w$]*)\s*[:=])/;function languageForFile(file){return file.endsWith(".py")?"python":file.endsWith(".ts")||file.endsWith(".tsx")?"typescript":null}function extractChangedSymbolsFromDiff(diffText){let symbols=[],currentFile=null,newLineNo=0;for(let rawLine of diffText.split(`
|
|
@@ -4694,7 +4733,7 @@ Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${err
|
|
|
4694
4733
|
`)){let line=rawLine.trim();if(!line)continue;let fields=parseLizardCsvLine(line);if(fields.length<11)continue;let[nloc,ccn,,,,,file,functionName,,startLine,endLine]=fields,ccnNum=Number(ccn),nlocNum=Number(nloc),startLineNum=Number(startLine),endLineNum=Number(endLine);[ccnNum,nlocNum,startLineNum,endLineNum].every(Number.isFinite)&&findings.push({functionName,file,ccn:ccnNum,nloc:nlocNum,startLine:startLineNum,endLine:endLineNum})}return findings}async function analyzeComplexityWithLizard(files,deps){if(files.length===0)return{ok:!0,findings:[]};if(!await isCommandOnPath(deps,"lizard"))return{ok:!1,error:"lizard not found on PATH"};let result=await deps.runCommand("lizard",["--csv",...files],{cwd:deps.cwd});return result.exitCode!==0&&result.stdout.trim().length===0?{ok:!1,error:redactSecrets2(result.stderr.trim()||`lizard exited ${result.exitCode}`)}:{ok:!0,findings:parseLizardCsv(result.stdout)}}var TEMPORAL_COUPLING_SOURCE_EXT=/\.(py|ts|tsx)$/,MIN_CO_OCCURRENCES=4,MIN_FILE_FREQUENCY=5,MIN_COUPLING_DEGREE=.5,MAX_FILES_PER_COMMIT=15,MAX_PAIRS_RETURNED=12;function computeTemporalCoupling(gitLogOutput,scopeFiles=[]){let scopeSet=scopeFiles.length>0?new Set(scopeFiles):null,commits=[],current=[];for(let line of gitLogOutput.split(`
|
|
4695
4734
|
`))line.startsWith("@")?(current.length&&commits.push(current),current=[]):line.trim()&&TEMPORAL_COUPLING_SOURCE_EXT.test(line.trim())&¤t.push(line.trim());current.length&&commits.push(current);let fileFreq=new Map,pairFreq=new Map;for(let files of commits){let uniq=Array.from(new Set(files)).filter(f=>!f.toLowerCase().includes("test"));if(uniq.length===0||uniq.length>MAX_FILES_PER_COMMIT||scopeSet&&!uniq.some(f=>scopeSet.has(f)))continue;for(let f of uniq)fileFreq.set(f,(fileFreq.get(f)??0)+1);let sorted=uniq.slice().sort();for(let i=0;i<sorted.length;i++)for(let j=i+1;j<sorted.length;j++){let key=`${sorted[i]}\0${sorted[j]}`;pairFreq.set(key,(pairFreq.get(key)??0)+1)}}let rows=[];for(let[key,n]of pairFreq){if(n<MIN_CO_OCCURRENCES)continue;let[a,b]=key.split("\0"),fa=fileFreq.get(a)??0,fb=fileFreq.get(b)??0,deg=n/Math.min(fa,fb);deg>=MIN_COUPLING_DEGREE&&fa>=MIN_FILE_FREQUENCY&&fb>=MIN_FILE_FREQUENCY&&rows.push({fileA:a,fileB:b,coOccurrences:n,couplingDegree:deg})}return rows.sort((x,y)=>y.couplingDegree-x.couplingDegree),rows.slice(0,MAX_PAIRS_RETURNED)}async function analyzeTemporalCoupling(files,deps,options={}){let commitLimit=options.commitLimit??800,result=await deps.runCommand("git",["log","--no-merges","-n",String(commitLimit),"--name-only","--pretty=format:@%H"],{cwd:deps.cwd});return result.exitCode!==0?{ok:!1,error:redactSecrets2(result.stderr.trim()||`git log exited ${result.exitCode}`)}:{ok:!0,pairs:computeTemporalCoupling(result.stdout,files)}}var HEAVY_CHURN_HOTSPOT_LIMIT=30,MAX_HEAVY_HOTSPOTS=50,MAX_HEAVY_SYMBOLS=MAX_SYMBOLS_PER_RUN,CHURN_LOG_LIMIT=500;async function analyzeChurnHotspots(deps,limit){let result=await deps.runCommand("git",["log","--no-merges","-n",String(CHURN_LOG_LIMIT),"--name-only","--pretty=format:"],{cwd:deps.cwd});if(result.exitCode!==0)return{ok:!1,error:redactSecrets2(result.stderr.trim()||`git log exited ${result.exitCode}`)};let freq=new Map;for(let rawLine of result.stdout.split(`
|
|
4696
4735
|
`)){let line=rawLine.trim();!line||!TEMPORAL_COUPLING_SOURCE_EXT.test(line)||freq.set(line,(freq.get(line)??0)+1)}return{ok:!0,files:Array.from(freq.entries()).sort((a,b)=>b[1]-a[1]).slice(0,limit).map(([file])=>file)}}async function resolveRepoRootDeps(deps){let result=await deps.runCommand("git",["rev-parse","--show-toplevel"],{cwd:deps.cwd}),root=result.exitCode===0?result.stdout.trim():"";return root?{...deps,cwd:root}:deps}async function runHeavyMode(deps){let repoDeps=await resolveRepoRootDeps(deps),degradedFlags=[],toolsUsed=new Set(["git"]),churnResult=await analyzeChurnHotspots(repoDeps,HEAVY_CHURN_HOTSPOT_LIMIT),churnFiles=churnResult.ok?churnResult.files:[];churnResult.ok||degradedFlags.push(`git churn analysis failed: ${churnResult.error}`);let couplingResult=await analyzeTemporalCoupling([],repoDeps),couplingPartners=new Map,couplingFiles=[];if(couplingResult.ok){for(let pair of couplingResult.pairs)couplingFiles.push(pair.fileA,pair.fileB),couplingPartners.set(pair.fileA,(couplingPartners.get(pair.fileA)??0)+1),couplingPartners.set(pair.fileB,(couplingPartners.get(pair.fileB)??0)+1);toolsUsed.add("git-log-temporal-coupling")}else degradedFlags.push(`temporal coupling analysis failed: ${couplingResult.error}`);let candidateFilesAll=Array.from(new Set([...churnFiles,...couplingFiles])),fileCapApplied=candidateFilesAll.length>MAX_HEAVY_HOTSPOTS,candidateFiles=fileCapApplied?candidateFilesAll.slice(0,MAX_HEAVY_HOTSPOTS):candidateFilesAll,complexityResult=await analyzeComplexityWithLizard(candidateFiles,repoDeps),candidateSymbols;complexityResult.ok?(candidateFiles.length>0&&toolsUsed.add("lizard"),candidateSymbols=complexityResult.findings.map(f=>({file:f.file,line:f.startLine,symbol:f.functionName,complexityScore:f.ccn}))):(degradedFlags.push(`lizard complexity analysis failed: ${complexityResult.error}`),degradedFlags.push("ast-grep fan-in skipped for these hotspots \u2014 no symbol-discovery fallback exists without lizard"),candidateSymbols=candidateFiles.map(file=>({file,line:1,symbol:null,complexityScore:null})));let symbolCapApplied=candidateSymbols.length>MAX_HEAVY_SYMBOLS;symbolCapApplied&&(candidateSymbols=candidateSymbols.slice().sort((a,b)=>(b.complexityScore??0)-(a.complexityScore??0)).slice(0,MAX_HEAVY_SYMBOLS));let astGrepBinary=await resolveFirstCommandOnPath(repoDeps,AST_GREP_CANDIDATES);!astGrepBinary&&candidateSymbols.some(s=>s.symbol)&°radedFlags.push("ast-grep (or sg) not found on PATH \u2014 fan-in analysis skipped for all symbols.");let findings=[];for(let candidate of candidateSymbols){let fanInCount=null;if(astGrepBinary&&candidate.symbol){let language=languageForFile(candidate.file),result=await findCallSites(repoDeps,astGrepBinary,candidate.symbol,language);result.ok?(fanInCount=result.count,toolsUsed.add("ast-grep")):degradedFlags.push(`ast-grep call-site search failed for '${candidate.symbol}': ${result.error}`)}let couplingPartnerCount=couplingPartners.get(candidate.file)??0,fragilityRank=(fanInCount??0)*2+couplingPartnerCount*3+(candidate.complexityScore??0);findings.push({location:{file:candidate.file,line:candidate.line},signals:{fan_in_count:fanInCount,coupling_partners:couplingPartnerCount,complexity_score:candidate.complexityScore},fragility_rank:fragilityRank})}return findings.sort((a,b)=>b.fragility_rank-a.fragility_rank),{mode:"heavy",summary:{hotspots_scanned:candidateFiles.length,cap_applied:fileCapApplied||symbolCapApplied,tools_used:Array.from(toolsUsed),degraded_flags:degradedFlags},findings}}function formatHeavyReportJson(report){return JSON.stringify(report,null,2)}function formatHeavyReportText(report){let lines=["regression-check report (heavy mode)","",`Hotspots scanned: ${report.summary.hotspots_scanned}${report.summary.cap_applied?" [CAPPED]":""}`,`Tools used: ${report.summary.tools_used.length>0?report.summary.tools_used.join(", "):"none"}`,""];report.findings.length===0&&lines.push("No fragility findings.");for(let f of report.findings){lines.push(`${f.location.file}:${f.location.line} fragility_rank=${f.fragility_rank}`);let fanIn=f.signals.fan_in_count===null?"[DEGRADED]":String(f.signals.fan_in_count),complexity=f.signals.complexity_score===null?"[DEGRADED]":String(f.signals.complexity_score);lines.push(` fan-in: ${fanIn} coupling partners: ${f.signals.coupling_partners} complexity: ${complexity}`)}if(lines.push(""),report.summary.degraded_flags.length>0){lines.push(`[DEGRADED] ${report.summary.degraded_flags.length} issue(s):`);for(let flag of report.summary.degraded_flags)lines.push(` - ${flag}`)}else lines.push("No degradation \u2014 all tools ran successfully.");return lines.join(`
|
|
4697
|
-
`)}async function runRegressionCheckCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseRegressionCheckArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getRegressionCheckUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),{options}=parsed;if(options.mode==="heavy"){let report=await runHeavyMode(deps);return log(options.json?formatHeavyReportJson(report):formatHeavyReportText(report)),0}let result=await runLightweightRegressionCheck(deps,options);return result.ok?(log(options.json?formatRegressionCheckJson(result.report):formatRegressionCheckReport(result.report)),0):(errorLog(`Error: ${result.error}`),1)}import{readFile as readFile11,writeFile as writeFile7,mkdir as mkdir7,stat as stat8,rename,chmod,unlink,open}from"fs/promises";import{spawn as spawn7}from"child_process";import{randomBytes as cryptoRandomBytes,createHash as createHash3}from"crypto";import os14 from"os";import path26 from"path";import
|
|
4736
|
+
`)}async function runRegressionCheckCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseRegressionCheckArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getRegressionCheckUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),{options}=parsed;if(options.mode==="heavy"){let report=await runHeavyMode(deps);return log(options.json?formatHeavyReportJson(report):formatHeavyReportText(report)),0}let result=await runLightweightRegressionCheck(deps,options);return result.ok?(log(options.json?formatRegressionCheckJson(result.report):formatRegressionCheckReport(result.report)),0):(errorLog(`Error: ${result.error}`),1)}import{readFile as readFile11,writeFile as writeFile7,mkdir as mkdir7,stat as stat8,rename,chmod,unlink,open}from"fs/promises";import{spawn as spawn7}from"child_process";import{randomBytes as cryptoRandomBytes,createHash as createHash3}from"crypto";import os14 from"os";import path26 from"path";import readline3 from"readline";init_version_generated();init_bridge_config();function joinPath(base,rel){let trimmedBase=base.endsWith("/")?base.slice(0,-1):base,trimmedRel=rel.startsWith("/")?rel.slice(1):rel;return`${trimmedBase}/${trimmedRel}`}var MCP_HOST_TARGETS={"claude-code":{id:"claude-code",label:"Claude Code",scope:"project",relPath:".mcp.json",displayPath:".mcp.json",format:"json",topLevelKey:"mcpServers",transportType:void 0,vendorCli:{bin:"claude",kind:"claude-add-json"},launchAgent:"claude",worktreeSupported:!0,writeStrategy:"vendor-first",detect:()=>!0},cursor:{id:"cursor",label:"Cursor",scope:"project",relPath:".cursor/mcp.json",displayPath:".cursor/mcp.json",format:"json",topLevelKey:"mcpServers",transportType:"stdio",launchAgent:"cursor-agent",worktreeSupported:!0,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".cursor"))||typeof ctx.env.CURSOR_TRACE_DIR=="string"&&ctx.env.CURSOR_TRACE_DIR.length>0},"copilot-vscode":{id:"copilot-vscode",label:"GitHub Copilot (VS Code)",scope:"project",relPath:".vscode/mcp.json",displayPath:".vscode/mcp.json",format:"json",topLevelKey:"servers",transportType:"stdio",worktreeSupported:!1,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".vscode"))},"copilot-cli":{id:"copilot-cli",label:"GitHub Copilot CLI",scope:"global",absPathResolver:homedir=>joinPath(homedir,".copilot/mcp-config.json"),displayPath:"~/.copilot/mcp-config.json",format:"json",topLevelKey:"mcpServers",transportType:"local",extraEntryKeys:{tools:["*"]},vendorCli:{bin:"copilot",kind:"copilot-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:()=>!1},codex:{id:"codex",label:"OpenAI Codex",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codex/config.toml"),displayPath:"~/.codex/config.toml",format:"toml",topLevelKey:"mcp_servers",transportType:void 0,vendorCli:{bin:"codex",kind:"codex-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:ctx=>ctx.exists(joinPath(ctx.homedir,".codex"))},windsurf:{id:"windsurf",label:"Windsurf",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codeium/windsurf/mcp_config.json"),displayPath:"~/.codeium/windsurf/mcp_config.json",format:"json",topLevelKey:"mcpServers",transportType:void 0,worktreeSupported:!1,writeStrategy:"manual-instructions",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".windsurf"))||ctx.exists(joinPath(ctx.cwd,".windsurfrules"))}},HOST_PLATFORM_ORDER=["claude-code","cursor","copilot-vscode","copilot-cli","codex","windsurf"];function allHostTargets(){return HOST_PLATFORM_ORDER.map(id=>MCP_HOST_TARGETS[id])}function isHostPlatformId(value){return Object.prototype.hasOwnProperty.call(MCP_HOST_TARGETS,value)}function agentForPlatform(id){return MCP_HOST_TARGETS[id].launchAgent}function detectDefaultPlatforms(ctx){return allHostTargets().filter(t=>t.detect(ctx)).map(t=>t.id)}init_version_generated();function joinPath2(base,rel){let b=base.endsWith("/")?base.slice(0,-1):base,r=rel.startsWith("/")?rel.slice(1):rel;return`${b}/${r}`}function dirnameOf(p){let i=p.lastIndexOf("/");return i<=0?"/":p.slice(0,i)}function resolveTargetAbsPath(target,ctx){if(target.scope==="project"){if(!target.relPath)throw new Error(`${target.id} missing relPath`);return joinPath2(ctx.cwd,target.relPath)}if(!target.absPathResolver)throw new Error(`${target.id} missing absPathResolver`);return target.absPathResolver(ctx.homedir)}function adaptBridgeEntryForHostTarget(entry,target){let physical={};if(target.transportType!==void 0&&(physical.type=target.transportType),physical.command=entry.command,physical.args=[...entry.args],physical.env={...entry.env},target.extraEntryKeys)for(let[k,v]of Object.entries(target.extraEntryKeys))physical[k]=v;return physical}function isEnoent(err){return typeof err=="object"&&err!==null&&err.code==="ENOENT"}async function readJsonHostConfig(path37,deps){let raw;try{raw=await deps.readFile(path37)}catch(err){return isEnoent(err)?{state:"missing"}:{state:"read-error",message:"config could not be read"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{state:"invalid"}}return typeof parsed!="object"||parsed===null||Array.isArray(parsed)?{state:"invalid"}:{state:"valid",value:parsed}}function mergeJsonHostConfig(existing,target,adaptedEntry){let merged={...existing},rootRaw=merged[target.topLevelKey],root=rootRaw&&typeof rootRaw=="object"&&!Array.isArray(rootRaw)?{...rootRaw}:{};return root["bridge-api"]=adaptedEntry,merged[target.topLevelKey]=root,merged}function freshJsonDocument(target,adaptedEntry){return{[target.topLevelKey]:{"bridge-api":adaptedEntry}}}async function writeJsonHostConfig(path37,target,adaptedEntry,deps){let read=await readJsonHostConfig(path37,deps);if(read.state==="invalid"||read.state==="read-error")return{status:"skipped-invalid",message:"skipped \u2014 invalid JSON"};let isCreate=read.state==="missing",doc=isCreate?freshJsonDocument(target,adaptedEntry):mergeJsonHostConfig(read.value,target,adaptedEntry);return await deps.mkdir(dirnameOf(path37),{recursive:!0}),await deps.writeFile(path37,JSON.stringify(doc,null,2)+`
|
|
4698
4737
|
`),{status:isCreate?"created":"direct-written",path:path37}}function tomlEscapeString(value){let out="";for(let ch of value)switch(ch){case"\\":out+="\\\\";break;case'"':out+='\\"';break;case`
|
|
4699
4738
|
`:out+="\\n";break;case"\r":out+="\\r";break;case" ":out+="\\t";break;case"\b":out+="\\b";break;case"\f":out+="\\f";break;default:{let code=ch.codePointAt(0);code<32?out+="\\u"+code.toString(16).padStart(4,"0"):out+=ch}}return out}function tomlBasicString(value){return`"${tomlEscapeString(value)}"`}function tomlStringArray(values){return`[${values.map(tomlBasicString).join(", ")}]`}var CODEX_ENV_ORDER=["BAPI_BASE_URL","BAPI_REPO_NAME","BAPI_API_KEY","BAPI_DOCS_DIR","BAPI_PROJECT_ROOT"];function orderedEnvKeys(env){let known=CODEX_ENV_ORDER.filter(k=>k in env),extra=Object.keys(env).filter(k=>!CODEX_ENV_ORDER.includes(k)).sort();return[...known,...extra]}function renderCodexBridgeToml(entry){let lines=["[mcp_servers.bridge-api]",`command = ${tomlBasicString(entry.command)}`,`args = ${tomlStringArray(entry.args)}`,"","[mcp_servers.bridge-api.env]"];for(let key of orderedEnvKeys(entry.env))lines.push(`${key} = ${tomlBasicString(entry.env[key])}`);return lines.join(`
|
|
4700
4739
|
`)+`
|
|
@@ -4703,7 +4742,7 @@ Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${err
|
|
|
4703
4742
|
`:`
|
|
4704
4743
|
|
|
4705
4744
|
`;return{action:"append",content:content+boundary+renderCodexBridgeToml(entry)}}async function readTomlHostConfig(path37,deps){try{return{state:"text",content:await deps.readFile(path37)}}catch(err){return isEnoent(err)?{state:"missing"}:{state:"read-error",message:"config could not be read"}}}async function writeCodexHostConfig(path37,entry,deps){let read=await readTomlHostConfig(path37,deps),merge=mergeCodexHostConfig(read,entry);return merge.action==="manual-required"?{status:"manual-required"}:(await deps.mkdir(dirnameOf(path37),{recursive:!0}),await deps.writeFile(path37,merge.content),{status:merge.action==="create"?"created":"direct-written",path:path37})}async function inspectJsonHostEntry(path37,target,deps){let read=await readJsonHostConfig(path37,deps);if(read.state!=="valid")return{present:!1};let root=read.value[target.topLevelKey];if(!root||typeof root!="object"||Array.isArray(root))return{present:!1};let entry=root["bridge-api"];if(!entry||typeof entry!="object")return{present:!1};let args=entry.args;return{present:!0,args:Array.isArray(args)?args.filter(a=>typeof a=="string"):void 0}}async function inspectTomlHostEntry(path37,deps){let read=await readTomlHostConfig(path37,deps);if(read.state!=="text")return{present:!1};if(!codexHasBridgeTable(read.content))return{present:!1};let m=read.content.match(/^\s*args\s*=\s*\[(.*?)\]/m),args;return m&&(args=m[1].split(",").map(s=>s.trim().replace(/^["']|["']$/g,"")).filter(s=>s.length>0)),{present:!0,args}}async function inspectHostEntry(target,ctx,deps){let path37=resolveTargetAbsPath(target,ctx);return target.format==="toml"?inspectTomlHostEntry(path37,deps):inspectJsonHostEntry(path37,target,deps)}function isVendorContractVerified(kind){return kind==="claude-add-json"||kind==="copilot-add"}function buildVendorInvocation(target,entry){let vendor=target.vendorCli;if(!vendor||!isVendorContractVerified(vendor.kind))return null;if(vendor.kind==="claude-add-json"){let json=JSON.stringify({command:entry.command,args:entry.args,env:entry.env});return{bin:vendor.bin,args:["mcp","add-json","bridge-api",json,"--scope","project"]}}if(vendor.kind==="copilot-add"){let envArgs=[];for(let[k,v]of Object.entries(entry.env))envArgs.push("--env",`${k}=${v}`);return{bin:vendor.bin,args:["mcp","add","bridge-api","--tools","*",...envArgs,"--",entry.command,...entry.args]}}return null}function outcome(target,status,detail){return{platform:target.id,status,displayPath:target.displayPath,detail}}async function directWriteTarget(target,entry,deps){let path37=resolveTargetAbsPath(target,deps);if(target.format==="toml")return(await writeCodexHostConfig(path37,entry,deps.fs)).status==="manual-required"?outcome(target,"manual-required","existing config could not be safely updated"):outcome(target,"direct-written");let adapted=adaptBridgeEntryForHostTarget(entry,target),res=await writeJsonHostConfig(path37,target,adapted,deps.fs);return res.status==="skipped-invalid"?outcome(target,"skipped-invalid",res.message):outcome(target,"direct-written")}async function provisionHostTarget(target,entry,deps){if(target.writeStrategy==="manual-instructions")return outcome(target,"manual-required","global config \u2014 manual setup");if(target.writeStrategy==="direct")return directWriteTarget(target,entry,deps);let invocation=buildVendorInvocation(target,entry);return invocation&&await deps.vendor.probeBinary(invocation.bin,deps.env)&&(await deps.vendor.invokeVendorAdd(invocation,deps.env)).ok&&(await inspectHostEntry(target,deps,deps.fs)).present?outcome(target,"vendor-written"):directWriteTarget(target,entry,deps)}var PROBE_SECRET_KEYS=["BAPI_API_KEY","BAPI_INVITE","BAPI_SIGNUP_EMAIL"];function sanitizeProbeEnv(env){let clone={...env};for(let key of PROBE_SECRET_KEYS)delete clone[key];return clone}var VENDOR_PROCESS_TIMEOUT_MS=8e3;function createDefaultVendorProcessDeps(spawnFn){return{probeBinary:(bin,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2(ok))};try{let child=spawnFn(bin,["--version"],{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}}),invokeVendorAdd:(invocation,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2({ok}))};try{let child=spawnFn(invocation.bin,invocation.args,{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}})}}var MCP_INSTALL_STATE_VERSION=1,MCP_INSTALL_STATE_RELPATH=".bridge/install-state.json";function joinCwd(cwd,rel){return`${cwd.endsWith("/")?cwd.slice(0,-1):cwd}/${rel}`}function installStatePath(cwd){return joinCwd(cwd,MCP_INSTALL_STATE_RELPATH)}function installStateTempPath(cwd){return joinCwd(cwd,`${MCP_INSTALL_STATE_RELPATH}.tmp`)}function bridgeDirPath(cwd){return joinCwd(cwd,".bridge")}function normalizePlatforms(ids){let wanted=new Set(ids);return HOST_PLATFORM_ORDER.filter(id=>wanted.has(id))}function normalizeProjectPaths(paths){let seen=new Set,out=[];for(let p of paths)typeof p=="string"&&p.length>0&&!seen.has(p)&&(seen.add(p),out.push(p));return out.sort(),out}function serializeMcpInstallState(state){let ordered={version:state.version,selectedPlatforms:state.selectedPlatforms,projectConfigPaths:state.projectConfigPaths};return JSON.stringify(ordered,null,2)+`
|
|
4706
|
-
`}async function writeMcpInstallState(cwd,input,deps){let state={version:MCP_INSTALL_STATE_VERSION,selectedPlatforms:normalizePlatforms(input.selectedPlatforms),projectConfigPaths:normalizeProjectPaths(input.projectConfigPaths)},finalPath=installStatePath(cwd),tempPath=installStateTempPath(cwd),serialized=serializeMcpInstallState(state);try{await deps.mkdir(bridgeDirPath(cwd),{recursive:!0}),await deps.writeFile(tempPath,serialized),await deps.rename(tempPath,finalPath)}catch(err){if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,error:`failed to persist install state: ${err instanceof Error?err.message:String(err)}`}}return{ok:!0,path:finalPath,state}}init_start_tickets_repo();init_credential_store();var TERMINAL_STATUSES=new Set(["staged","awaiting-organization-approval","connected","expired","invalid","verification-failed","no-repositories","conflict","failed"]),ALL_STATUSES=new Set(["waiting",...TERMINAL_STATUSES]),REQUEST_TIMEOUT_MS=15e3;async function postJson(deps,path37,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path37}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":deps.apiKey},body:JSON.stringify(payload),signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch(e){return{ok:!1,kind:e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?"timeout":"network"}}let body=null;try{body=await resp.json()}catch{}return{ok:!0,value:{status:resp.status,body,retryAfter:resp.headers.get("Retry-After")}}}function classifyStatus(status){return status===401||status===403?"unauthorized":status===404?"not-found":"server"}function asRecord(value){return value&&typeof value=="object"&&!Array.isArray(value)?value:null}function asString3(value){return typeof value=="string"&&value.length>0?value:null}function asNullableString(value){return typeof value=="string"&&value.length>0?value:null}function parseRetryAfterMs(header,remainingMs,nowMs){if(!header)return null;let trimmed=header.trim();if(!trimmed)return null;let clamp=ms=>!Number.isFinite(ms)||ms<0?null:Math.min(ms,Math.max(0,remainingMs));if(/^\d+$/.test(trimmed))return clamp(Number(trimmed)*1e3);let parsed=Date.parse(trimmed);return Number.isNaN(parsed)?null:clamp(parsed-nowMs)}async function mintGithubConnection(deps,repoName){let res=await postJson(deps,"/setup/github/cli/connection-code",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),state=asString3(body?.state),installUrl=asString3(body?.install_url),ttlSeconds=body?.ttl_seconds;return!body||!state||!installUrl||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{state,installUrl,ttlSeconds}}}function parseCandidates(value){if(!Array.isArray(value))return null;let out=[];for(let raw of value){let rec=asRecord(raw),id=asString3(rec?.github_repository_id),name=asString3(rec?.github_repo_name);if(!rec||!id||!name)return null;out.push({github_repository_id:id,github_repo_name:name,github_repo_full_name:asNullableString(rec.github_repo_full_name),owner:asNullableString(rec.owner)})}return out}async function confirmGithubConnection(deps,repoName,state,githubRepositoryId){let res=await postJson(deps,"/setup/github/cli/confirm",{repo_name:repoName,state,github_repository_id:githubRepositoryId});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),name=asString3(body?.github_repo_name);
|
|
4745
|
+
`}async function writeMcpInstallState(cwd,input,deps){let state={version:MCP_INSTALL_STATE_VERSION,selectedPlatforms:normalizePlatforms(input.selectedPlatforms),projectConfigPaths:normalizeProjectPaths(input.projectConfigPaths)},finalPath=installStatePath(cwd),tempPath=installStateTempPath(cwd),serialized=serializeMcpInstallState(state);try{await deps.mkdir(bridgeDirPath(cwd),{recursive:!0}),await deps.writeFile(tempPath,serialized),await deps.rename(tempPath,finalPath)}catch(err){if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,error:`failed to persist install state: ${err instanceof Error?err.message:String(err)}`}}return{ok:!0,path:finalPath,state}}init_start_tickets_repo();init_credential_store();var TERMINAL_STATUSES=new Set(["staged","awaiting-organization-approval","connected","expired","invalid","verification-failed","no-repositories","conflict","failed"]),ALL_STATUSES=new Set(["waiting",...TERMINAL_STATUSES]),KNOWN_INDEXING_STATUSES=new Set(["started","waiting-for-setup"]),REQUEST_TIMEOUT_MS=15e3;async function postJson(deps,path37,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path37}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":deps.apiKey},body:JSON.stringify(payload),signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch(e){return{ok:!1,kind:e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?"timeout":"network"}}let body=null;try{body=await resp.json()}catch{}return{ok:!0,value:{status:resp.status,body,retryAfter:resp.headers.get("Retry-After")}}}function classifyStatus(status){return status===401||status===403?"unauthorized":status===404?"not-found":"server"}function asRecord(value){return value&&typeof value=="object"&&!Array.isArray(value)?value:null}function asString3(value){return typeof value=="string"&&value.length>0?value:null}function asNullableString(value){return typeof value=="string"&&value.length>0?value:null}function parseRetryAfterMs(header,remainingMs,nowMs){if(!header)return null;let trimmed=header.trim();if(!trimmed)return null;let clamp=ms=>!Number.isFinite(ms)||ms<0?null:Math.min(ms,Math.max(0,remainingMs));if(/^\d+$/.test(trimmed))return clamp(Number(trimmed)*1e3);let parsed=Date.parse(trimmed);return Number.isNaN(parsed)?null:clamp(parsed-nowMs)}async function mintGithubConnection(deps,repoName){let res=await postJson(deps,"/setup/github/cli/connection-code",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),state=asString3(body?.state),installUrl=asString3(body?.install_url),ttlSeconds=body?.ttl_seconds;return!body||!state||!installUrl||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{state,installUrl,ttlSeconds}}}function parseCandidates(value){if(!Array.isArray(value))return null;let out=[];for(let raw of value){let rec=asRecord(raw),id=asString3(rec?.github_repository_id),name=asString3(rec?.github_repo_name);if(!rec||!id||!name)return null;out.push({github_repository_id:id,github_repo_name:name,github_repo_full_name:asNullableString(rec.github_repo_full_name),owner:asNullableString(rec.owner)})}return out}async function confirmGithubConnection(deps,repoName,state,githubRepositoryId){let res=await postJson(deps,"/setup/github/cli/confirm",{repo_name:repoName,state,github_repository_id:githubRepositoryId});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),name=asString3(body?.github_repo_name);if(!body||body.status!=="connected"||!name)return{ok:!1,kind:"malformed"};let rawIndexingStatus=body.indexing_status,indexingStatus=typeof rawIndexingStatus=="string"&&KNOWN_INDEXING_STATUSES.has(rawIndexingStatus)?rawIndexingStatus:null;return{ok:!0,value:{githubRepoName:name,githubRepoFullName:asNullableString(body.github_repo_full_name),indexingStatus}}}async function fetchGithubConfigurationState(deps,repoName){let resp;try{resp=await deps.fetch(`${deps.baseUrl}/jira/config/install-manifest?repo_name=${encodeURIComponent(repoName)}`,{headers:{"X-API-Key":deps.apiKey},signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch{return"unavailable"}if(resp.status!==200)return"unavailable";let body;try{body=await resp.json()}catch{return"unavailable"}let integrations=asRecord(body)?.integrations;if(!Array.isArray(integrations))return"unavailable";for(let raw of integrations){let rec=asRecord(raw);if(rec?.id==="github_app")return typeof rec.is_configured!="boolean"?"unavailable":rec.is_configured?"configured":"unconfigured"}return"unavailable"}var POLL_DEADLINE_MS=15.5*60*1e3,POLL_DELAYS_MS=[2e3,3e3,5e3],MAX_JITTER_MS=400,RETRYABLE_TRANSPORT=new Set(["network","timeout"]);function isRetryableStatus(status){return status===429||status>=500}async function pollGithubConnection(deps,poll,repoName,state){let started=poll.now(),attempt=0;for(;;){let elapsed=poll.now()-started,remaining=POLL_DEADLINE_MS-elapsed;if(remaining<=0)return{ok:!1,kind:"deadline"};let res=await postJson(deps,"/setup/github/cli/status",{repo_name:repoName,state}),waitMs=null;if(res.ok)if(res.value.status===200){let body=asRecord(res.value.body),status=asString3(body?.status);if(!body||!status||!ALL_STATUSES.has(status))return{ok:!1,kind:"malformed"};let candidates=parseCandidates(body.candidates??[]);if(candidates===null)return{ok:!1,kind:"malformed"};let typed=status;if(TERMINAL_STATUSES.has(typed))return{ok:!0,value:{status:typed,candidates,githubRepoName:asNullableString(body.github_repo_name),retryAfterMs:null}};waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now())}else if(isRetryableStatus(res.value.status))waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now());else return{ok:!1,kind:classifyStatus(res.value.status)};else if(!RETRYABLE_TRANSPORT.has(res.kind))return{ok:!1,kind:res.kind};waitMs===null&&(waitMs=POLL_DELAYS_MS[Math.min(attempt,POLL_DELAYS_MS.length-1)]+Math.floor(poll.jitter()*MAX_JITTER_MS)),attempt+=1;let capped=Math.min(waitMs,Math.max(0,POLL_DEADLINE_MS-(poll.now()-started)));if(capped<=0)return{ok:!1,kind:"deadline"};await poll.sleep(capped)}}import{readFile as readFile10,stat as stat7}from"fs/promises";import{spawn as spawn6}from"child_process";import os13 from"os";import path25 from"path";import readline2 from"readline";init_bridge_config();init_start_tickets_repo();init_credential_store();var USAGE2=`Usage: connect-github [--repo <repo_name>]
|
|
4707
4746
|
|
|
4708
4747
|
Connect a GitHub repository to a Bridge project from your terminal.
|
|
4709
4748
|
|
|
@@ -4714,18 +4753,23 @@ password \u2014 you authenticate to GitHub in the browser.
|
|
|
4714
4753
|
Options:
|
|
4715
4754
|
--repo <repo_name> Bridge project to connect (inferred from this directory
|
|
4716
4755
|
when omitted; you will be asked to confirm).
|
|
4717
|
-
--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
|
|
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}
|
|
4718
4757
|
`),stderr:message=>process.stderr.write(`${message}
|
|
4719
|
-
`)}}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}.`),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 and Stage 9 offers \u2014 this session's only closing interaction is the single indexing question below). Do NOT run /learn-repository. Do NOT call parse_repository (or otherwise start indexing) before the capability report and explicit consent below. Complete the command's read-after-write five-section capability report first: 'Connected \u2713', 'Not yet connected \u2717', 'Tools you can use now', 'Tools you'll unlock', and 'Recommended next step + why'. Only AFTER that report is fully presented, ask exactly one question using this visible prompt: '[Y/n] Index repository now?'. Only an explicit affirmative answer (e.g. 'y'/'yes') starts indexing; a blank answer, a negative answer, EOF, an unavailable interaction, and any non-interactive/headless run all resolve to NO. On an affirmative answer: call the parse_repository MCP tool exactly once, describe the accepted job as QUEUED, and direct later progress checks to get_parse_status or /check-parse-status (do NOT poll it to completion). If parse_repository returns a blocking refusal or error, do NOT claim the job was queued \u2014 report the sanitized result and leave indexing pending. On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste continuation command '/parse-repository' on its own line and state that indexing remains pending. 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') and whether indexing was queued or left pending \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 opens a fresh agent session to derive the remaining","config, present a capability report, and offer optional repository indexing.","","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 you are"," asked which tools you use (Claude Code plus any"," detected editors are pre-checked). A non-"," interactive run without --tools writes the legacy"," automatic set (Claude Code plus any detected"," Cursor / Copilot VS Code). --tools= (empty) is an"," explicit empty selection and writes 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, spawn command)"," without writing, pinging, or spawning anything."," With --invite it also never calls the exchange"," endpoint and never generates or stores a secret."," --agent claude|cursor-agent Agent to launch for the agentic remainder"," (default: claude)."," -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(`
|
|
4720
|
-
`)}function parseInstallBridgeArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getInstallBridgeUsage()};let apiKey,repo,force=!1,dryRun=!1,agentName
|
|
4721
|
-
`),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=
|
|
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(`
|
|
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:"An API key is required. Pass --api-key or set the BAPI_API_KEY environment variable (no interactive terminal is available to prompt for it)."}}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? [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(`
|
|
4722
4761
|
${promptText}
|
|
4762
|
+
`),output.write(`[x] = selected \xB7 [ ] = not selected
|
|
4723
4763
|
`),options.forEach((opt,idx)=>{let mark=selected.has(opt.id)?"[x]":"[ ]";output.write(` ${idx+1}. ${mark} ${opt.label}
|
|
4724
|
-
`)}),output.write(
|
|
4725
|
-
`),
|
|
4764
|
+
`)}),output.write(`Type numbers to toggle, e.g. 1,3 \u2014 then Enter. Enter with \u22651 selected accepts.
|
|
4765
|
+
`),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
|
+
`),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
|
+
`):["To finish configuring this project, open it in an AI coding tool that has the","Bridge MCP server configured and run /install-bridge.","Until the project is configured, your Bridge MCP tools stay limited."].join(`
|
|
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)+`
|
|
4726
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.`}}return resp.ok?{ok:!0}:resp.status===401||resp.status===403?{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.)`}: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 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(`
|
|
4727
|
-
`),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: ${plan.
|
|
4728
|
-
`)}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 agent=resolveAgentSpec(options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),spawnCommand=deps.buildShellCommand(agent,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform),credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),plan={repoName,baseUrl,docsDir,agentName:options.agentName,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),spawnCommand,...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}let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",spawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;let launchCommand=materialized.command;if(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");let prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env),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'.")}}await offerGithubConnection(repoName,deps,log);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}`),log(`Step 5/5 \u2014 opening a ${agent.name} session for /install-bridge configuration + capability report\u2026`);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd});return spawnResult.ok?(log(""),log(`install-bridge setup steps complete. A fresh ${agent.name} session is now applying configuration, presenting the capability report, and ending with one indexing-consent question.`),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, and it will close by asking '[Y/n] Index repository now?'. 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}). The project is NOT configured yet \u2014 run /install-bridge manually in this project to derive and apply the config fields and see the capability report, then choose whether to run /parse-repository to index the repository.`),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(`
|
|
4771
|
+
`),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(`
|
|
4729
4773
|
[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:
|
|
4730
4774
|
${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(`
|
|
4731
4775
|
Upgrading @bridge_gpt/mcp-server to ${targetVersion}...
|
|
@@ -4735,8 +4779,8 @@ Upgrade complete: ${oldVersion} -> ${targetVersion}`),console.log("Please reconn
|
|
|
4735
4779
|
Opening a fresh agent session to reconnect...`),(await spawnTerminalTab(deps,terminal,spawnCommand,{key:"reconnect",worktreePath:cwd})).ok||console.log(`
|
|
4736
4780
|
To continue, manually run:
|
|
4737
4781
|
${spawnCommand}
|
|
4738
|
-
`)}catch{console.log("To continue, please reload your agent session manually.")}return 0}init_credential_store();import{readFile as readFile12,mkdir as mkdir8,writeFile as writeFile8,rename as rename2,chmod as chmod2,unlink as unlink2}from"fs/promises";import os15 from"os";import
|
|
4739
|
-
`)}function parseCredentialsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help"};let positionals=[],writeCredentials=!1,sources=[];for(let arg of argv){if(arg==="--write-credentials"){writeCredentials=!0;continue}if(arg==="--no-write-credentials"){writeCredentials=!1;continue}if(arg.startsWith("--source=")){let value=arg.slice(9);if(!ALLOWED_SOURCES.includes(value))return{status:"error",message:`Invalid --source value: '${value}' (allowed: ${ALLOWED_SOURCES.join(", ")}).`};sources.push(value);continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};positionals.push(arg)}return positionals.length===0?{status:"error",message:"Missing subcommand. Expected: migrate-agent-config."}:positionals.length>1?{status:"error",message:`Unexpected extra argument: '${positionals[1]}'.`}:positionals[0]!=="migrate-agent-config"?{status:"error",message:`Unknown subcommand: '${positionals[0]}'. Expected: migrate-agent-config.`}:{status:"ok",subcommand:"migrate-agent-config",writeCredentials,sources}}function promptChoiceViaReadline(candidates){return new Promise(resolve2=>{let rl=
|
|
4782
|
+
`)}catch{console.log("To continue, please reload your agent session manually.")}return 0}init_credential_store();import{readFile as readFile12,mkdir as mkdir8,writeFile as writeFile8,rename as rename2,chmod as chmod2,unlink as unlink2}from"fs/promises";import os15 from"os";import readline4 from"readline";init_credential_store();init_start_tickets_repo();import path28 from"path";async function readAgentMcpConfigIfPresent(filePath,readFile15){let raw;try{raw=await readFile15(filePath)}catch(err){return(err&&typeof err=="object"?err.code:void 0)==="ENOENT"?{state:"missing"}:{state:"error",error:`Unable to read agent MCP config at ${filePath}.`}}let json;try{json=JSON.parse(raw)}catch{return{state:"error",error:`Agent MCP config at ${filePath} is not valid JSON.`}}return{state:"present",json}}function isPlainObject2(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function extractBapiApiKeyCandidates(filePath,json){let candidates=[];if(!isPlainObject2(json))return candidates;for(let topLevelKey of["mcpServers","servers"]){let serversBlock=json[topLevelKey];if(isPlainObject2(serversBlock))for(let[serverName,serverConfig]of Object.entries(serversBlock)){if(!isPlainObject2(serverConfig))continue;let env=serverConfig.env;if(!isPlainObject2(env))continue;let rawKey=env.BAPI_API_KEY;if(typeof rawKey!="string")continue;let apiKey=rawKey.trim();apiKey.length!==0&&candidates.push({filePath,serverName,topLevelKey,apiKey})}}return candidates}var AGENT_CONFIG_SOURCE_NAMES=[".mcp.json",".cursor/mcp.json"];function resolveAgentConfigScanTargets(cwd,sources){return(sources&&sources.length>0?AGENT_CONFIG_SOURCE_NAMES.filter(name=>sources.includes(name)):AGENT_CONFIG_SOURCE_NAMES).map(name=>({name,filePath:name===".mcp.json"?path28.join(cwd,".mcp.json"):path28.join(cwd,".cursor","mcp.json")}))}async function scanAgentMcpConfigsForBapiApiKey(deps){let targets=resolveAgentConfigScanTargets(deps.cwd,deps.sources),candidates=[];for(let{filePath}of targets){let result=await readAgentMcpConfigIfPresent(filePath,deps.readFile);result.state==="present"&&candidates.push(...extractBapiApiKeyCandidates(filePath,result.json))}return candidates}function classifyAgentConfigCredentialCandidates(candidates){if(candidates.length===0)return{kind:"none"};let firstValue=candidates[0].apiKey;return candidates.every(c=>c.apiKey===firstValue)?{kind:"single-value",value:firstValue,candidates}:{kind:"conflicting-values",candidates}}function describeScannedFiles(cwd,sources){return resolveAgentConfigScanTargets(cwd,sources).map(t=>t.filePath).join(" and ")}function describeCandidateSources(candidates){return candidates.map(c=>`${c.serverName} in ${c.filePath}`).join(", ")}async function migrateAgentConfigCredentialToStore(deps){let repoResult=await resolveRequiredStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!repoResult.ok)return{ok:!1,kind:"repo-missing",message:repoResult.error};let candidates=await scanAgentMcpConfigsForBapiApiKey({cwd:deps.cwd,readFile:deps.readFile,sources:deps.sources}),classification=classifyAgentConfigCredentialCandidates(candidates);if(classification.kind==="none")return{ok:!1,kind:"no-candidates",message:`No BAPI_API_KEY found in ${describeScannedFiles(deps.cwd,deps.sources)}.`};if(!deps.writeCredentials)return{ok:!1,kind:"consent-required",message:`Found a BAPI_API_KEY to migrate (${describeCandidateSources(classification.candidates)}). Re-run with --write-credentials to store it in the user-scoped credential store.`,candidates:classification.candidates};let chosenCandidate;if(classification.kind==="conflicting-values"){if(!deps.promptChoice)return{ok:!1,kind:"conflicting-values",message:`Found conflicting BAPI_API_KEY values across ${describeCandidateSources(classification.candidates)}. Re-run interactively to choose, or reconcile the configs so they agree.`,candidates:classification.candidates};let chosenIndex=await deps.promptChoice(classification.candidates);if(chosenIndex===null)return{ok:!1,kind:"aborted",message:"Migration aborted: no credential source was chosen."};let picked=classification.candidates[chosenIndex];if(!picked)return{ok:!1,kind:"aborted",message:"Migration aborted: the chosen credential source was out of range."};chosenCandidate=picked}else chosenCandidate=classification.candidates[0];let result=await upsertBapiCredential(repoResult.repoName,chosenCandidate.apiKey,deps);return result.ok?{ok:!0,action:result.action,target:result.target,path:result.path,sourceFilePath:chosenCandidate.filePath,sourceServerName:chosenCandidate.serverName}:{ok:!1,kind:result.kind,message:result.error}}var ALLOWED_SOURCES=[".mcp.json",".cursor/mcp.json"];function getCredentialsUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server credentials migrate-agent-config \\"," [--write-credentials|--no-write-credentials] \\"," [--source=.mcp.json|--source=.cursor/mcp.json]","","Migrates a BAPI_API_KEY found in .mcp.json / .cursor/mcp.json into the","user-scoped credential store (~/.config/bridge/credentials.json), so that a","Bash-spawned CLI (e.g. start-tickets) can resolve it. The key value is never","printed.","","Without --write-credentials this is a dry preview: it scans and reports what","it WOULD migrate but writes nothing.","","Flags:"," --write-credentials Consent to write the discovered key into the store"," --no-write-credentials Dry preview only (default)"," --source=<file> Restrict scanning (repeatable):"," .mcp.json or .cursor/mcp.json"," -h, --help Show this help"].join(`
|
|
4783
|
+
`)}function parseCredentialsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help"};let positionals=[],writeCredentials=!1,sources=[];for(let arg of argv){if(arg==="--write-credentials"){writeCredentials=!0;continue}if(arg==="--no-write-credentials"){writeCredentials=!1;continue}if(arg.startsWith("--source=")){let value=arg.slice(9);if(!ALLOWED_SOURCES.includes(value))return{status:"error",message:`Invalid --source value: '${value}' (allowed: ${ALLOWED_SOURCES.join(", ")}).`};sources.push(value);continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};positionals.push(arg)}return positionals.length===0?{status:"error",message:"Missing subcommand. Expected: migrate-agent-config."}:positionals.length>1?{status:"error",message:`Unexpected extra argument: '${positionals[1]}'.`}:positionals[0]!=="migrate-agent-config"?{status:"error",message:`Unknown subcommand: '${positionals[0]}'. Expected: migrate-agent-config.`}:{status:"ok",subcommand:"migrate-agent-config",writeCredentials,sources}}function promptChoiceViaReadline(candidates){return new Promise(resolve2=>{let rl=readline4.createInterface({input:process.stdin,output:process.stderr});process.stderr.write(`Multiple, conflicting BAPI_API_KEY values were found. Choose a source:
|
|
4740
4784
|
`),candidates.forEach((c,i)=>{process.stderr.write(` [${i}] ${c.serverName} in ${c.filePath}
|
|
4741
4785
|
`)}),rl.question("Enter the number to migrate (or blank to abort): ",answer=>{rl.close();let trimmed=answer.trim();if(trimmed.length===0){resolve2(null);return}let index=Number.parseInt(trimmed,10);if(Number.isInteger(index)&&index>=0&&index<candidates.length){resolve2(index);return}resolve2(null)})})}function createDefaultCredentialsDeps(writeCredentials){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os15.homedir,readFile:p=>readFile12(p,"utf-8"),mkdir:(p,o)=>mkdir8(p,o),writeFile:(p,d,o)=>writeFile8(p,d,o),rename:(a,b)=>rename2(a,b),chmod:(p,m)=>chmod2(p,m),unlink:p=>unlink2(p),writeCredentials,promptChoice:process.stdin.isTTY?promptChoiceViaReadline:void 0,log:m=>console.log(m),errorLog:m=>console.error(m)}}async function runCredentialsCli(argv,overrides){let parsed=(overrides?.parse??parseCredentialsArgs)(argv),log=overrides?.log??(m=>console.log(m)),errorLog=overrides?.errorLog??(m=>console.error(m));if(parsed.status==="help")return log(getCredentialsUsage()),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getCredentialsUsage()),1;let deps={...createDefaultCredentialsDeps(parsed.writeCredentials),...overrides};overrides?.writeCredentials===void 0&&(deps.writeCredentials=parsed.writeCredentials),overrides?.sources===void 0&&(deps.sources=parsed.sources);let result=await migrateAgentConfigCredentialToStore(deps);if(result.ok)return deps.log(`Stored routing credential for ${result.target} at ${result.path} (migrated from ${result.sourceServerName} in ${result.sourceFilePath}).`),0;if(result.kind==="consent-required"){if(deps.log(result.message),deps.log(""),deps.log("To migrate it, re-run with --write-credentials:"),deps.log(" npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials"),result.candidates&&result.candidates.length>0){deps.log(""),deps.log("Discovered source(s):");for(let candidate of result.candidates)deps.log(` - ${candidate.serverName} in ${candidate.filePath}`)}return 0}return deps.errorLog(`Error: ${result.message}`),1}init_taxonomy();init_errors();init_store();init_git_ci_types();init_pr_ci_producer();import{z as z2}from"zod";init_errors();init_producer_ledger();import*as nodeChildProcess from"node:child_process";import{isAbsolute as pathIsAbsolute}from"node:path";var CONDUCTOR_NODE_PATH_ENV="CONDUCTOR_NODE_PATH",BAPI_CONDUCTOR_CLI_FILE_ENV="BAPI_CONDUCTOR_CLI_FILE",WORKER_LEDGER_CLI_MAX_BUFFER=8*1024*1024,WORKER_LEDGER_CLI_TIMEOUT_MS=3e4,defaultExecFile=(file,args,options,callback)=>nodeChildProcess.execFile(file,args,options,callback);function nonEmpty2(value){return typeof value=="string"&&value.trim().length>0}function resolveWorkerLedgerCliRuntime(deps={}){let env=deps.env??process.env,isAbsolute2=deps.isAbsolute??pathIsAbsolute,nodePathRaw=env[CONDUCTOR_NODE_PATH_ENV];if(!nonEmpty2(nodePathRaw))throw new ConductorLedgerSubprocessRuntimeError("missing",CONDUCTOR_NODE_PATH_ENV);let nodePath=nodePathRaw.trim();if(!isAbsolute2(nodePath))throw new ConductorLedgerSubprocessRuntimeError("invalid",CONDUCTOR_NODE_PATH_ENV);let cliFileRaw=env[BAPI_CONDUCTOR_CLI_FILE_ENV];if(!nonEmpty2(cliFileRaw))throw new ConductorLedgerSubprocessRuntimeError("cli_missing",BAPI_CONDUCTOR_CLI_FILE_ENV);return{nodePath,cliFile:cliFileRaw.trim()}}function execConductorCli(subcommandArgs,stdin,deps={}){let runtime=resolveWorkerLedgerCliRuntime(deps),execFile7=deps.execFile??defaultExecFile,maxBuffer=deps.maxBuffer??WORKER_LEDGER_CLI_MAX_BUFFER,timeout=deps.timeout??WORKER_LEDGER_CLI_TIMEOUT_MS,argv=[runtime.cliFile,...subcommandArgs];return new Promise((resolve2,reject)=>{let child;try{child=execFile7(runtime.nodePath,argv,{maxBuffer,encoding:"utf8",timeout},(error,stdout)=>{if(error){reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",CONDUCTOR_NODE_PATH_ENV));return}resolve2(stdout)})}catch{reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",CONDUCTOR_NODE_PATH_ENV));return}if(stdin!==void 0){child.stdin?.on("error",()=>{});try{child.stdin?.write(stdin),child.stdin?.end()}catch{}}})}function parseCliJsonStdout(stdout){try{return JSON.parse(stdout)}catch{throw new ConductorLedgerSubprocessRuntimeError("malformed_stdout",CONDUCTOR_NODE_PATH_ENV)}}async function checkWorkerMessagesViaCli(input,deps={}){let args=["check-messages","--run-id",input.runId,"--worker-id",input.workerId];input.limit!==void 0&&args.push("--limit",String(input.limit)),args.push("--json");let stdout=await execConductorCli(args,void 0,deps);return parseCliJsonStdout(stdout)}function buildEmitEventArgs(event){let args=["emit-event","--type",event.type,"--source",event.source];return nonEmpty2(event.id??void 0)&&args.push("--id",event.id),nonEmpty2(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty2(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty2(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty2(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty2(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),event.schema_version!==void 0&&args.push("--schema-version",String(event.schema_version)),nonEmpty2(event.time??void 0)&&args.push("--time",event.time),typeof event.confidence=="number"&&args.push("--confidence",String(event.confidence)),args.push("--data-json-stdin","--json"),args}async function emitConductorEventViaCli(event,deps={}){let args=buildEmitEventArgs(event),stdinPayload=JSON.stringify(event.data??{}),stdout=await execConductorCli(args,stdinPayload,deps);return parseCliJsonStdout(stdout)}async function emitConductorEventIfNewViaCli(input,dimensions,deps={}){let dedupeKey=makeProducerDedupeKey(dimensions),eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}},result=await emitConductorEventViaCli({...input,id:eventId,data},deps);return result&&result.ok===!1&&result.reason==="duplicate"?{emitted:!1,reason:"duplicate"}:{emitted:!0,event_id:eventId}}init_bridge_api_client();function buildEventTypeZodEnum(){return z2.enum(SEMANTIC_EVENT_TYPES)}var EventFilterSchema=z2.object({type:buildEventTypeZodEnum().optional(),types:z2.array(buildEventTypeZodEnum()).optional(),source:z2.string().optional(),run_id:z2.string().optional(),worker_id:z2.string().optional(),subject:z2.string().optional(),producer:z2.string().optional()}).strict();function jsonResult(value){return{content:[{type:"text",text:JSON.stringify(value,null,2)}]}}function withConductorToolErrorHandling(handler){return async args=>{try{return await handler(args)}catch(error){return jsonResult(toConductorErrorEnvelope(error))}}}function registerEmitEventTool(registerTool2){registerTool2("emit_event",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},description:"Append a semantic coordination event to the LOCAL conductor ledger (~/.config/bridge/events.db). This is a local, append-only event store for multi-agent coordination \u2014 it does NOT call the Bridge API. Only the fixed semantic event taxonomy is accepted (e.g. run.started, agent.notification, ci.passed). Place tool-native fields (branch, commitSha, etc.) under data.raw \u2014 non-allowlisted top-level data keys are rejected. Secrets are redacted before storage and large payloads must be passed by reference (data.payload_ref / data.references).",inputSchema:{source:z2.string().describe("Logical producer of the event (e.g. 'claude-code', 'git-hook')."),type:buildEventTypeZodEnum().describe("Semantic event type from the fixed conductor taxonomy."),subject:z2.string().optional().describe("Optional subject the event is about (e.g. a ticket key)."),run_id:z2.string().optional().describe("Optional run/session identifier this event belongs to."),worker_id:z2.string().optional().describe("Optional worker/agent identifier."),producer:z2.string().optional().describe("Optional finer-grained producer identity."),schema_version:z2.number().int().positive().optional().describe("Event schema version (default 1)."),time:z2.string().optional().describe("Optional ISO-8601 event time (defaults to now)."),data:z2.record(z2.string(),z2.unknown()).optional().describe("Normalized event data. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw. Tool-native fields go under 'raw'."),confidence:z2.number().min(0).max(1).optional().describe("Optional confidence in [0,1]."),observed_via:z2.string().optional().describe("Optional channel the event was observed through.")}},withConductorToolErrorHandling(async args=>{let result=await emitConductorEvent({source:args.source,type:args.type,subject:args.subject,run_id:args.run_id,worker_id:args.worker_id,producer:args.producer,schema_version:args.schema_version,time:args.time,data:args.data??{},confidence:args.confidence,observed_via:args.observed_via});return jsonResult(result)}))}function registerPollEventsTool(registerTool2){registerTool2("poll_events",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Read ordered events from the LOCAL conductor ledger starting at an inclusive 'since_seq' cursor. Returns compact metadata-first summaries by default (data.raw omitted; raw_keys surfaced) and a 'next_seq' cursor to pass on the next call. Set data_mode='full' to retrieve complete (redacted) event data. Local read-only; does not call the Bridge API.",inputSchema:{since_seq:z2.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),filter:EventFilterSchema.optional().describe("Optional allowlisted filter."),data_mode:z2.enum(["summary","full"]).optional().describe("Projection mode (default 'summary')."),limit:z2.number().int().positive().optional().describe("Max events to return (default 100, max 1000).")}},withConductorToolErrorHandling(async args=>{let result=await pollConductorEvents({since_seq:args.since_seq??1,filter:args.filter,data_mode:args.data_mode??"summary",limit:args.limit});return jsonResult(result)}))}function registerWaitForEventTool(registerTool2){registerTool2("wait_for_event",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Long-poll the LOCAL conductor ledger: block up to 'timeout_ms' (bounded, max 120000) until events matching the filter appear at/after 'since_seq'. Returns the same shape as poll_events plus 'timed_out'. SQLite locks are never held between polls. Local read-only; does not call the Bridge API.",inputSchema:{since_seq:z2.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),filter:EventFilterSchema.optional().describe("Optional allowlisted filter."),data_mode:z2.enum(["summary","full"]).optional().describe("Projection mode (default 'summary')."),timeout_ms:z2.number().int().nonnegative().optional().describe("Max wait in ms (bounded, max 120000)."),limit:z2.number().int().positive().optional().describe("Max events to return (default 100, max 1000).")}},withConductorToolErrorHandling(async args=>{let result=await waitForConductorEvent({since_seq:args.since_seq??1,filter:args.filter,data_mode:args.data_mode??"summary",timeout_ms:args.timeout_ms,limit:args.limit});return jsonResult(result)}))}function registerGetSupervisorSnapshotTool(registerTool2){registerTool2("get_supervisor_snapshot",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Read the supervisor projection for a run_id from the LOCAL conductor ledger. The projection is maintained by the conductor supervisor runtime (`conductor supervise --run-id <id>`), which owns the deterministic worker watchdog state; this tool ONLY reads that projection and never derives state from raw events. Returns { run_id, status, projection } where projection is null and status is 'unknown' when no projection exists yet. Local read-only; does not call the Bridge API.",inputSchema:{run_id:z2.string().describe("The run/session identifier to read the supervisor projection for.")}},withConductorToolErrorHandling(async args=>{let result=await getSupervisorSnapshot(args.run_id);return jsonResult(result)}))}function registerGetEpicSnapshotTool(registerTool2){registerTool2("get_epic_snapshot",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the Epic Run snapshot for an epic key from the Bridge API. Returns the full EpicRunState: the epic run record (status, plan version, lease state, budget/consumed), per-ticket status rows, and dispatch rows. Returns { epic_key, status: 'unknown', state: null } if the Epic Run does not exist. Read-only; never triggers a tick, transitions Jira, or mutates Epic state.",inputSchema:{epic_key:z2.string().min(1).describe("The epic identifier to fetch the snapshot for (e.g. EPIC-123).")}},withConductorToolErrorHandling(async args=>{let epicKey=args.epic_key,accessResult=await resolveConductorBridgeApiAccess();if(!accessResult.ok)throw new ConductorValidationError(accessResult.error);try{let result=await fetchEpicRunState(accessResult.access,epicKey);return jsonResult(result)}catch(error){if(error instanceof ConductorBridgeApiError&&error.status===404)return jsonResult({epic_key:epicKey,status:"unknown",state:null});throw error}}))}var SHA_PATTERN=/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/;function registerWaitForDoneGateTool(registerTool2){registerTool2("wait_for_done_gate",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Bounded wait for the conductor done-gate on a pull request. Resolves the PR number + immutable head SHA once, polls CI for that SHA on a clamped interval, and emits conductor events (git.pr_opened, ci.passed/ci.failed, and gate.met when the configured required CI checks are green). This EMITS conductor coordination events only \u2014 it does NOT merge the PR, transition Jira, or mutate any repository state. Fails closed: an unset/disabled/malformed conductor_done_gate config never produces gate.met.",inputSchema:{repo_name:z2.string().optional().describe("Optional repo name override (defaults to BAPI_REPO_NAME/.bridge/config)."),pr_number:z2.number().int().positive().optional().describe("Optional explicit PR number (positive integer)."),head_sha:z2.string().regex(SHA_PATTERN).optional().describe("Optional explicit head SHA (40- or 64-character hex)."),timeout_ms:z2.number().int().nonnegative().optional().describe("Max wait in ms (clamped, max 120000)."),poll_interval_ms:z2.number().int().nonnegative().optional().describe("CI poll interval in ms (clamped)."),worktree_path:z2.string().optional().describe("Optional worktree path to resolve git/PR context from.")}},withConductorToolErrorHandling(async args=>{if(args.pr_number!==void 0&&normalizePrNumber(args.pr_number)===null)throw new ConductorValidationError("'pr_number' must be a positive integer.");if(args.head_sha!==void 0&&normalizeSha(args.head_sha)===null)throw new ConductorValidationError("'head_sha' must be a 40- or 64-character hex SHA.");let result=await waitForDoneGate({repoName:args.repo_name,prNumber:args.pr_number,headSha:args.head_sha,timeoutMs:args.timeout_ms,pollIntervalMs:args.poll_interval_ms,worktreePath:args.worktree_path},{resolveRunId:resolveDispatchRunIdForBinding,emitIfNew:(input,dimensions)=>emitConductorEventIfNewViaCli(input,dimensions)});return jsonResult({gate_met:result.gate_met,timed_out:result.timed_out,reason:result.reason,repo:result.repo,pr_number:result.pr_number,head_sha:result.head_sha,gate_event_summary:result.gate_event_summary})}))}function registerSendMessageTool(registerTool2){registerTool2("send_message",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Enqueue a typed, auditable message for ONE worker through the LOCAL cooperative conductor relay (~/.config/bridge/events.db). The supervisor sends; the worker reads/acknowledges later via check_messages. This is COOPERATIVE \u2014 it does NOT inject into, mutate, or prompt-inject a live worker session. Idempotent: a duplicate idempotency key (run_id+worker_id+type+cause_seq) does not enqueue a second message, and a same-type message inside the cooldown window is suppressed. Local only; does not call the Bridge API.",inputSchema:{run_id:z2.string().min(1).describe("Run/session identifier the message is scoped to."),worker_id:z2.string().min(1).describe("Target worker/agent identifier."),type:z2.string().min(1).describe("Typed message kind (e.g. 'supervisor.worker_stalled')."),cause_seq:z2.number().int().nonnegative().describe("Idempotency cause sequence (the supervisor's last_seq at decision time)."),payload:z2.record(z2.string(),z2.unknown()).optional().default({}).describe("Optional compact payload. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw."),available_at:z2.string().optional().describe("Optional ISO-8601 time the message becomes available (default now)."),cooldown_ms:z2.number().int().nonnegative().optional().describe("Optional per-call cooldown override in ms (falls back to the configured cooldown).")}},withConductorToolErrorHandling(async args=>{let result=await sendWorkerMessage({run_id:args.run_id,worker_id:args.worker_id,type:args.type,cause_seq:args.cause_seq,payload:args.payload??{},available_at:args.available_at,cooldown_ms:args.cooldown_ms});return jsonResult(result)}))}function registerCheckMessagesTool(registerTool2){registerTool2("check_messages",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},description:"Worker checkpoint poll for the LOCAL cooperative conductor relay. Call this at natural checkpoints to read any supervisor messages addressed to this worker. Returned messages are ACKNOWLEDGED by this call and are NOT redelivered on later polls. This is cooperative polling \u2014 it is NOT live prompt injection. run_id/worker_id default to BAPI_CONDUCTOR_RUN_ID / BAPI_CONDUCTOR_WORKER_ID from the environment when omitted. Local only; does not call the Bridge API.",inputSchema:{run_id:z2.string().optional().describe("Run identifier (defaults to BAPI_CONDUCTOR_RUN_ID)."),worker_id:z2.string().optional().describe("Worker identifier (defaults to BAPI_CONDUCTOR_WORKER_ID)."),limit:z2.number().int().positive().max(100).optional().describe("Max messages to deliver/ack (default 10, max 100).")}},withConductorToolErrorHandling(async args=>{let runId=args.run_id??process.env.BAPI_CONDUCTOR_RUN_ID??"",workerId=args.worker_id??process.env.BAPI_CONDUCTOR_WORKER_ID??"";if(runId.trim().length===0||workerId.trim().length===0)throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");let result=await checkWorkerMessagesViaCli({runId,workerId,limit:args.limit});return jsonResult(result)}))}function registerConductorTools(registerTool2){let reg=registerTool2;registerEmitEventTool(reg),registerPollEventsTool(reg),registerWaitForEventTool(reg),registerGetSupervisorSnapshotTool(reg),registerGetEpicSnapshotTool(reg),registerWaitForDoneGateTool(reg),registerSendMessageTool(reg),registerCheckMessagesTool(reg)}import{z as z14}from"zod";var SFCC_VERSIONS=["sfra","pwakit","sitegenesis","storefrontnext","hybrid"],DEFAULT_OCAPI_VERSION="v25_6",AM_HOST="account.demandware.com",AM_TOKEN_URL=`https://${AM_HOST}/dwsso/oauth2/access_token`;async function getSfccVersionConfig(buildGetUrl2,getGetHeaders2,repoName){try{let url=buildGetUrl2("/config-field/version",{repo_name:repoName}),resp=await fetch(url,{headers:await getGetHeaders2()});if(!resp.ok)return null;let value=(await resp.json()).value;return value==null||typeof value!="string"?null:value}catch{return null}}import{readFile as readFile13,writeFile as writeFile9,mkdir as mkdir9}from"fs/promises";import path29 from"path";var ENV_HOSTNAME="SFCC_HOSTNAME",ENV_CLIENT_ID="SFCC_CLIENT_ID",ENV_CLIENT_SECRET="SFCC_CLIENT_SECRET",DW_JSON="dw.json";function safeHostLabel(hostname){return hostname.split(".")[0]??hostname}async function resolveSfccCredentials(explicitHostname,env=process.env,deps={}){if(explicitHostname){let clientId2=env[ENV_CLIENT_ID],clientSecret2=env[ENV_CLIENT_SECRET];return!clientId2||!clientSecret2?{ok:!1,error:`Explicit instance '${safeHostLabel(explicitHostname)}' provided but ${ENV_CLIENT_ID} and/or ${ENV_CLIENT_SECRET} are not set in environment. Set them and retry.`}:{ok:!0,credentials:{hostname:explicitHostname,clientId:clientId2,clientSecret:clientSecret2,source:`explicit arg + env (${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`}}}let envHostname=env[ENV_HOSTNAME],envClientId=env[ENV_CLIENT_ID],envClientSecret=env[ENV_CLIENT_SECRET];if(envHostname&&envClientId&&envClientSecret)return{ok:!0,credentials:{hostname:envHostname,clientId:envClientId,clientSecret:envClientSecret,source:`env (${ENV_HOSTNAME}/${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`}};let cwd=deps.cwd??process.cwd(),rf=deps.readFile??(p=>readFile13(p,"utf-8")),wf=deps.writeFile??((p,data)=>writeFile9(p,data,"utf-8")),mk=deps.mkdir??((p,opts)=>mkdir9(p,opts));try{await ensureGitInfoExcluded(cwd,DW_JSON,{readFile:rf,writeFile:wf,mkdir:mk})}catch{}let dwJsonPath=path29.join(cwd,DW_JSON),dwJson;try{let raw=await rf(dwJsonPath);dwJson=JSON.parse(raw)}catch{return{ok:!1,error:"Could not read dw.json. Create a dw.json file in your project root with your SFCC sandbox credentials (hostname, client-id, client-secret)."}}let configs=Array.isArray(dwJson.configs)?dwJson.configs:null;if(configs&&configs.length>1){let instances=configs.map(c=>safeHostLabel(String(c.hostname??c.host??"unknown"))).join(", ");return{ok:!1,error:`dw.json contains multiple sandboxes (${instances}). Pass an explicit 'instance' argument to select one: ${instances}.`}}let cfg=configs&&configs.length===1?configs[0]:dwJson,hostname=String(cfg.hostname??cfg.host??""),clientId=String(cfg["client-id"]??cfg.clientId??cfg.client_id??""),clientSecret=String(cfg["client-secret"]??cfg.clientSecret??cfg.client_secret??"");return!hostname||!clientId||!clientSecret?{ok:!1,error:"dw.json is present but missing required fields (hostname, client-id, client-secret). Ensure all three fields are set."}:{ok:!0,credentials:{hostname,clientId,clientSecret,source:`dw.json (instance: ${safeHostLabel(hostname)})`}}}var KNOWN_WRITE_FAULTS={400:"MalformedKeyParameterException",404:"AttributeDefinitionNotFoundException",409:"IfMatchRequiredException",412:"InvalidIfMatchException"};function extractOcapiFaultType(body){if(body===null||typeof body!="object")return;let record=body,fault=record.fault;if(typeof fault=="string"&&fault.length>0)return fault;if(fault!==null&&typeof fault=="object"){let faultType=fault.type;if(typeof faultType=="string"&&faultType.length>0)return faultType}let type=record.type;if(typeof type=="string"&&type.length>0)return type}function mapOcapiWriteFault(status,body){let faultType=extractOcapiFaultType(body),expected=KNOWN_WRITE_FAULTS[status],known=expected!==void 0&&faultType===expected;return{status,faultType,known,errorCode:known?expected:"OCAPI_WRITE_FAULT"}}function buildSyntheticIfMatchRequiredBody(path37){return{fault:{type:"IfMatchRequiredException",message:`PATCH ${path37} requires an ETag (If-Match) captured from the GET round trip, but the GET response returned no ETag header. Cannot safely issue a conditional PATCH.`}}}var tokenMutex=new Map,tokenCache=new Map;async function getAmToken(credentials){let instanceKey=credentials.hostname,cached=tokenCache.get(instanceKey);if(cached)return cached;let existing=tokenMutex.get(instanceKey);if(existing)return existing;let promise=(async()=>{let body=new URLSearchParams({grant_type:"client_credentials",client_id:credentials.clientId,client_secret:credentials.clientSecret}),resp=await fetch(AM_TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:body.toString()});if(!resp.ok)throw new Error(`AM token acquisition failed: HTTP ${resp.status} for instance ${instanceKey.split(".")[0]}`);let data=await resp.json(),token=typeof data.access_token=="string"?data.access_token:"";if(!token)throw new Error(`AM token acquisition returned no access_token for instance ${instanceKey.split(".")[0]}`);return tokenCache.set(instanceKey,token),token})();tokenMutex.set(instanceKey,promise);try{return await promise}finally{tokenMutex.delete(instanceKey)}}function invalidateAmToken(credentials){tokenCache.delete(credentials.hostname)}function buildOcapiUrl(hostname,ocapiVersion,path37){return`${`https://${hostname}/s/-/dw/data/${ocapiVersion}`}${path37.startsWith("/")?path37:"/"+path37}`}async function parseOcapiResponse(resp){try{return await resp.json()}catch{return null}}function captureOcapiHeaders(resp){let headers={},respHeaders=resp.headers;return respHeaders&&typeof respHeaders.forEach=="function"?(respHeaders.forEach((value,key)=>{headers[key.toLowerCase()]=value}),{headers,etag:respHeaders.get("etag")}):{headers,etag:null}}function sleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}var MAX_RETRY_AFTER_MS=5e3;function parseRetryAfterMs2(headerValue){if(!headerValue)return;let trimmed=headerValue.trim();if(trimmed==="")return;if(/^\d+$/.test(trimmed))return Number(trimmed)*1e3;let dateMs=Date.parse(trimmed);if(!Number.isNaN(dateMs)){let delta=dateMs-Date.now();return delta>0?delta:0}}var BACKOFF_SCHEDULE_MS=[250,500,1e3];async function fetchWith429Backoff(url,init){let resp=await fetch(url,init);for(let attempt=0;attempt<BACKOFF_SCHEDULE_MS.length;attempt++){if(resp.status!==429)return resp;let retryAfterHeader=resp.headers&&typeof resp.headers.get=="function"?resp.headers.get("retry-after"):null,retryAfter=parseRetryAfterMs2(retryAfterHeader),backoff=retryAfter!==void 0?Math.min(retryAfter,MAX_RETRY_AFTER_MS):BACKOFF_SCHEDULE_MS[attempt];await sleep2(backoff),resp=await fetch(url,init)}return resp}async function ocapiRequest(method,path37,body,credentials,ocapiVersion,extraHeaders){let doRequest=async()=>{let token=await getAmToken(credentials),url=buildOcapiUrl(credentials.hostname,ocapiVersion,path37),init={method,headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/json",...extraHeaders??{}}};body!==void 0&&(init.body=JSON.stringify(body));let resp=await fetchWith429Backoff(url,init),status=resp.status,respBody=await parseOcapiResponse(resp),{headers,etag}=captureOcapiHeaders(resp),result={ok:resp.ok,status,body:respBody,headers,etag};return(method==="PUT"||method==="PATCH")&&resp.ok&&(status===201?result.outcome="created":status===200&&(result.outcome="updated")),resp.ok||(result.fault=mapOcapiWriteFault(status,respBody)),result},first=await doRequest();return first.status===401?(invalidateAmToken(credentials),doRequest()):first}async function ocapiGet(path37,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("GET",path37,void 0,credentials,ocapiVersion)}async function ocapiPost(path37,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("POST",path37,body,credentials,ocapiVersion)}async function ocapiPut(path37,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("PUT",path37,body,credentials,ocapiVersion)}async function ocapiPatch(path37,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){let getResult=await ocapiGet(path37,credentials,ocapiVersion);if(!getResult.ok)return getResult;let etag=getResult.etag;if(etag==null||etag.trim()===""){let syntheticBody=buildSyntheticIfMatchRequiredBody(path37);return{ok:!1,status:409,body:syntheticBody,fault:mapOcapiWriteFault(409,syntheticBody)}}return ocapiRequest("PATCH",path37,body,credentials,ocapiVersion,{"If-Match":etag})}async function ocapiPatchDirect(path37,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("PATCH",path37,body,credentials,ocapiVersion)}async function sfccSetupStatusTool(deps){let lines=[`## SFCC Setup Status
|
|
4742
4786
|
`],apiKeyOk=!!deps.apiKey;lines.push(`1. Bridge API Key: ${apiKeyOk?"\u2713 Resolved":"\u2717 Missing (set BAPI_API_KEY)"}`);let repoOk=!!deps.repoName;lines.push(`2. Repo Name: ${repoOk?`\u2713 Set (${deps.repoName})`:"\u2717 Not set (set BAPI_REPO_NAME)"}`);let versionStatus="\u2717 Not set",resolvedVersion=null;if(apiKeyOk&&repoOk)try{resolvedVersion=await getSfccVersionConfig(deps.buildGetUrl,deps.getGetHeaders,deps.repoName),resolvedVersion===null?versionStatus="\u2717 Not set (configure the 'version' field in Bridge API project settings)":SFCC_VERSIONS.includes(resolvedVersion)?versionStatus=`\u2713 '${resolvedVersion}'`:versionStatus=`\u2717 '${resolvedVersion}' is not an SFCC version (expected: ${SFCC_VERSIONS.join(", ")})`}catch{versionStatus="\u2717 Could not read (Bridge API error)"}else versionStatus="\u2014 Skipped (Bridge API not configured)";lines.push(`3. SFCC Version: ${versionStatus}`);let credStatus="\u2717 Missing",resolvedCredentials=null;try{let result=await resolveSfccCredentials();result.ok?(resolvedCredentials={hostname:result.credentials.hostname.split(".")[0]??result.credentials.hostname,source:result.credentials.source},credStatus=`\u2713 Found (${resolvedCredentials.source})`):credStatus=`\u2717 ${result.error}`}catch(err){credStatus=`\u2717 Resolution error: ${err instanceof Error?err.message:String(err)}`}lines.push(`4. dw.json / Credentials: ${credStatus}`);let tokenStatus="\u2014 Skipped (credentials not available)";if(resolvedCredentials)try{let credResult=await resolveSfccCredentials();credResult.ok?(await getAmToken(credResult.credentials),tokenStatus=`\u2713 Token acquired for instance ${resolvedCredentials.hostname}`):tokenStatus="\u2717 Credentials not resolved"}catch(err){tokenStatus=`\u2717 ${err instanceof Error?err.message:String(err)}`}lines.push(`5. AM Token (OCAPI): ${tokenStatus}`);let logQueryStatus="\u2014 Skipped (Bridge API not configured)";if(apiKeyOk&&repoOk)try{let url=deps.buildGetUrl("/sfcc/logs/capability",{repo_name:deps.repoName}),resp=await fetch(url,{headers:await deps.getGetHeaders()});if(!resp.ok)logQueryStatus=`\u2717 Could not read (Bridge API ${resp.status})`;else{let body=await resp.json();body?.configured===!0?logQueryStatus="\u2713 Configured (WebDAV log access ready)":logQueryStatus=`\u2717 ${typeof body?.message=="string"?body.message:"Not configured"}`}}catch(err){logQueryStatus=`\u2717 Resolution error: ${err instanceof Error?err.message:String(err)}`}return lines.push(`6. SFCC Log Query (WebDAV): ${logQueryStatus}`),lines.push("\nRun `check_permissions` to probe OCAPI access once steps 1\u20135 are all green. Step 6 (log/WebDAV access) is independent and gates `sfcc_log_query`."),{content:[{type:"text",text:lines.join(`
|
|
@@ -4812,7 +4856,7 @@ When done, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_ru
|
|
|
4812
4856
|
|
|
4813
4857
|
${command}
|
|
4814
4858
|
|
|
4815
|
-
When the worktrees have been spawned, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_run_id}" and \`agent_result\` set to a short summary of what start-tickets reported.`;return buildNeedsAgentTaskEnvelope({chainRunId:updated.chain_run_id,chainStage:START_TICKETS_PIPELINE,chainStep:idx+1,chainTotal:total,preamble:buildPreamble(recipe,idx,updated.stages),instruction})}function numericArg(value){if(typeof value=="number"&&Number.isFinite(value))return value}async function continueChainExecution(deps,persistence,recipe,row,autoApprove){let guard=0,guardMax=1e4;for(;guard++<guardMax;){let idx=row.current_stage_index,total=recipe.stages.length;if(idx>=total){try{row=await persistence.patchRun(row.chain_run_id,{status:"completed"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,row)}let stageRecipe=recipe.stages[idx],outcome2=null;if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE)return startStartTicketsStage(persistence,recipe,row);if(stageRecipe.fan_out_input?outcome2=await startOrContinueReviewTicketStage(deps,persistence,recipe,row,autoApprove):outcome2=await startOrContinueIdeaToTicketStage(deps,persistence,recipe,row,autoApprove),outcome2.kind==="pause"||outcome2.kind==="fail")return outcome2.envelope;row=outcome2.row}return failedEnvelope2("TOOL_ERROR","Chain execution exceeded its step guard.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length})}async function runFullAutomation(deps,input){try{if(typeof input.idea!="string"||input.idea.trim()==="")return failedEnvelope2("VALIDATION","idea must be a non-empty string.");let agent=input.agent??"claude";if(agent!=="claude")return failedEnvelope2("VALIDATION",`Unsupported agent "${String(input.agent)}". Only "claude" is supported.`);let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`);let autoApprove=input.auto_approve===void 0?!0:normalizeAutoApprove2(input.auto_approve),args={idea:input.idea,auto_approve:autoApprove,scheduled_at:input.scheduled_at??"",max_children:input.max_children,allow_duplicate:input.allow_duplicate,agent,ttl_seconds:input.ttl_seconds},initialStages=recipe.stages.map(stage=>({pipeline_name:stage.pipeline_name,status:"pending"})),persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.createRun({chain_name:CHAIN_NAME,args,current_stage_index:0,stages:initialStages,status:"running",ttl_seconds:input.ttl_seconds})}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while creating the chain run.")}return continueChainExecution(deps,persistence,recipe,row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in runFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while executing the full-automation chain.")}}async function resumeFullAutomation(deps,input){try{let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`,{chain_run_id:input.chain_run_id});let persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.getRun(input.chain_run_id)}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message,{chain_run_id:input.chain_run_id}):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while fetching the chain run.",{chain_run_id:input.chain_run_id})}if(row.status==="expired")return failedEnvelope2("EXPIRED","Chain run has expired.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length});let autoApprove=normalizeAutoApprove2(row.args.auto_approve),idx=row.current_stage_index,stageRecipe=recipe.stages[idx],total=recipe.stages.length;if(!stageRecipe)return failedEnvelope2("VALIDATION",`Chain run has no active stage at index ${idx}.`,{chain_run_id:row.chain_run_id,chain_total:total});if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE){if(typeof input.agent_result!="string"||input.agent_result.trim()==="")return failedEnvelope2("VALIDATION","agent_result must be a non-empty string to complete the start-tickets stage.",{chain_run_id:row.chain_run_id,chain_stage:START_TICKETS_PIPELINE,chain_step:idx+1,chain_total:total});let startResolution=resolveStartTicketKeys(row,idx,stageRecipe.fan_out_input??"reviewed_ticket_keys"),startedKeys=startResolution.ok?startResolution.keys:[],stages=cloneStages(row.stages);stages[idx].status="completed",stages[idx].pipeline_run_id=null,stages[idx].outputs={started_ticket_keys:startedKeys},stages[idx].summary=summarizeStageCompletion(START_TICKETS_PIPELINE,startedKeys);let updated;try{updated=await persistence.patchRun(row.chain_run_id,{stages,current_stage_index:idx+1,status:"completed",expected_status:"paused",expected_current_stage_index:idx})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,updated)}let activePipelineRunId=row.stages[idx]?.pipeline_run_id;if(!activePipelineRunId)return failedEnvelope2("VALIDATION",`No active child pipeline to resume for stage ${idx+1}.`,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});let peek=await peekPipelineRun(deps,activePipelineRunId);if("error_code"in peek)return failedEnvelope2(peek.error_code,peek.error,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});if(peek.status!=="paused"&&peek.status!=="completed"&&peek.status!=="failed")return{status:"failed",error_code:"VALIDATION",error:`Inner pipeline run is in status "${peek.status}" and cannot be safely resumed or recovered. Inspect pipeline_run_id ${activePipelineRunId}.`,chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total,pipeline_run_id:activePipelineRunId,resumable:!1};try{row=await persistence.patchRun(row.chain_run_id,{status:"running"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}let childEnv;if(peek.status==="paused")childEnv=await resumePipeline(deps,{pipeline_run_id:activePipelineRunId,agent_result:input.agent_result});else if(peek.status==="completed")childEnv={status:"completed",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,total_steps:peek.total_steps,results:peek.results};else{let failedStepError=peek.results.find(r=>!r.ok&&typeof r.error=="string")?.error;childEnv={status:"failed",error_code:"TOOL_ERROR",error:failedStepError?`Inner pipeline run failed before the chain could advance: ${failedStepError}`:"Inner pipeline run failed before the chain could advance.",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,results:peek.results}}let fanOut=!!stageRecipe.fan_out_input,childIndex=row.stages[idx]?.current_child_index??0,ticketKey=fanOut?(resolveCrossStageList(row,idx,stageRecipe.fan_out_input)??[])[childIndex]:void 0,outcome2=await handleChildPipelineEnvelope(persistence,recipe,row,childEnv,{fanOut,ticketKey,childIndex});return outcome2.kind==="pause"||outcome2.kind==="fail"?outcome2.envelope:continueChainExecution(deps,persistence,recipe,outcome2.row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in resumeFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while resuming the full-automation chain.",{chain_run_id:input.chain_run_id})}}import path35 from"path";import{Worker}from"worker_threads";import{PNG}from"pngjs";import pixelmatch from"pixelmatch";import{isMainThread,parentPort,workerData}from"worker_threads";var PIXELMATCH_COLOR_THRESHOLD=.1,DEFAULT_PASS_MISMATCH_PCT=2,MAX_DIFF_REGIONS=10;function decodePng(buffer,label){try{let png=PNG.sync.read(Buffer.from(buffer));return!Number.isInteger(png.width)||!Number.isInteger(png.height)||png.width<=0||png.height<=0?{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Decoded ${label} PNG has invalid dimensions.`}:{width:png.width,height:png.height,data:png.data}}catch{return{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Failed to decode ${label} image as PNG.`}}}function extractOverlap(src,srcW,overlapW,overlapH){let out=new Uint8Array(overlapW*overlapH*4);for(let y=0;y<overlapH;y++){let srcRow=y*srcW*4,dstRow=y*overlapW*4;out.set(src.subarray(srcRow,srcRow+overlapW*4),dstRow)}return out}function buildMaskGrid(boxes,unionW,unionH){let grid=new Uint8Array(unionW*unionH);for(let box of boxes){let x0=Math.max(0,Math.floor(box.x)),y0=Math.max(0,Math.floor(box.y)),x1=Math.min(unionW,Math.floor(box.x+box.width)),y1=Math.min(unionH,Math.floor(box.y+box.height));for(let y=y0;y<y1;y++)for(let x=x0;x<x1;x++)grid[y*unionW+x]=1}return grid}function applyMaskToOverlap(buf,overlapW,overlapH,maskGrid,unionW){for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++)if(maskGrid[y*unionW+x]===1){let off=(y*overlapW+x)*4;buf[off]=0,buf[off+1]=0,buf[off+2]=0,buf[off+3]=255}}function extractDiffRegions(mask,width,height,maxRegions){let visited=new Uint8Array(width*height),regions=[],stack=[];for(let start=0;start<mask.length;start++){if(mask[start]===0||visited[start]===1)continue;let minX=width,minY=height,maxX=-1,maxY=-1,pixels=0;for(stack.length=0,stack.push(start),visited[start]=1;stack.length>0;){let idx=stack.pop(),x=idx%width,y=(idx-x)/width;if(pixels++,x<minX&&(minX=x),y<minY&&(minY=y),x>maxX&&(maxX=x),y>maxY&&(maxY=y),x>0){let n=idx-1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(x<width-1){let n=idx+1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y>0){let n=idx-width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y<height-1){let n=idx+width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}}regions.push({x:minX,y:minY,width:maxX-minX+1,height:maxY-minY+1,pixels})}return regions.sort((a,b)=>b.pixels!==a.pixels?b.pixels-a.pixels:a.y!==b.y?a.y-b.y:a.x-b.x),regions.slice(0,Math.max(0,maxRegions))}function computePngVisualDiff(input){let comp=decodePng(input.compPngBuffer,"comp");if("ok"in comp&&comp.ok===!1)return comp;let render=decodePng(input.renderPngBuffer,"render");if("ok"in render&&render.ok===!1)return render;let compImg=comp,renderImg=render,dimensionMatch=compImg.width===renderImg.width&&compImg.height===renderImg.height,unionW=Math.max(compImg.width,renderImg.width),unionH=Math.max(compImg.height,renderImg.height),overlapW=Math.min(compImg.width,renderImg.width),overlapH=Math.min(compImg.height,renderImg.height),maskGrid=buildMaskGrid(input.maskBoxes??[],unionW,unionH),output=new Uint8Array(unionW*unionH*4),diffMask=new Uint8Array(unionW*unionH),differingPixels=0;if(overlapW>0&&overlapH>0){let compOverlap=extractOverlap(compImg.data,compImg.width,overlapW,overlapH),renderOverlap=extractOverlap(renderImg.data,renderImg.width,overlapW,overlapH);applyMaskToOverlap(compOverlap,overlapW,overlapH,maskGrid,unionW),applyMaskToOverlap(renderOverlap,overlapW,overlapH,maskGrid,unionW);let overlapOut=new Uint8Array(overlapW*overlapH*4);try{differingPixels=pixelmatch(compOverlap,renderOverlap,overlapOut,overlapW,overlapH,{threshold:input.pixelmatchColorThreshold,includeAA:!1,diffColor:[255,0,0],diffColorAlt:[255,0,0],aaColor:[255,255,0]})}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Pixel comparison failed."}}for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++){let so=(y*overlapW+x)*4,uo=(y*unionW+x)*4;output[uo]=overlapOut[so],output[uo+1]=overlapOut[so+1],output[uo+2]=overlapOut[so+2],output[uo+3]=255,overlapOut[so]===255&&overlapOut[so+1]===0&&overlapOut[so+2]===0&&(diffMask[y*unionW+x]=1)}}for(let y=0;y<unionH;y++)for(let x=0;x<unionW;x++){let inComp=x<compImg.width&&y<compImg.height,inRender=x<renderImg.width&&y<renderImg.height;if(inComp===inRender||maskGrid[y*unionW+x]===1)continue;let off=(y*unionW+x)*4;output[off]=255,output[off+1]=0,output[off+2]=0,output[off+3]=255,diffMask[y*unionW+x]=1,differingPixels++}let diffRegions=extractDiffRegions(diffMask,unionW,unionH,input.maxRegions??MAX_DIFF_REGIONS),totalPixels=unionW*unionH,mismatchPct=totalPixels>0?differingPixels/totalPixels*100:0,passed=dimensionMatch&&mismatchPct<=input.passMismatchPct,heatmapBase64;try{let png=new PNG({width:unionW,height:unionH});png.data=Buffer.from(output),heatmapBase64=PNG.sync.write(png).toString("base64")}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Failed to encode diff heatmap."}}return{ok:!0,mismatch_pct:mismatchPct,dimension_match:dimensionMatch,passed,diff_regions:diffRegions,comp_dimensions:{width:compImg.width,height:compImg.height},render_dimensions:{width:renderImg.width,height:renderImg.height},differing_pixels:differingPixels,total_pixels:totalPixels,heatmap_base64:heatmapBase64}}if(!isMainThread&&parentPort)try{let result=computePngVisualDiff(workerData);parentPort.postMessage(result)}catch(err){parentPort.postMessage({ok:!1,error:"DIFF_FAILED",status:500,message:`Diff worker failed: ${err instanceof Error?err.message:"unknown error"}`})}var NAV_TIMEOUT_MS=3e4,NETWORK_IDLE_TIMEOUT_MS=15e3,FONTS_READY_TIMEOUT_MS=5e3,SCREENSHOT_TIMEOUT_MS=2e4,MAX_VIEWPORT_DIMENSION=16384,MAX_VIEWPORT_PIXELS=32e6,MAX_COMP_BYTES=25*1024*1024,DETERMINISTIC_CSS="* { animation: none !important; transition: none !important; caret-color: transparent !important; }";function textJson(value){return{type:"text",text:JSON.stringify(value,null,2)}}function errorContent(error,status,message,extra){return{content:[{type:"text",text:JSON.stringify({error,status,message,...extra??{}})}]}}var PNG_MAGIC=[137,80,78,71,13,10,26,10];function sniffImageFormat(bytes){return bytes.length>=8&&PNG_MAGIC.every((b,i)=>bytes[i]===b)?"png":bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255?"jpeg":null}function readPngDimensions(bytes){if(bytes.length<24||sniffImageFormat(bytes)!=="png")return{ok:!1,message:"Not a valid PNG header."};if(bytes[12]!==73||bytes[13]!==72||bytes[14]!==68||bytes[15]!==82)return{ok:!1,message:"PNG IHDR chunk not found."};let width=readUInt32BE(bytes,16),height=readUInt32BE(bytes,20);return width<=0||height<=0?{ok:!1,message:"PNG reports non-positive dimensions."}:{ok:!0,width,height}}function readJpegDimensions(bytes){if(sniffImageFormat(bytes)!=="jpeg")return{ok:!1,message:"Not a valid JPEG header."};let offset=2,len=bytes.length;for(;offset+1<len;){if(bytes[offset]!==255){offset++;continue}let marker=bytes[offset+1];for(;marker===255&&offset+1<len;)offset++,marker=bytes[offset+1];if(offset+=2,marker>=208&&marker<=217||marker===1)continue;if(offset+1>=len)break;let segLen=readUInt16BE(bytes,offset);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204){if(offset+5>=len)break;let height=readUInt16BE(bytes,offset+3),width=readUInt16BE(bytes,offset+5);return width<=0||height<=0?{ok:!1,message:"JPEG SOF reports non-positive dimensions."}:{ok:!0,width,height}}offset+=segLen}return{ok:!1,message:"No supported JPEG SOF marker found."}}function readUInt32BE(b,o){return b[o]*16777216+(b[o+1]<<16)+(b[o+2]<<8)+b[o+3]}function readUInt16BE(b,o){return(b[o]<<8)+b[o+1]}function isPositiveInt(n){return Number.isInteger(n)&&n>0}function resolveViewport(input,compDimensions){let vp=input.viewport??compDimensions;return!isPositiveInt(vp.width)||!isPositiveInt(vp.height)?{ok:!1,error:"INVALID_VIEWPORT",status:400,message:`Viewport must be positive integers, got ${vp.width}x${vp.height}.`}:vp.width>MAX_VIEWPORT_DIMENSION||vp.height>MAX_VIEWPORT_DIMENSION||vp.width*vp.height>MAX_VIEWPORT_PIXELS?{ok:!1,error:"IMAGE_TOO_LARGE",status:413,message:`Requested render area ${vp.width}x${vp.height} exceeds the local pixel guard.`}:{ok:!0,viewport:{width:vp.width,height:vp.height}}}function toUint8(bytes){return bytes instanceof Uint8Array?bytes:Buffer.from(bytes)}async function resolveCompRef(compRef,deps){let candidates=[];if(path35.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path35.resolve(root,compRef));let cwdCandidate=path35.resolve(process.cwd(),compRef);candidates.includes(cwdCandidate)||candidates.push(cwdCandidate)}for(let candidate of candidates)try{let st=await deps.stat(candidate);if(st&&st.isFile())return{ok:!0,bytes:toUint8(await deps.readFile(candidate)),source:"local",sourcePath:candidate,warnings:[]}}catch{}let trimmed=compRef.trim(),lookup=/^\d+$/.test(trimmed)?{kind:"attachment_id",attachment_id:trimmed}:{kind:"filename",filename:compRef},fetched=await deps.fetchAttachmentBytes(lookup);if(!fetched.ok)return{ok:!1,error:fetched.error,status:fetched.status,message:fetched.message};let bytes=toUint8(fetched.bytes),warnings=[];try{let dir=await deps.getDocsPath("visual-diffs"),rawName=fetched.filename||(lookup.kind==="attachment_id"?`attachment-${lookup.attachment_id}`:lookup.filename),base=path35.basename(rawName),target=path35.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path35.resolve(dir)+path35.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(bytes))):warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.")}catch(err){warnings.push(`Attachment comp copy could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`)}return{ok:!0,bytes,source:"attachment",warnings}}async function loadPlaywright(){try{let mod=await import("playwright"),chromium=mod?.chromium??mod?.default?.chromium;return!chromium||typeof chromium.launch!="function"?{ok:!1}:{ok:!0,playwright:{chromium}}}catch{return{ok:!1}}}async function launchBrowser(playwright){try{return{ok:!0,browser:await playwright.chromium.launch({headless:!0})}}catch(err){return{ok:!1,error:"BROWSER_UNAVAILABLE",status:503,message:`Chromium could not be launched: ${err instanceof Error?err.message:"unknown error"}. Run "npx playwright install chromium".`}}}function normalizeMaskBoxes(raw,viewport){let boxes=[];for(let r of raw){let x0=Math.max(0,Math.floor(r.x)),y0=Math.max(0,Math.floor(r.y)),x1=Math.min(viewport.width,Math.ceil(r.x+r.width)),y1=Math.min(viewport.height,Math.ceil(r.y+r.height)),width=x1-x0,height=y1-y0;width>0&&height>0&&boxes.push({x:x0,y:y0,width,height})}return boxes.sort((a,b)=>a.y!==b.y?a.y-b.y:a.x!==b.x?a.x-b.x:a.width!==b.width?a.width-b.width:a.height-b.height),boxes}async function collectMaskBoxes(page,selectors,viewport){if(!selectors||selectors.length===0)return[];let raw=await page.evaluate(sels=>{let out=[];for(let sel of sels)document.querySelectorAll(sel).forEach(el=>{let rect=el.getBoundingClientRect();out.push({x:rect.x,y:rect.y,width:rect.width,height:rect.height})});return out},selectors);return normalizeMaskBoxes(Array.isArray(raw)?raw:[],viewport)}async function captureRenderPng(browser,targetUrl,viewport,maskSelectors){let context=await browser.newContext({viewport,deviceScaleFactor:1}),page;try{page=await context.newPage();try{await page.goto(targetUrl,{timeout:NAV_TIMEOUT_MS,waitUntil:"load"}),await page.waitForLoadState("networkidle",{timeout:NETWORK_IDLE_TIMEOUT_MS})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Failed to load ${targetUrl}: ${err instanceof Error?err.message:"unknown error"}`}}await page.addStyleTag({content:DETERMINISTIC_CSS}),await settleFonts(page);let maskBoxes=await collectMaskBoxes(page,maskSelectors,viewport),png;try{png=await page.screenshot({clip:{x:0,y:0,width:viewport.width,height:viewport.height},timeout:SCREENSHOT_TIMEOUT_MS,animations:"disabled"})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Screenshot capture failed: ${err instanceof Error?err.message:"unknown error"}`}}return{ok:!0,png:toUint8(png),maskBoxes,dimensions:viewport}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function settleFonts(page){try{await Promise.race([page.evaluate(()=>{let d=document;return d.fonts&&d.fonts.ready?d.fonts.ready.then(()=>!0):!0}),new Promise(resolve2=>setTimeout(resolve2,FONTS_READY_TIMEOUT_MS))])}catch{}}function isTimeoutError(err){let msg=err instanceof Error?err.message:String(err??"");return/timeout|timed out|TimeoutError/i.test(msg)}async function normalizeCompToPng(browser,compBytes,format,dims){if(format==="png")return{ok:!0,png:Buffer.from(compBytes)};let context=await browser.newContext({viewport:dims,deviceScaleFactor:1}),page;try{page=await context.newPage(),page.setContent&&await page.setContent("<!doctype html><html><body></body></html>");let dataUrl=`data:image/jpeg;base64,${Buffer.from(compBytes).toString("base64")}`,base64=(await page.evaluate(async arg=>{let img=new Image;await new Promise((resolve2,reject)=>{img.onload=()=>resolve2(),img.onerror=()=>reject(new Error("image load failed")),img.src=arg.url});let canvas=document.createElement("canvas");canvas.width=arg.w,canvas.height=arg.h;let ctx=canvas.getContext("2d");if(!ctx)throw new Error("no 2d context");return ctx.drawImage(img,0,0,arg.w,arg.h),canvas.toDataURL("image/png")},{url:dataUrl,w:dims.width,h:dims.height})).split(",")[1]??"";return base64?{ok:!0,png:Buffer.from(base64,"base64")}:{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:"Browser-side JPEG normalization produced no PNG data."}}catch(err){return{ok:!1,error:"UNSUPPORTED_COMP_IMAGE",status:400,message:`Failed to normalize JPEG comp to PNG: ${err instanceof Error?err.message:"unknown error"}`}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function computeVisualDiffInWorker(args,deps){try{return await runInWorker(args)}catch(err){deps?.logger?.(`visual_diff: worker unavailable, computing inline (${err instanceof Error?err.message:"unknown error"}).`);try{return computePngVisualDiff(args)}catch(inlineErr){return{ok:!1,error:"DIFF_FAILED",status:500,message:`Diff computation failed: ${inlineErr instanceof Error?inlineErr.message:"unknown error"}`}}}}function runInWorker(args){return new Promise((resolve2,reject)=>{let workerRelative="./visual-diff-worker.js",workerUrl=new URL(workerRelative,import.meta.url),settled=!1,worker;try{worker=new Worker(workerUrl,{workerData:args})}catch(err){reject(err);return}worker.once("message",msg=>{settled=!0,resolve2(msg),worker.terminate()}),worker.once("error",err=>{settled||reject(err)}),worker.once("exit",code=>{!settled&&code!==0&&reject(new Error(`diff worker exited with code ${code}`))})})}async function saveHeatmap(heatmapBase64,deps){try{let dir=await deps.getDocsPath("visual-diffs"),target=path35.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path35.resolve(dir)+path35.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(heatmapBase64,"base64")),{ok:!0,path:target}):{ok:!1,warning:"Heatmap not saved: resolved path escaped the visual-diffs directory."}}catch(err){return{ok:!1,warning:`Heatmap could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`}}}async function runVisualDiff(input,deps){try{let warnings=[],resolved=await resolveCompRef(input.comp_ref,deps);if(!resolved.ok)return errorContent(resolved.error,resolved.status,resolved.message);warnings.push(...resolved.warnings);let compBytes=resolved.bytes;if(compBytes.length>MAX_COMP_BYTES)return errorContent("IMAGE_TOO_LARGE",413,`Comp image is ${compBytes.length} bytes, exceeding the ${MAX_COMP_BYTES}-byte guard.`);let format=sniffImageFormat(compBytes);if(!format)return errorContent("UNSUPPORTED_COMP_IMAGE",400,"comp_ref resolved but is not a decodable PNG or JPEG image.");let dims=format==="png"?readPngDimensions(compBytes):readJpegDimensions(compBytes);if(!dims.ok)return errorContent("UNSUPPORTED_COMP_IMAGE",400,dims.message);let compDimensions={width:dims.width,height:dims.height},vp=resolveViewport(input,compDimensions);if(!vp.ok)return errorContent(vp.error,vp.status,vp.message);let loaded=await(deps.loadPlaywright??loadPlaywright)();if(!loaded.ok)return errorContent("BROWSER_UNAVAILABLE",503,'The optional Playwright browser runtime is not available. Install it with "npm i playwright && npx playwright install chromium".');let launch=await launchBrowser(loaded.playwright);if(!launch.ok)return errorContent(launch.error,launch.status,launch.message);let browser=launch.browser,capture,normalized;try{if(capture=await captureRenderPng(browser,input.target_url,vp.viewport,input.mask_selectors),!capture.ok)return errorContent(capture.error,capture.status,capture.message);if(normalized=await normalizeCompToPng(browser,compBytes,format,compDimensions),!normalized.ok)return errorContent(normalized.error,normalized.status,normalized.message)}finally{try{await browser.close()}catch{}}let passMismatchPct=typeof input.threshold=="number"?input.threshold:DEFAULT_PASS_MISMATCH_PCT,diff=await computeVisualDiffInWorker({compPngBuffer:normalized.png,renderPngBuffer:capture.png,maskBoxes:capture.maskBoxes,passMismatchPct,pixelmatchColorThreshold:PIXELMATCH_COLOR_THRESHOLD},deps);if(!diff.ok)return errorContent(diff.error,diff.status,diff.message);let saved=await saveHeatmap(diff.heatmap_base64,deps),heatmapPath=saved.ok?saved.path:null;saved.ok||warnings.push(saved.warning);let result={mismatch_pct:diff.mismatch_pct,dimension_match:diff.dimension_match,diff_regions:diff.diff_regions,heatmap_path:heatmapPath,comp_dimensions:diff.comp_dimensions,render_dimensions:diff.render_dimensions,threshold_used:passMismatchPct,passed:diff.passed};return diff.dimension_match||(result.message=`Render dimensions ${diff.render_dimensions.width}x${diff.render_dimensions.height} differ from comp dimensions ${diff.comp_dimensions.width}x${diff.comp_dimensions.height}. Images were NOT rescaled, so this result is automatically not passed; the diff covers the union region.`),warnings.length>0&&(result.warnings=warnings),{content:[textJson(result),{type:"image",data:diff.heatmap_base64,mimeType:"image/png"}]}}catch(err){return errorContent("VISUAL_DIFF_FAILED",500,`visual_diff failed: ${err instanceof Error?err.message:"unknown error"}`)}}function validateEstimateEpicInput(input){let hasEpic=typeof input.epic_key=="string"&&input.epic_key.trim().length>0,hasKeys=Array.isArray(input.ticket_keys);return hasEpic&&hasKeys?"epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.":!hasEpic&&!hasKeys?"Exactly one of epic_key or ticket_keys is required.":hasKeys&&input.ticket_keys.length===0?"ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.":null}function buildEstimateEpicErrorEnvelope(code,message,extras){return JSON.stringify({error:code,message,...extras??{}},null,2)}async function runEstimateEpic(input,deps){let validationError2=validateEstimateEpicInput(input);if(validationError2)return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("VALIDATION_ERROR",validationError2)}]};let payload={repo_name:deps.repoName};typeof input.epic_key=="string"&&(payload.epic_key=input.epic_key),Array.isArray(input.ticket_keys)&&(payload.ticket_keys=input.ticket_keys),typeof input.allow_partial=="boolean"&&(payload.allow_partial=input.allow_partial);let fetchImpl=deps.fetchImpl??fetch,resp;try{resp=await fetchImpl(deps.buildUrl("/estimate-epic"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(payload)})}catch{return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("NETWORK_ERROR","Failed to reach the Bridge API estimate-epic endpoint.")}]}}return{content:[{type:"text",text:await deps.handleResponse(resp)}]}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),stat:p=>stat10(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),stat:p=>stat10(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),mkdir:(p,options)=>mkdir12(p,options),writeFile:(p,data,options)=>writeFile12(p,data,options),rename:(oldPath,newPath)=>rename3(oldPath,newPath),chmod:(p,mode)=>chmod3(p,mode),unlink:p=>unlink3(p),open:async(p,flags,mode)=>{let handle=await open2(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}async function getGetHeaders(){return{"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION}}async function getPostHeaders(){return{"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION}}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath3(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path36.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path36.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path36.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path36.join(path36.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return JSON.stringify(body,null,2)}return await resp.text()}let rawText=await resp.text(),errorCode=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),JSON.stringify({...detail,error:errorCode,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}async function saveLocally(dir,filename,content){let filePath=path36.join(dir,filename);try{return await mkdir12(dir,{recursive:!0}),await writeFile12(filePath,content,"utf-8"),`
|
|
4859
|
+
When the worktrees have been spawned, call \`resume_full_automation\` with \`chain_run_id\` "${row.chain_run_id}" and \`agent_result\` set to a short summary of what start-tickets reported.`;return buildNeedsAgentTaskEnvelope({chainRunId:updated.chain_run_id,chainStage:START_TICKETS_PIPELINE,chainStep:idx+1,chainTotal:total,preamble:buildPreamble(recipe,idx,updated.stages),instruction})}function numericArg(value){if(typeof value=="number"&&Number.isFinite(value))return value}async function continueChainExecution(deps,persistence,recipe,row,autoApprove){let guard=0,guardMax=1e4;for(;guard++<guardMax;){let idx=row.current_stage_index,total=recipe.stages.length;if(idx>=total){try{row=await persistence.patchRun(row.chain_run_id,{status:"completed"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,row)}let stageRecipe=recipe.stages[idx],outcome2=null;if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE)return startStartTicketsStage(persistence,recipe,row);if(stageRecipe.fan_out_input?outcome2=await startOrContinueReviewTicketStage(deps,persistence,recipe,row,autoApprove):outcome2=await startOrContinueIdeaToTicketStage(deps,persistence,recipe,row,autoApprove),outcome2.kind==="pause"||outcome2.kind==="fail")return outcome2.envelope;row=outcome2.row}return failedEnvelope2("TOOL_ERROR","Chain execution exceeded its step guard.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length})}async function runFullAutomation(deps,input){try{if(typeof input.idea!="string"||input.idea.trim()==="")return failedEnvelope2("VALIDATION","idea must be a non-empty string.");let agent=input.agent??"claude";if(agent!=="claude")return failedEnvelope2("VALIDATION",`Unsupported agent "${String(input.agent)}". Only "claude" is supported.`);let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`);let autoApprove=input.auto_approve===void 0?!0:normalizeAutoApprove2(input.auto_approve),args={idea:input.idea,auto_approve:autoApprove,scheduled_at:input.scheduled_at??"",max_children:input.max_children,allow_duplicate:input.allow_duplicate,agent,ttl_seconds:input.ttl_seconds},initialStages=recipe.stages.map(stage=>({pipeline_name:stage.pipeline_name,status:"pending"})),persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.createRun({chain_name:CHAIN_NAME,args,current_stage_index:0,stages:initialStages,status:"running",ttl_seconds:input.ttl_seconds})}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while creating the chain run.")}return continueChainExecution(deps,persistence,recipe,row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in runFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while executing the full-automation chain.")}}async function resumeFullAutomation(deps,input){try{let recipe=deps.chainRecipes[CHAIN_NAME];if(!recipe)return failedEnvelope2("VALIDATION",`Chain recipe "${CHAIN_NAME}" not found.`,{chain_run_id:input.chain_run_id});let persistence=createChainPersistenceClient({baseUrl:deps.baseUrl,apiKey:deps.apiKey,repoName:deps.repoName}),row;try{row=await persistence.getRun(input.chain_run_id)}catch(err){return err instanceof ChainPersistenceError?failedEnvelope2(err.code,err.message,{chain_run_id:input.chain_run_id}):failedEnvelope2("TOOL_ERROR","An unexpected error occurred while fetching the chain run.",{chain_run_id:input.chain_run_id})}if(row.status==="expired")return failedEnvelope2("EXPIRED","Chain run has expired.",{chain_run_id:row.chain_run_id,chain_total:recipe.stages.length});let autoApprove=normalizeAutoApprove2(row.args.auto_approve),idx=row.current_stage_index,stageRecipe=recipe.stages[idx],total=recipe.stages.length;if(!stageRecipe)return failedEnvelope2("VALIDATION",`Chain run has no active stage at index ${idx}.`,{chain_run_id:row.chain_run_id,chain_total:total});if(stageRecipe.pipeline_name===START_TICKETS_PIPELINE){if(typeof input.agent_result!="string"||input.agent_result.trim()==="")return failedEnvelope2("VALIDATION","agent_result must be a non-empty string to complete the start-tickets stage.",{chain_run_id:row.chain_run_id,chain_stage:START_TICKETS_PIPELINE,chain_step:idx+1,chain_total:total});let startResolution=resolveStartTicketKeys(row,idx,stageRecipe.fan_out_input??"reviewed_ticket_keys"),startedKeys=startResolution.ok?startResolution.keys:[],stages=cloneStages(row.stages);stages[idx].status="completed",stages[idx].pipeline_run_id=null,stages[idx].outputs={started_ticket_keys:startedKeys},stages[idx].summary=summarizeStageCompletion(START_TICKETS_PIPELINE,startedKeys);let updated;try{updated=await persistence.patchRun(row.chain_run_id,{stages,current_stage_index:idx+1,status:"completed",expected_status:"paused",expected_current_stage_index:idx})}catch(err){return persistenceFailEnvelope(err,row,recipe)}return buildCompletedEnvelope(recipe,updated)}let activePipelineRunId=row.stages[idx]?.pipeline_run_id;if(!activePipelineRunId)return failedEnvelope2("VALIDATION",`No active child pipeline to resume for stage ${idx+1}.`,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});let peek=await peekPipelineRun(deps,activePipelineRunId);if("error_code"in peek)return failedEnvelope2(peek.error_code,peek.error,{chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total});if(peek.status!=="paused"&&peek.status!=="completed"&&peek.status!=="failed")return{status:"failed",error_code:"VALIDATION",error:`Inner pipeline run is in status "${peek.status}" and cannot be safely resumed or recovered. Inspect pipeline_run_id ${activePipelineRunId}.`,chain_run_id:row.chain_run_id,chain_stage:stageRecipe.pipeline_name,chain_step:idx+1,chain_total:total,pipeline_run_id:activePipelineRunId,resumable:!1};try{row=await persistence.patchRun(row.chain_run_id,{status:"running"})}catch(err){return persistenceFailEnvelope(err,row,recipe)}let childEnv;if(peek.status==="paused")childEnv=await resumePipeline(deps,{pipeline_run_id:activePipelineRunId,agent_result:input.agent_result});else if(peek.status==="completed")childEnv={status:"completed",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,total_steps:peek.total_steps,results:peek.results};else{let failedStepError=peek.results.find(r=>!r.ok&&typeof r.error=="string")?.error;childEnv={status:"failed",error_code:"TOOL_ERROR",error:failedStepError?`Inner pipeline run failed before the chain could advance: ${failedStepError}`:"Inner pipeline run failed before the chain could advance.",pipeline_run_id:peek.pipeline_run_id,pipeline:peek.pipeline,results:peek.results}}let fanOut=!!stageRecipe.fan_out_input,childIndex=row.stages[idx]?.current_child_index??0,ticketKey=fanOut?(resolveCrossStageList(row,idx,stageRecipe.fan_out_input)??[])[childIndex]:void 0,outcome2=await handleChildPipelineEnvelope(persistence,recipe,row,childEnv,{fanOut,ticketKey,childIndex});return outcome2.kind==="pause"||outcome2.kind==="fail"?outcome2.envelope:continueChainExecution(deps,persistence,recipe,outcome2.row,autoApprove)}catch(err){return console.error("[chain-orchestrator] unexpected error in resumeFullAutomation:",err),failedEnvelope2("TOOL_ERROR","An unexpected error occurred while resuming the full-automation chain.",{chain_run_id:input.chain_run_id})}}import path35 from"path";import{Worker}from"worker_threads";import{PNG}from"pngjs";import pixelmatch from"pixelmatch";import{isMainThread,parentPort,workerData}from"worker_threads";var PIXELMATCH_COLOR_THRESHOLD=.1,DEFAULT_PASS_MISMATCH_PCT=2,MAX_DIFF_REGIONS=10;function decodePng(buffer,label){try{let png=PNG.sync.read(Buffer.from(buffer));return!Number.isInteger(png.width)||!Number.isInteger(png.height)||png.width<=0||png.height<=0?{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Decoded ${label} PNG has invalid dimensions.`}:{width:png.width,height:png.height,data:png.data}}catch{return{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Failed to decode ${label} image as PNG.`}}}function extractOverlap(src,srcW,overlapW,overlapH){let out=new Uint8Array(overlapW*overlapH*4);for(let y=0;y<overlapH;y++){let srcRow=y*srcW*4,dstRow=y*overlapW*4;out.set(src.subarray(srcRow,srcRow+overlapW*4),dstRow)}return out}function buildMaskGrid(boxes,unionW,unionH){let grid=new Uint8Array(unionW*unionH);for(let box of boxes){let x0=Math.max(0,Math.floor(box.x)),y0=Math.max(0,Math.floor(box.y)),x1=Math.min(unionW,Math.floor(box.x+box.width)),y1=Math.min(unionH,Math.floor(box.y+box.height));for(let y=y0;y<y1;y++)for(let x=x0;x<x1;x++)grid[y*unionW+x]=1}return grid}function applyMaskToOverlap(buf,overlapW,overlapH,maskGrid,unionW){for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++)if(maskGrid[y*unionW+x]===1){let off=(y*overlapW+x)*4;buf[off]=0,buf[off+1]=0,buf[off+2]=0,buf[off+3]=255}}function extractDiffRegions(mask,width,height,maxRegions){let visited=new Uint8Array(width*height),regions=[],stack=[];for(let start=0;start<mask.length;start++){if(mask[start]===0||visited[start]===1)continue;let minX=width,minY=height,maxX=-1,maxY=-1,pixels=0;for(stack.length=0,stack.push(start),visited[start]=1;stack.length>0;){let idx=stack.pop(),x=idx%width,y=(idx-x)/width;if(pixels++,x<minX&&(minX=x),y<minY&&(minY=y),x>maxX&&(maxX=x),y>maxY&&(maxY=y),x>0){let n=idx-1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(x<width-1){let n=idx+1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y>0){let n=idx-width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y<height-1){let n=idx+width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}}regions.push({x:minX,y:minY,width:maxX-minX+1,height:maxY-minY+1,pixels})}return regions.sort((a,b)=>b.pixels!==a.pixels?b.pixels-a.pixels:a.y!==b.y?a.y-b.y:a.x-b.x),regions.slice(0,Math.max(0,maxRegions))}function computePngVisualDiff(input){let comp=decodePng(input.compPngBuffer,"comp");if("ok"in comp&&comp.ok===!1)return comp;let render=decodePng(input.renderPngBuffer,"render");if("ok"in render&&render.ok===!1)return render;let compImg=comp,renderImg=render,dimensionMatch=compImg.width===renderImg.width&&compImg.height===renderImg.height,unionW=Math.max(compImg.width,renderImg.width),unionH=Math.max(compImg.height,renderImg.height),overlapW=Math.min(compImg.width,renderImg.width),overlapH=Math.min(compImg.height,renderImg.height),maskGrid=buildMaskGrid(input.maskBoxes??[],unionW,unionH),output=new Uint8Array(unionW*unionH*4),diffMask=new Uint8Array(unionW*unionH),differingPixels=0;if(overlapW>0&&overlapH>0){let compOverlap=extractOverlap(compImg.data,compImg.width,overlapW,overlapH),renderOverlap=extractOverlap(renderImg.data,renderImg.width,overlapW,overlapH);applyMaskToOverlap(compOverlap,overlapW,overlapH,maskGrid,unionW),applyMaskToOverlap(renderOverlap,overlapW,overlapH,maskGrid,unionW);let overlapOut=new Uint8Array(overlapW*overlapH*4);try{differingPixels=pixelmatch(compOverlap,renderOverlap,overlapOut,overlapW,overlapH,{threshold:input.pixelmatchColorThreshold,includeAA:!1,diffColor:[255,0,0],diffColorAlt:[255,0,0],aaColor:[255,255,0]})}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Pixel comparison failed."}}for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++){let so=(y*overlapW+x)*4,uo=(y*unionW+x)*4;output[uo]=overlapOut[so],output[uo+1]=overlapOut[so+1],output[uo+2]=overlapOut[so+2],output[uo+3]=255,overlapOut[so]===255&&overlapOut[so+1]===0&&overlapOut[so+2]===0&&(diffMask[y*unionW+x]=1)}}for(let y=0;y<unionH;y++)for(let x=0;x<unionW;x++){let inComp=x<compImg.width&&y<compImg.height,inRender=x<renderImg.width&&y<renderImg.height;if(inComp===inRender||maskGrid[y*unionW+x]===1)continue;let off=(y*unionW+x)*4;output[off]=255,output[off+1]=0,output[off+2]=0,output[off+3]=255,diffMask[y*unionW+x]=1,differingPixels++}let diffRegions=extractDiffRegions(diffMask,unionW,unionH,input.maxRegions??MAX_DIFF_REGIONS),totalPixels=unionW*unionH,mismatchPct=totalPixels>0?differingPixels/totalPixels*100:0,passed=dimensionMatch&&mismatchPct<=input.passMismatchPct,heatmapBase64;try{let png=new PNG({width:unionW,height:unionH});png.data=Buffer.from(output),heatmapBase64=PNG.sync.write(png).toString("base64")}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Failed to encode diff heatmap."}}return{ok:!0,mismatch_pct:mismatchPct,dimension_match:dimensionMatch,passed,diff_regions:diffRegions,comp_dimensions:{width:compImg.width,height:compImg.height},render_dimensions:{width:renderImg.width,height:renderImg.height},differing_pixels:differingPixels,total_pixels:totalPixels,heatmap_base64:heatmapBase64}}if(!isMainThread&&parentPort)try{let result=computePngVisualDiff(workerData);parentPort.postMessage(result)}catch(err){parentPort.postMessage({ok:!1,error:"DIFF_FAILED",status:500,message:`Diff worker failed: ${err instanceof Error?err.message:"unknown error"}`})}var NAV_TIMEOUT_MS=3e4,NETWORK_IDLE_TIMEOUT_MS=15e3,FONTS_READY_TIMEOUT_MS=5e3,SCREENSHOT_TIMEOUT_MS=2e4,MAX_VIEWPORT_DIMENSION=16384,MAX_VIEWPORT_PIXELS=32e6,MAX_COMP_BYTES=25*1024*1024,DETERMINISTIC_CSS="* { animation: none !important; transition: none !important; caret-color: transparent !important; }";function textJson(value){return{type:"text",text:JSON.stringify(value,null,2)}}function errorContent(error,status,message,extra){return{content:[{type:"text",text:JSON.stringify({error,status,message,...extra??{}})}]}}var PNG_MAGIC=[137,80,78,71,13,10,26,10];function sniffImageFormat(bytes){return bytes.length>=8&&PNG_MAGIC.every((b,i)=>bytes[i]===b)?"png":bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255?"jpeg":null}function readPngDimensions(bytes){if(bytes.length<24||sniffImageFormat(bytes)!=="png")return{ok:!1,message:"Not a valid PNG header."};if(bytes[12]!==73||bytes[13]!==72||bytes[14]!==68||bytes[15]!==82)return{ok:!1,message:"PNG IHDR chunk not found."};let width=readUInt32BE(bytes,16),height=readUInt32BE(bytes,20);return width<=0||height<=0?{ok:!1,message:"PNG reports non-positive dimensions."}:{ok:!0,width,height}}function readJpegDimensions(bytes){if(sniffImageFormat(bytes)!=="jpeg")return{ok:!1,message:"Not a valid JPEG header."};let offset=2,len=bytes.length;for(;offset+1<len;){if(bytes[offset]!==255){offset++;continue}let marker=bytes[offset+1];for(;marker===255&&offset+1<len;)offset++,marker=bytes[offset+1];if(offset+=2,marker>=208&&marker<=217||marker===1)continue;if(offset+1>=len)break;let segLen=readUInt16BE(bytes,offset);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204){if(offset+5>=len)break;let height=readUInt16BE(bytes,offset+3),width=readUInt16BE(bytes,offset+5);return width<=0||height<=0?{ok:!1,message:"JPEG SOF reports non-positive dimensions."}:{ok:!0,width,height}}offset+=segLen}return{ok:!1,message:"No supported JPEG SOF marker found."}}function readUInt32BE(b,o){return b[o]*16777216+(b[o+1]<<16)+(b[o+2]<<8)+b[o+3]}function readUInt16BE(b,o){return(b[o]<<8)+b[o+1]}function isPositiveInt(n){return Number.isInteger(n)&&n>0}function resolveViewport(input,compDimensions){let vp=input.viewport??compDimensions;return!isPositiveInt(vp.width)||!isPositiveInt(vp.height)?{ok:!1,error:"INVALID_VIEWPORT",status:400,message:`Viewport must be positive integers, got ${vp.width}x${vp.height}.`}:vp.width>MAX_VIEWPORT_DIMENSION||vp.height>MAX_VIEWPORT_DIMENSION||vp.width*vp.height>MAX_VIEWPORT_PIXELS?{ok:!1,error:"IMAGE_TOO_LARGE",status:413,message:`Requested render area ${vp.width}x${vp.height} exceeds the local pixel guard.`}:{ok:!0,viewport:{width:vp.width,height:vp.height}}}function toUint8(bytes){return bytes instanceof Uint8Array?bytes:Buffer.from(bytes)}async function resolveCompRef(compRef,deps){let candidates=[];if(path35.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path35.resolve(root,compRef));let cwdCandidate=path35.resolve(process.cwd(),compRef);candidates.includes(cwdCandidate)||candidates.push(cwdCandidate)}for(let candidate of candidates)try{let st=await deps.stat(candidate);if(st&&st.isFile())return{ok:!0,bytes:toUint8(await deps.readFile(candidate)),source:"local",sourcePath:candidate,warnings:[]}}catch{}let trimmed=compRef.trim(),lookup=/^\d+$/.test(trimmed)?{kind:"attachment_id",attachment_id:trimmed}:{kind:"filename",filename:compRef},fetched=await deps.fetchAttachmentBytes(lookup);if(!fetched.ok)return{ok:!1,error:fetched.error,status:fetched.status,message:fetched.message};let bytes=toUint8(fetched.bytes),warnings=[];try{let dir=await deps.getDocsPath("visual-diffs"),rawName=fetched.filename||(lookup.kind==="attachment_id"?`attachment-${lookup.attachment_id}`:lookup.filename),base=path35.basename(rawName),target=path35.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path35.resolve(dir)+path35.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(bytes))):warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.")}catch(err){warnings.push(`Attachment comp copy could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`)}return{ok:!0,bytes,source:"attachment",warnings}}async function loadPlaywright(){try{let mod=await import("playwright"),chromium=mod?.chromium??mod?.default?.chromium;return!chromium||typeof chromium.launch!="function"?{ok:!1}:{ok:!0,playwright:{chromium}}}catch{return{ok:!1}}}async function launchBrowser(playwright){try{return{ok:!0,browser:await playwright.chromium.launch({headless:!0})}}catch(err){return{ok:!1,error:"BROWSER_UNAVAILABLE",status:503,message:`Chromium could not be launched: ${err instanceof Error?err.message:"unknown error"}. Run "npx playwright install chromium".`}}}function normalizeMaskBoxes(raw,viewport){let boxes=[];for(let r of raw){let x0=Math.max(0,Math.floor(r.x)),y0=Math.max(0,Math.floor(r.y)),x1=Math.min(viewport.width,Math.ceil(r.x+r.width)),y1=Math.min(viewport.height,Math.ceil(r.y+r.height)),width=x1-x0,height=y1-y0;width>0&&height>0&&boxes.push({x:x0,y:y0,width,height})}return boxes.sort((a,b)=>a.y!==b.y?a.y-b.y:a.x!==b.x?a.x-b.x:a.width!==b.width?a.width-b.width:a.height-b.height),boxes}async function collectMaskBoxes(page,selectors,viewport){if(!selectors||selectors.length===0)return[];let raw=await page.evaluate(sels=>{let out=[];for(let sel of sels)document.querySelectorAll(sel).forEach(el=>{let rect=el.getBoundingClientRect();out.push({x:rect.x,y:rect.y,width:rect.width,height:rect.height})});return out},selectors);return normalizeMaskBoxes(Array.isArray(raw)?raw:[],viewport)}async function captureRenderPng(browser,targetUrl,viewport,maskSelectors){let context=await browser.newContext({viewport,deviceScaleFactor:1}),page;try{page=await context.newPage();try{await page.goto(targetUrl,{timeout:NAV_TIMEOUT_MS,waitUntil:"load"}),await page.waitForLoadState("networkidle",{timeout:NETWORK_IDLE_TIMEOUT_MS})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Failed to load ${targetUrl}: ${err instanceof Error?err.message:"unknown error"}`}}await page.addStyleTag({content:DETERMINISTIC_CSS}),await settleFonts(page);let maskBoxes=await collectMaskBoxes(page,maskSelectors,viewport),png;try{png=await page.screenshot({clip:{x:0,y:0,width:viewport.width,height:viewport.height},timeout:SCREENSHOT_TIMEOUT_MS,animations:"disabled"})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Screenshot capture failed: ${err instanceof Error?err.message:"unknown error"}`}}return{ok:!0,png:toUint8(png),maskBoxes,dimensions:viewport}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function settleFonts(page){try{await Promise.race([page.evaluate(()=>{let d=document;return d.fonts&&d.fonts.ready?d.fonts.ready.then(()=>!0):!0}),new Promise(resolve2=>setTimeout(resolve2,FONTS_READY_TIMEOUT_MS))])}catch{}}function isTimeoutError(err){let msg=err instanceof Error?err.message:String(err??"");return/timeout|timed out|TimeoutError/i.test(msg)}async function normalizeCompToPng(browser,compBytes,format,dims){if(format==="png")return{ok:!0,png:Buffer.from(compBytes)};let context=await browser.newContext({viewport:dims,deviceScaleFactor:1}),page;try{page=await context.newPage(),page.setContent&&await page.setContent("<!doctype html><html><body></body></html>");let dataUrl=`data:image/jpeg;base64,${Buffer.from(compBytes).toString("base64")}`,base64=(await page.evaluate(async arg=>{let img=new Image;await new Promise((resolve2,reject)=>{img.onload=()=>resolve2(),img.onerror=()=>reject(new Error("image load failed")),img.src=arg.url});let canvas=document.createElement("canvas");canvas.width=arg.w,canvas.height=arg.h;let ctx=canvas.getContext("2d");if(!ctx)throw new Error("no 2d context");return ctx.drawImage(img,0,0,arg.w,arg.h),canvas.toDataURL("image/png")},{url:dataUrl,w:dims.width,h:dims.height})).split(",")[1]??"";return base64?{ok:!0,png:Buffer.from(base64,"base64")}:{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:"Browser-side JPEG normalization produced no PNG data."}}catch(err){return{ok:!1,error:"UNSUPPORTED_COMP_IMAGE",status:400,message:`Failed to normalize JPEG comp to PNG: ${err instanceof Error?err.message:"unknown error"}`}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function computeVisualDiffInWorker(args,deps){try{return await runInWorker(args)}catch(err){deps?.logger?.(`visual_diff: worker unavailable, computing inline (${err instanceof Error?err.message:"unknown error"}).`);try{return computePngVisualDiff(args)}catch(inlineErr){return{ok:!1,error:"DIFF_FAILED",status:500,message:`Diff computation failed: ${inlineErr instanceof Error?inlineErr.message:"unknown error"}`}}}}function runInWorker(args){return new Promise((resolve2,reject)=>{let workerRelative="./visual-diff-worker.js",workerUrl=new URL(workerRelative,import.meta.url),settled=!1,worker;try{worker=new Worker(workerUrl,{workerData:args})}catch(err){reject(err);return}worker.once("message",msg=>{settled=!0,resolve2(msg),worker.terminate()}),worker.once("error",err=>{settled||reject(err)}),worker.once("exit",code=>{!settled&&code!==0&&reject(new Error(`diff worker exited with code ${code}`))})})}async function saveHeatmap(heatmapBase64,deps){try{let dir=await deps.getDocsPath("visual-diffs"),target=path35.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path35.resolve(dir)+path35.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(heatmapBase64,"base64")),{ok:!0,path:target}):{ok:!1,warning:"Heatmap not saved: resolved path escaped the visual-diffs directory."}}catch(err){return{ok:!1,warning:`Heatmap could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`}}}async function runVisualDiff(input,deps){try{let warnings=[],resolved=await resolveCompRef(input.comp_ref,deps);if(!resolved.ok)return errorContent(resolved.error,resolved.status,resolved.message);warnings.push(...resolved.warnings);let compBytes=resolved.bytes;if(compBytes.length>MAX_COMP_BYTES)return errorContent("IMAGE_TOO_LARGE",413,`Comp image is ${compBytes.length} bytes, exceeding the ${MAX_COMP_BYTES}-byte guard.`);let format=sniffImageFormat(compBytes);if(!format)return errorContent("UNSUPPORTED_COMP_IMAGE",400,"comp_ref resolved but is not a decodable PNG or JPEG image.");let dims=format==="png"?readPngDimensions(compBytes):readJpegDimensions(compBytes);if(!dims.ok)return errorContent("UNSUPPORTED_COMP_IMAGE",400,dims.message);let compDimensions={width:dims.width,height:dims.height},vp=resolveViewport(input,compDimensions);if(!vp.ok)return errorContent(vp.error,vp.status,vp.message);let loaded=await(deps.loadPlaywright??loadPlaywright)();if(!loaded.ok)return errorContent("BROWSER_UNAVAILABLE",503,'The optional Playwright browser runtime is not available. Install it with "npm i playwright && npx playwright install chromium".');let launch=await launchBrowser(loaded.playwright);if(!launch.ok)return errorContent(launch.error,launch.status,launch.message);let browser=launch.browser,capture,normalized;try{if(capture=await captureRenderPng(browser,input.target_url,vp.viewport,input.mask_selectors),!capture.ok)return errorContent(capture.error,capture.status,capture.message);if(normalized=await normalizeCompToPng(browser,compBytes,format,compDimensions),!normalized.ok)return errorContent(normalized.error,normalized.status,normalized.message)}finally{try{await browser.close()}catch{}}let passMismatchPct=typeof input.threshold=="number"?input.threshold:DEFAULT_PASS_MISMATCH_PCT,diff=await computeVisualDiffInWorker({compPngBuffer:normalized.png,renderPngBuffer:capture.png,maskBoxes:capture.maskBoxes,passMismatchPct,pixelmatchColorThreshold:PIXELMATCH_COLOR_THRESHOLD},deps);if(!diff.ok)return errorContent(diff.error,diff.status,diff.message);let saved=await saveHeatmap(diff.heatmap_base64,deps),heatmapPath=saved.ok?saved.path:null;saved.ok||warnings.push(saved.warning);let result={mismatch_pct:diff.mismatch_pct,dimension_match:diff.dimension_match,diff_regions:diff.diff_regions,heatmap_path:heatmapPath,comp_dimensions:diff.comp_dimensions,render_dimensions:diff.render_dimensions,threshold_used:passMismatchPct,passed:diff.passed};return diff.dimension_match||(result.message=`Render dimensions ${diff.render_dimensions.width}x${diff.render_dimensions.height} differ from comp dimensions ${diff.comp_dimensions.width}x${diff.comp_dimensions.height}. Images were NOT rescaled, so this result is automatically not passed; the diff covers the union region.`),warnings.length>0&&(result.warnings=warnings),{content:[textJson(result),{type:"image",data:diff.heatmap_base64,mimeType:"image/png"}]}}catch(err){return errorContent("VISUAL_DIFF_FAILED",500,`visual_diff failed: ${err instanceof Error?err.message:"unknown error"}`)}}function validateEstimateEpicInput(input){let hasEpic=typeof input.epic_key=="string"&&input.epic_key.trim().length>0,hasKeys=Array.isArray(input.ticket_keys);return hasEpic&&hasKeys?"epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.":!hasEpic&&!hasKeys?"Exactly one of epic_key or ticket_keys is required.":hasKeys&&input.ticket_keys.length===0?"ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.":null}function buildEstimateEpicErrorEnvelope(code,message,extras){return JSON.stringify({error:code,message,...extras??{}},null,2)}async function runEstimateEpic(input,deps){let validationError2=validateEstimateEpicInput(input);if(validationError2)return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("VALIDATION_ERROR",validationError2)}]};let payload={repo_name:deps.repoName};typeof input.epic_key=="string"&&(payload.epic_key=input.epic_key),Array.isArray(input.ticket_keys)&&(payload.ticket_keys=input.ticket_keys),typeof input.allow_partial=="boolean"&&(payload.allow_partial=input.allow_partial);let fetchImpl=deps.fetchImpl??fetch,resp;try{resp=await fetchImpl(deps.buildUrl("/estimate-epic"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(payload)})}catch{return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("NETWORK_ERROR","Failed to reach the Bridge API estimate-epic endpoint.")}]}}return{content:[{type:"text",text:await deps.handleResponse(resp)}]}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),TOOL_SURFACE_POLL_ENABLED=parseDefaultOffEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_POLL_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),stat:p=>stat10(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),stat:p=>stat10(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os16.homedir,platform:process.platform,readFile:p=>readFile14(p,"utf-8"),mkdir:(p,options)=>mkdir12(p,options),writeFile:(p,data,options)=>writeFile12(p,data,options),rename:(oldPath,newPath)=>rename3(oldPath,newPath),chmod:(p,mode)=>chmod3(p,mode),unlink:p=>unlink3(p),open:async(p,flags,mode)=>{let handle=await open2(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}async function getGetHeaders(){return{"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION}}async function getPostHeaders(){return{"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION}}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath3(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path36.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path36.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path36.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path36.join(path36.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return JSON.stringify(body,null,2)}return await resp.text()}let rawText=await resp.text(),errorCode=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),JSON.stringify({...detail,error:errorCode,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}async function saveLocally(dir,filename,content){let filePath=path36.join(dir,filename);try{return await mkdir12(dir,{recursive:!0}),await writeFile12(filePath,content,"utf-8"),`
|
|
4816
4860
|
|
|
4817
4861
|
---
|
|
4818
4862
|
Saved to ${filePath}`}catch(writeErr){return`
|
|
@@ -4827,9 +4871,11 @@ Warning: response was NOT truncated because the full payload could not be saved.
|
|
|
4827
4871
|
|
|
4828
4872
|
Note: Both file_path and ${textLabel} were provided. file_path content was used.`)}catch(err){return{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Error reading file at ${filePath}: ${err instanceof Error?err.message:String(err)}`})}]}}}return{ok:!0,text:resolvedText,note}}var BINARY_EXTENSIONS=new Set([".png",".jpg",".jpeg",".gif",".webp",".heic",".heif",".bmp",".tiff",".pdf",".zip",".docx",".xlsx",".pptx"]),ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif"},ALLOWED_BINARY_UPLOAD_MIME_TYPES=Array.from(new Set(Object.values(ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION))).sort();function deriveAllowedBinaryUploadMimeType(effectiveFileName){let ext=path36.extname(effectiveFileName).toLowerCase();return ALLOWED_BINARY_UPLOAD_MIME_BY_EXTENSION[ext]}async function resolveUploadAttachment(textValue,filePath,textLabel,effectiveFileName){if(!filePath&&!textValue)return{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Either ${textLabel} or file_path must be provided.`})}]}};if(!filePath)return{ok:!0,text:textValue,encoding:void 0,note:""};try{if((await stat10(filePath)).size>10*1024*1024)return{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`File at ${filePath} exceeds 10MB size limit.`})}]}};let ext=path36.extname(filePath).toLowerCase(),buf=await readFile14(filePath),isBinary=BINARY_EXTENSIONS.has(ext);if(!isBinary)try{new TextDecoder("utf-8",{fatal:!0}).decode(buf)}catch{isBinary=!0}let note=textValue?`
|
|
4829
4873
|
|
|
4830
|
-
Note: Both file_path and ${textLabel} were provided. file_path content was used.`:"";if(isBinary){let contentType=deriveAllowedBinaryUploadMimeType(effectiveFileName);return contentType?{ok:!0,text:buf.toString("base64"),encoding:"base64",note,contentType}:{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Unsupported attachment type for binary upload: ${effectiveFileName}. Allowed types: ${ALLOWED_BINARY_UPLOAD_MIME_TYPES.join(", ")}.`})}]}}}return buf.length>1048576?{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Text file at ${filePath} exceeds 1MB size limit.`})}]}}:{ok:!0,text:buf.toString("utf8"),encoding:void 0,note}}catch(err){return{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Error reading file at ${filePath}: ${err instanceof Error?err.message:String(err)}`})}]}}}}function
|
|
4874
|
+
Note: Both file_path and ${textLabel} were provided. file_path content was used.`:"";if(isBinary){let contentType=deriveAllowedBinaryUploadMimeType(effectiveFileName);return contentType?{ok:!0,text:buf.toString("base64"),encoding:"base64",note,contentType}:{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Unsupported attachment type for binary upload: ${effectiveFileName}. Allowed types: ${ALLOWED_BINARY_UPLOAD_MIME_TYPES.join(", ")}.`})}]}}}return buf.length>1048576?{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Text file at ${filePath} exceeds 1MB size limit.`})}]}}:{ok:!0,text:buf.toString("utf8"),encoding:void 0,note}}catch(err){return{ok:!1,errorResponse:{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:`Error reading file at ${filePath}: ${err instanceof Error?err.message:String(err)}`})}]}}}}var MAX_CONSECUTIVE_POLL_FAILURES=3;function formatRecoverablePollGiveUp(situation,handle){let recovery=`Server-side processing may still be continuing. Once it completes, retrieve the result with a GET to ${handle.recoveryGetUrl}`+(handle.retrievalToolName?` (or the ${handle.retrievalToolName} MCP tool).`:"."),envelope={error:"GATEWAY_TIMEOUT",status:504,[handle.handleName]:handle.handleValue,recovery_get:handle.recoveryGetUrl,message:`${situation}
|
|
4875
|
+
|
|
4876
|
+
${recovery}`};return JSON.stringify(envelope)}function formatTriggerConnectionFailure(retryHint,recovery){let parts=["The connection to Bridge API dropped before the submission outcome could be confirmed, so it is unknown whether the server accepted the request."];recovery&&parts.push(`Check whether it was accepted with a GET to ${recovery.recoveryGetUrl} before retrying.`),parts.push(`If it was not accepted, retry ${retryHint}.`);let envelope={error:"SERVICE_UNAVAILABLE",status:503,message:parts.join(" ")};return recovery&&(envelope[recovery.handleName]=recovery.handleValue,envelope.recovery_get=recovery.recoveryGetUrl),JSON.stringify(envelope)}function parseAutomationProgress(text){try{let obj=JSON.parse(text);if(obj&&typeof obj=="object"&&Array.isArray(obj.stages_in_flight)&&typeof obj.completed_stage_count=="number")return obj}catch{}return null}function emitAutomationProgressNotice(label,elapsedSeconds,progress){try{server.server.sendLoggingMessage({level:"notice",logger:"bridge-api.progress",data:{label,elapsed_seconds:elapsedSeconds,status:progress.status??"in_progress",last_progress_at:progress.last_progress_at??null,stages_in_flight:progress.stages_in_flight??[],completed_stage_count:progress.completed_stage_count??0,failed_stages:progress.failed_stages??[]}})}catch{}}async function pollForResult(getUrl,timeoutMs,label,recovery,schedule){let startTime=Date.now(),waitMs=schedule?schedule.initialDelayMs:15e3,latestProgress=null,consecutiveFetchFailures=0,progressSuffix=()=>latestProgress?`
|
|
4831
4877
|
|
|
4832
|
-
Last observed progress: ${JSON.stringify({status:latestProgress.status??"in_progress",last_progress_at:latestProgress.last_progress_at??null,stages_in_flight:latestProgress.stages_in_flight??[],failed_stages:latestProgress.failed_stages??[],completed_stage_count:latestProgress.completed_stage_count??0})}`),{ok:!1,text:timeoutText}}console.error(`${label} in progress... (elapsed: ${elapsed}s)`);let resp=await fetch(getUrl,{headers:await getGetHeaders()});if(resp.status===404||resp.status===202){let body=await resp.text();if(resp.status===202){let progress=parseAutomationProgress(body);progress&&(latestProgress=progress,emitAutomationProgressNotice(label,elapsed,progress))}}else{let isOk=resp.ok,text=await handleResponse(resp);return{ok:isOk,text}}schedule?waitMs=schedule.intervalMs:Date.now()-startTime>6e4&&(waitMs=3e4)}}function normalizeReviewRounds(rounds){if(rounds===1||rounds==="1")return 1;if(rounds===2||rounds==="2")return 2}var TICKET_ARTIFACTS={plan:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-plan`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/plan`,saveSubdir:"plans",filename:n=>`${n}-plan.md`,requestErrorPrefix:"Failed to request plan generation: ",confirmationText:n=>`Plan generation requested for ${n}. Processing typically takes 10-15 minutes. Use get_plan with ticket_number "${n}" to retrieve the plan once processing completes.`,pollLabel:n=>`Plan generation for ${n}`,pollSchedule:{initialDelayMs:18e4,intervalMs:6e4}},architecture:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-architecture`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/architecture-plan`,saveSubdir:"architecture",filename:n=>`${n}-architecture-plan.md`,requestErrorPrefix:"Failed to request architecture generation: ",confirmationText:n=>`Architecture generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_architecture with ticket_number "${n}" (or get_doc with doc_type "tdd") to retrieve the architecture plan once processing completes.`,pollLabel:n=>`Architecture generation for ${n}`},fsd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-fsd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/fsd`,saveSubdir:"fsd",filename:n=>`${n}-fsd-plan.md`,requestErrorPrefix:"Failed to request FSD generation: ",confirmationText:n=>`FSD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,pollLabel:n=>`FSD generation for ${n}`},prd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-prd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/prd`,saveSubdir:"prd",filename:n=>`${n}-prd-plan.md`,requestErrorPrefix:"Failed to request PRD generation: ",confirmationText:n=>`PRD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_prd with ticket_number "${n}" (or get_doc with doc_type "prd") to retrieve the PRD once processing completes.`,pollLabel:n=>`PRD generation for ${n}`},clarifying_questions:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/clarifying-questions`,saveSubdir:"clarifying-questions",filename:n=>`${n}-clarifying-questions.md`,requestErrorPrefix:"Failed to request clarifying questions: ",confirmationText:n=>`Clarifying questions requested for ${n}. Processing typically takes 1-5 minutes. Use get_clarifying_questions with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Clarifying questions for ${n}`},ticket_critique:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-critique`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-critique`,saveSubdir:"ticket-critiques",filename:n=>`${n}-ticket-quality-critique.md`,requestErrorPrefix:"Failed to request ticket critique: ",confirmationText:n=>`Ticket critique requested for ${n}. Processing typically takes 1-5 minutes. Use get_ticket_critique with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Ticket critique for ${n}`},reimplement_context:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/request-reimplement-context`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/reimplement-context`,saveSubdir:"reimplementations",filename:n=>`${n}-context.md`,requestErrorPrefix:"Failed to request reimplement context: ",confirmationText:n=>`Reimplement context processing requested for ${n}. Processing typically takes 1-2 minutes. Use get_reimplement_context with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Reimplement context for ${n}`},ticket_review:{kind:"review",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-review`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-review`,requestErrorPrefix:"Failed to request ticket review: ",confirmationText:n=>`Combined ticket review requested for ${n}. Processing typically takes 2-6 minutes. Use get_clarifying_questions and get_ticket_critique with ticket_number "${n}" to retrieve the two documents once processing completes.`,pollLabel:n=>`Ticket review for ${n}`,clarifySaveSubdir:"clarifying-questions",critiqueSaveSubdir:"ticket-critiques"}};function buildTicketArtifactRequestBody(args){let body={repo_name:REPO_NAME},trimmedSecondOpinion=args.second_opinion?.trim();trimmedSecondOpinion&&(body.provider_override=trimmedSecondOpinion);let trimmedProvider=args.provider?.trim();trimmedProvider&&!trimmedSecondOpinion&&(body.provider=trimmedProvider);let normalizedRounds=normalizeReviewRounds(args.rounds);return normalizedRounds!==void 0&&(body.rounds=normalizedRounds),body}async function getTicketArtifactDocsPath(subdir){switch(subdir){case"plans":return getDocsPath("plans");case"architecture":return getDocsPath("architecture");case"fsd":return getDocsPath("fsd");case"prd":return getDocsPath("prd");case"clarifying-questions":return getDocsPath("clarifying-questions");case"ticket-critiques":return getDocsPath("ticket-critiques");case"reimplementations":return getDocsPath("reimplementations")}}function resolveDesignDocArtifactType(docType){return docType==="fsd"?"fsd":docType==="prd"?"prd":"architecture"}async function requestTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))});if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(args.wait_for_result){let getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),config.pollSchedule);if(!result.ok)return{content:[{type:"text",text:result.text}]};let text=result.text;if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text);text+=note}return{content:[{type:"text",text:appendBackendWarningsToText(text,backendWarnings)}]}}return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]}}async function getTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],resp=await fetch(buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(resp.status===202)return{content:[{type:"text",text:await resp.text()}]};if(type==="reimplement_context"){if(resp.status===404)return{content:[{type:"text",text:JSON.stringify({error:"NOT_FOUND",message:`Reimplement context for ${args.ticket_number} is not yet available. Processing may still be in progress. Try again in a moment, or call request_reimplement_context to trigger processing.`})}]};let text2=await handleResponse(resp);if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text2);return{content:[{type:"text",text:text2+note}]}}return{content:[{type:"text",text:text2}]}}let ok=resp.ok,text=await handleResponse(resp);if(ok&&args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text);text+=note}return{content:[{type:"text",text}]}}async function requestTicketReview(args){let config=TICKET_ARTIFACTS.ticket_review,resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))});if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(!args.wait_for_result)return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]};let getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number));if(!result.ok)return{content:[{type:"text",text:result.text}]};let envelope;try{envelope=JSON.parse(result.text)}catch(parseErr){return{content:[{type:"text",text:`Failed to parse ticket review response: ${parseErr}
|
|
4878
|
+
Last observed progress: ${JSON.stringify({status:latestProgress.status??"in_progress",last_progress_at:latestProgress.last_progress_at??null,stages_in_flight:latestProgress.stages_in_flight??[],failed_stages:latestProgress.failed_stages??[],completed_stage_count:latestProgress.completed_stage_count??0})}`:"";for(;;){await new Promise(resolve2=>setTimeout(resolve2,waitMs));let elapsed=Math.round((Date.now()-startTime)/1e3);if(Date.now()-startTime>=timeoutMs){let situation=`${label} timed out after ${Math.round(timeoutMs/1e3)} seconds. The task may still be processing on the server.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}console.error(`${label} in progress... (elapsed: ${elapsed}s)`);let resp;try{resp=await fetch(getUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`${label} poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation=`${label} stopped polling after ${MAX_CONSECUTIVE_POLL_FAILURES} consecutive connection failures.${progressSuffix()}`;return{ok:!1,text:formatRecoverablePollGiveUp(situation,recovery)}}schedule?waitMs=schedule.intervalMs:Date.now()-startTime>6e4&&(waitMs=3e4);continue}if(consecutiveFetchFailures=0,resp.status===404||resp.status===202){let body=await resp.text();if(resp.status===202){let progress=parseAutomationProgress(body);progress&&(latestProgress=progress,emitAutomationProgressNotice(label,elapsed,progress))}}else{let isOk=resp.ok,text=await handleResponse(resp);return{ok:isOk,text}}schedule?waitMs=schedule.intervalMs:Date.now()-startTime>6e4&&(waitMs=3e4)}}function normalizeReviewRounds(rounds){if(rounds===1||rounds==="1")return 1;if(rounds===2||rounds==="2")return 2}var TICKET_ARTIFACTS={plan:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-plan`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/plan`,saveSubdir:"plans",filename:n=>`${n}-plan.md`,requestErrorPrefix:"Failed to request plan generation: ",confirmationText:n=>`Plan generation requested for ${n}. Processing typically takes 10-15 minutes. Use get_plan with ticket_number "${n}" to retrieve the plan once processing completes.`,pollLabel:n=>`Plan generation for ${n}`,pollSchedule:{initialDelayMs:18e4,intervalMs:6e4}},architecture:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-architecture`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/architecture-plan`,saveSubdir:"architecture",filename:n=>`${n}-architecture-plan.md`,requestErrorPrefix:"Failed to request architecture generation: ",confirmationText:n=>`Architecture generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_architecture with ticket_number "${n}" (or get_doc with doc_type "tdd") to retrieve the architecture plan once processing completes.`,pollLabel:n=>`Architecture generation for ${n}`},fsd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-fsd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/fsd`,saveSubdir:"fsd",filename:n=>`${n}-fsd-plan.md`,requestErrorPrefix:"Failed to request FSD generation: ",confirmationText:n=>`FSD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,pollLabel:n=>`FSD generation for ${n}`},prd:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-prd`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/prd`,saveSubdir:"prd",filename:n=>`${n}-prd-plan.md`,requestErrorPrefix:"Failed to request PRD generation: ",confirmationText:n=>`PRD generation requested for ${n}. Processing typically takes 2-4 minutes. Use get_prd with ticket_number "${n}" (or get_doc with doc_type "prd") to retrieve the PRD once processing completes.`,pollLabel:n=>`PRD generation for ${n}`},clarifying_questions:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/clarifying-questions`,saveSubdir:"clarifying-questions",filename:n=>`${n}-clarifying-questions.md`,requestErrorPrefix:"Failed to request clarifying questions: ",confirmationText:n=>`Clarifying questions requested for ${n}. Processing typically takes 1-5 minutes. Use get_clarifying_questions with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Clarifying questions for ${n}`},ticket_critique:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-critique`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-critique`,saveSubdir:"ticket-critiques",filename:n=>`${n}-ticket-quality-critique.md`,requestErrorPrefix:"Failed to request ticket critique: ",confirmationText:n=>`Ticket critique requested for ${n}. Processing typically takes 1-5 minutes. Use get_ticket_critique with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Ticket critique for ${n}`},reimplement_context:{kind:"single",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/request-reimplement-context`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/reimplement-context`,saveSubdir:"reimplementations",filename:n=>`${n}-context.md`,requestErrorPrefix:"Failed to request reimplement context: ",confirmationText:n=>`Reimplement context processing requested for ${n}. Processing typically takes 1-2 minutes. Use get_reimplement_context with ticket_number "${n}" to retrieve the results once processing completes.`,pollLabel:n=>`Reimplement context for ${n}`},ticket_review:{kind:"review",generateEndpoint:n=>`/ticket/${encodeURIComponent(n)}/generate-ticket-review`,getEndpoint:n=>`/ticket/${encodeURIComponent(n)}/ticket-review`,requestErrorPrefix:"Failed to request ticket review: ",confirmationText:n=>`Combined ticket review requested for ${n}. Processing typically takes 2-6 minutes. Use get_clarifying_questions and get_ticket_critique with ticket_number "${n}" to retrieve the two documents once processing completes.`,pollLabel:n=>`Ticket review for ${n}`,clarifySaveSubdir:"clarifying-questions",critiqueSaveSubdir:"ticket-critiques"}};function buildTicketArtifactRequestBody(args){let body={repo_name:REPO_NAME},trimmedSecondOpinion=args.second_opinion?.trim();trimmedSecondOpinion&&(body.provider_override=trimmedSecondOpinion);let trimmedProvider=args.provider?.trim();trimmedProvider&&!trimmedSecondOpinion&&(body.provider=trimmedProvider);let normalizedRounds=normalizeReviewRounds(args.rounds);return normalizedRounds!==void 0&&(body.rounds=normalizedRounds),body}async function getTicketArtifactDocsPath(subdir){switch(subdir){case"plans":return getDocsPath("plans");case"architecture":return getDocsPath("architecture");case"fsd":return getDocsPath("fsd");case"prd":return getDocsPath("prd");case"clarifying-questions":return getDocsPath("clarifying-questions");case"ticket-critiques":return getDocsPath("ticket-critiques");case"reimplementations":return getDocsPath("reimplementations")}}function resolveDesignDocArtifactType(docType){return docType==="fsd"?"fsd":docType==="prd"?"prd":"architecture"}async function requestTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(args.wait_for_result){let retrievalToolName=type==="fsd"?"get_doc":`get_${type}`,result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl,retrievalToolName},config.pollSchedule);if(!result.ok)return{content:[{type:"text",text:result.text}]};let text=result.text;if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text);text+=note}return{content:[{type:"text",text:appendBackendWarningsToText(text,backendWarnings)}]}}return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]}}async function getTicketArtifact(type,args){let config=TICKET_ARTIFACTS[type],resp=await fetch(buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(resp.status===202)return{content:[{type:"text",text:await resp.text()}]};if(type==="reimplement_context"){if(resp.status===404)return{content:[{type:"text",text:JSON.stringify({error:"NOT_FOUND",message:`Reimplement context for ${args.ticket_number} is not yet available. Processing may still be in progress. Try again in a moment, or call request_reimplement_context to trigger processing.`})}]};let text2=await handleResponse(resp);if(args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text2);return{content:[{type:"text",text:text2+note}]}}return{content:[{type:"text",text:text2}]}}let ok=resp.ok,text=await handleResponse(resp);if(ok&&args.save_locally){let note=await saveLocally(await getTicketArtifactDocsPath(config.saveSubdir),config.filename(args.ticket_number),text);text+=note}return{content:[{type:"text",text}]}}async function requestTicketReview(args){let config=TICKET_ARTIFACTS.ticket_review,getUrl=buildGetUrl(config.getEndpoint(args.ticket_number),{repo_name:REPO_NAME}),resp;try{resp=await fetch(buildUrl(config.generateEndpoint(args.ticket_number)),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(buildTicketArtifactRequestBody(args))})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request that triggered this",{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl})}]}}if(!resp.ok){let errorText=await handleResponse(resp);return{content:[{type:"text",text:`${config.requestErrorPrefix}${errorText}`}]}}let backendWarnings=[];try{backendWarnings=extractBackendWarnings(await resp.json())}catch{}if(!args.wait_for_result)return{content:[{type:"text",text:appendBackendWarningsToText(config.confirmationText(args.ticket_number),backendWarnings)}]};let result=await pollForResult(getUrl,9e5,config.pollLabel(args.ticket_number),{handleName:"ticket_number",handleValue:args.ticket_number,recoveryGetUrl:getUrl});if(!result.ok)return{content:[{type:"text",text:result.text}]};let envelope;try{envelope=JSON.parse(result.text)}catch(parseErr){return{content:[{type:"text",text:`Failed to parse ticket review response: ${parseErr}
|
|
4833
4879
|
Raw body: ${result.text}`}]}}let clarify=envelope.clarify??{},critique=envelope.critique??{},parts=[],notes="";if(clarify.status==="success"&&typeof clarify.content=="string"&&clarify.content){if(parts.push(clarify.content),args.save_locally&&clarify.doc_type){let filename=`${args.ticket_number}-${clarify.doc_type}`,note=await saveLocally(await getTicketArtifactDocsPath(config.clarifySaveSubdir),filename,clarify.content);notes+=note}}else parts.push(`> **Note:** Clarifying questions sub-flow ${clarify.status??"unavailable"} (no content returned).`);if(critique.status==="success"&&typeof critique.content=="string"&&critique.content){if(parts.push(critique.content),args.save_locally&&critique.doc_type){let filename=`${args.ticket_number}-${critique.doc_type}`,note=await saveLocally(await getTicketArtifactDocsPath(config.critiqueSaveSubdir),filename,critique.content);notes+=note}}else parts.push(`> **Note:** Ticket critique sub-flow ${critique.status??"unavailable"} (no content returned).`);let text=parts.join(`
|
|
4834
4880
|
|
|
4835
4881
|
---
|
|
@@ -4847,7 +4893,7 @@ ${content.slice(0,MAX_INLINE_TEXT_LENGTH)}
|
|
|
4847
4893
|
|
|
4848
4894
|
[Content truncated. Full content saved to ${resolvedSave}]`:resultText+=`
|
|
4849
4895
|
|
|
4850
|
-
${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text}]}}case"delete":{let{ticket_number,file_name}=args,url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an implementation plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 10-15 minutes depending on ticket complexity and number of attachments. The matching get_plan tool retrieves the generated plan later (call get_plan with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns the plan directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 10-15 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("plan",args));registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Estimate a Jira Epic or ticket-key group via the epic estimation orchestrator. Exactly one of epic_key/ticket_keys required (never both). allow_partial allows a partial result on child failures (default: fail-closed). No mode field; source is inferred.",inputSchema:{epic_key:z16.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z16.array(z16.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z16.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("request_architecture",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an architecture plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_architecture tool retrieves the generated architecture plan later (call get_architecture with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("architecture",args));registerTool("request_prd",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("prd",args));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:z16.string().optional().describe("Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."),provider:z16.string().optional().describe("Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence.")}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("request_clarifying_questions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of clarifying questions or debugging guidance for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_clarifying_questions tool retrieves the generated questions later (call get_clarifying_questions with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns them directly. For bug tickets, the result may be debugging guidance instead of clarifying questions. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("clarifying_questions",args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a Jira ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_critique` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));registerTool("request_ticket_critique",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a ticket critique for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_ticket_critique tool retrieves the generated critique later (call get_ticket_critique with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("ticket_critique",args));registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two requests sequentially. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-6 minutes. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until both documents are ready and receive them concatenated.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z16.union([z16.literal(1),z16.literal(2),z16.literal("1"),z16.literal("2"),z16.literal("")]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing.")}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented Jira ticket. Use this for follow-up requests on tickets that have already been through the plan+implement cycle. This triggers an asynchronous background job to process new attachments/images. The matching get_reimplement_context tool retrieves the assembled context later (call get_reimplement_context with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Set wait_for_result to true to block until the context is ready instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a Jira ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z16.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z16.array(z16.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_jira_transitions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List available Jira workflow transitions for a ticket. Returns each transition's id, name, and target status. Use this to discover what status changes are possible for a given ticket. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-transitions`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',inputSchema:{ticket_number:commonFields.ticket_number,target_status:z16.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z16.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("resolve_target_status",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,force_rerun:z16.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")}},async({ticket_number,force_rerun})=>{let payload={repo_name:REPO_NAME,ticket_number};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-target-status"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","ai_automation_level","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", ");registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list.",inputSchema:z16.discriminatedUnion("operation",[z16.object({operation:z16.literal("get"),field_name:z16.string().describe(`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`)}).strict(),z16.object({operation:z16.literal("update"),field_name:z16.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`),value:z16.union([z16.string(),z16.boolean(),z16.array(z16.string()),z16.array(z16.record(z16.string(),z16.unknown())),z16.record(z16.string(),z16.union([z16.string(),z16.null()]))]).optional().describe(`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z16.string().optional().describe("Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."),only_if_null:z16.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}).strict(),z16.object({operation:z16.literal("list")}).strict()])},async args=>{switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("get_my_role",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:'Check the role and auth source for the current API key. Returns JSON with {role: "admin" | "member" | null, source: "user_access" | "legacy"}. Use this to check if the current key has admin permissions before attempting configuration updates via config_field with operation: "update". Non-admin user_access keys will be blocked from config updates.',inputSchema:{}},async()=>{let url=buildGetUrl("/my-role",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text);text=text+note}return{content:[{type:"text",text}]}});registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z16.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z16.record(z16.string(),z16.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud"){let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text+=`
|
|
4896
|
+
${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text}]}}case"delete":{let{ticket_number,file_name}=args,url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an implementation plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 10-15 minutes depending on ticket complexity and number of attachments. The matching get_plan tool retrieves the generated plan later (call get_plan with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns the plan directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 10-15 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("plan",args));registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Estimate a Jira Epic or ticket-key group via the epic estimation orchestrator. Exactly one of epic_key/ticket_keys required (never both). allow_partial allows a partial result on child failures (default: fail-closed). No mode field; source is inferred.",inputSchema:{epic_key:z16.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z16.array(z16.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z16.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("request_architecture",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an architecture plan for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_architecture tool retrieves the generated architecture plan later (call get_architecture with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("architecture",args));registerTool("request_prd",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("prd",args));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:z16.string().optional().describe("Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."),provider:z16.string().optional().describe("Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence.")}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z16.enum(["tdd","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("request_clarifying_questions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of clarifying questions or debugging guidance for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_clarifying_questions tool retrieves the generated questions later (call get_clarifying_questions with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns them directly. For bug tickets, the result may be debugging guidance instead of clarifying questions. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("clarifying_questions",args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a Jira ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_critique` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));registerTool("request_ticket_critique",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a ticket critique for a Jira ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_ticket_critique tool retrieves the generated critique later (call get_ticket_critique with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("ticket_critique",args));registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two requests sequentially. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-6 minutes. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until both documents are ready and receive them concatenated.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z16.union([z16.literal(1),z16.literal(2),z16.literal("1"),z16.literal("2"),z16.literal("")]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing.")}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented Jira ticket. Use this for follow-up requests on tickets that have already been through the plan+implement cycle. This triggers an asynchronous background job to process new attachments/images. The matching get_reimplement_context tool retrieves the assembled context later (call get_reimplement_context with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Set wait_for_result to true to block until the context is ready instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a Jira ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z16.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z16.array(z16.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_jira_transitions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List available Jira workflow transitions for a ticket. Returns each transition's id, name, and target status. Use this to discover what status changes are possible for a given ticket. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-transitions`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',inputSchema:{ticket_number:commonFields.ticket_number,target_status:z16.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z16.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("resolve_target_status",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,force_rerun:z16.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")}},async({ticket_number,force_rerun})=>{let payload={repo_name:REPO_NAME,ticket_number};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-target-status"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","ai_automation_level","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", ");registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list.",inputSchema:z16.discriminatedUnion("operation",[z16.object({operation:z16.literal("get"),field_name:z16.string().describe(`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`)}).strict(),z16.object({operation:z16.literal("update"),field_name:z16.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`),value:z16.union([z16.string(),z16.boolean(),z16.array(z16.string()),z16.array(z16.record(z16.string(),z16.unknown())),z16.record(z16.string(),z16.union([z16.string(),z16.null()]))]).optional().describe(`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z16.string().optional().describe("Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."),only_if_null:z16.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}).strict(),z16.object({operation:z16.literal("list")}).strict()])},async args=>{switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("get_my_role",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:'Check the role and auth source for the current API key. Returns JSON with {role: "admin" | "member" | null, source: "user_access" | "legacy"}. Use this to check if the current key has admin permissions before attempting configuration updates via config_field with operation: "update". Non-admin user_access keys will be blocked from config updates.',inputSchema:{}},async()=>{let url=buildGetUrl("/my-role",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. concise_tool_capabilities is an additive projection (tier1/tier2, available_now only, {tools: [{tool, display_name}], more_count}); render as given, never recompute. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text);text=text+note}return{content:[{type:"text",text}]}});registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z16.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z16.record(z16.string(),z16.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud"){let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text+=`
|
|
4851
4897
|
|
|
4852
|
-
\u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`)}}catch{}return{content:[{type:"text",text}]}});registerTool("persist_routing_credential",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",inputSchema:{repo_name:z16.string().describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process.")}},async({repo_name})=>{let repoName=typeof repo_name=="string"?repo_name.trim():"",deps=buildCredentialStoreWriteDeps(),storePath=getPrimaryCredentialStorePath(deps);if(repoName.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,message:"Cannot persist routing credential: repo_name is required. Pass the repo name this install is configuring.",path:storePath})}]};let target=`bapi:${repoName}`,apiKey=await getResolvedApiKeyForRepo(repoName);if(apiKey.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,target,path:storePath,message:`No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`})}]};let result=await upsertBapiCredential(repoName,apiKey,deps);return result.ok?{content:[{type:"text",text:JSON.stringify({ok:!0,action:result.action,target:result.target,path:result.path,migratedFallback:result.migratedFallback,message:`Stored routing credential for ${result.target} at ${result.path}.`})}]}:{content:[{type:"text",text:JSON.stringify({ok:!1,target:result.target,path:result.path,kind:result.kind,message:`Failed to persist routing credential for ${result.target}: ${result.error} You can rerun /install-bridge or migrate manually.`})}]}});function formatDeepResearchProviderReason(meta){if(!meta)return"";let parts=[],reason=meta.incomplete_details?.reason;reason&&parts.push(`provider reason: ${reason}`);let errMsg=meta.error?.message,errCode=meta.error?.code;return(errMsg||errCode)&&(errCode&&errMsg?parts.push(`provider error: ${errCode}: ${errMsg}`):errMsg?parts.push(`provider error: ${errMsg}`):errCode&&parts.push(`provider error: ${errCode}`)),parts.length?` (${parts.join("; ")})`:""}function _safeIsoMs(value){if(!value)return null;let ms=new Date(value).getTime();return Number.isNaN(ms)?null:ms}function formatDeepResearchElapsed(createdAt,lastPollAt){let createdMs=_safeIsoMs(createdAt);if(createdMs===null)return"";let now=Date.now(),startedMs=Math.max(0,now-createdMs),startedMin=Math.floor(startedMs/6e4),lastPollMs=_safeIsoMs(lastPollAt),pollSuffix="";return lastPollMs!==null&&(pollSuffix=`, last poll ${Math.max(0,Math.floor((now-lastPollMs)/1e3))}s ago`),` (running ${startedMin}m${pollSuffix})`}function formatDeepResearchFailure(body){let kind=body.error_kind||body.error_message||"Unknown error",reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`}function formatDeepResearchStatus(body,taskId){let elapsed=formatDeepResearchElapsed(body.created_at,body.last_poll_at),reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`}registerTool("request_deep_research",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",inputSchema:{query:z16.string().describe("The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"),context:z16.string().optional().describe("Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"),ticket_number:commonFields.ticket_number.optional(),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally}},async({query,context,ticket_number,wait_for_result,save_locally})=>{let submitPayload={repo_name:REPO_NAME,query};context&&(submitPayload.context=context),ticket_number&&(submitPayload.ticket_number=ticket_number);let submitResp=await fetch(buildUrl("/deep-research"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let taskId=(await submitResp.json()).task_id;if(!wait_for_result)return{content:[{type:"text",text:`Deep research submitted (task_id: ${taskId}). Processing typically takes 2-10 minutes. Use get_deep_research with task_id ${taskId} to retrieve the result once processing completes.`}]};let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,lastStatus="queued",latestStatusBody=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3);console.error(`Deep research in progress... (elapsed: ${elapsed}s, status: ${lastStatus})`);let statusUrl=buildGetUrl(`/deep-research/${taskId}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok){let errorText=await handleResponse(statusResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error polling deep research status: ${errorText}`})}]}}let statusBody=await statusResp.json();if(lastStatus=statusBody.status,latestStatusBody=statusBody,lastStatus==="completed")break;if(lastStatus==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}if(lastStatus!=="completed"){let statusSuffix=latestStatusBody?` ${formatDeepResearchStatus(latestStatusBody,taskId)}`:"";return{content:[{type:"text",text:`Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`}]}}let resultUrl=buildGetUrl(`/deep-research/${taskId}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok){let errorText=await handleResponse(resultResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error retrieving deep research result: ${errorText}`})}]}}let resultText=await resultResp.text();if(save_locally){let slug=slugify(query),note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${taskId}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});registerTool("get_deep_research",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",inputSchema:{task_id:z16.number().describe("The task ID returned by request_deep_research."),query_slug:z16.string().optional().describe("Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."),save_locally:commonFields.save_locally}},async({task_id,query_slug,save_locally})=>{let statusUrl=buildGetUrl(`/deep-research/${task_id}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(statusBody.status==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};if(statusBody.status!=="completed")return{content:[{type:"text",text:formatDeepResearchStatus(statusBody,task_id)}]};let resultUrl=buildGetUrl(`/deep-research/${task_id}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let resultText=await resultResp.text();if(save_locally){let slug=query_slug||"research",note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${task_id}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});var BRAINSTORM_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isBrainstormTerminalStatus(status){return BRAINSTORM_TERMINAL_STATUSES.has(status)}async function pollBrainstormUntilTerminal(brainstormId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let statusUrl=buildGetUrl(`/brainstorms/${brainstormId}/status`,{repo_name:repoName}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok||(latest=await statusResp.json(),latest.rows.every(row=>isBrainstormTerminalStatus(row.status))))return latest;Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}return latest}async function saveBrainstormResultsLocally(envelope,subject){let dir=await getDocsPath("brainstorm");return saveBrainstormResultsToDir(envelope,dir,subject)}function formatBrainstormToolResponse(envelope,savedPaths){let lines=[];lines.push(`# Council ${envelope.brainstorm_id}`),lines.push(`Repo: ${envelope.repo_name}`),lines.push("");for(let row of envelope.results)lines.push(`## ${row.provider} \u2014 status: ${row.status}`),lines.push(`error_kind: ${row.error_kind??"null"}`),row.error_message&&lines.push(`error_message: ${row.error_message}`),row.markdown&&(lines.push(""),lines.push(row.markdown)),lines.push("");if(savedPaths.length>0){lines.push("---"),lines.push("Saved files:");for(let p of savedPaths)lines.push(`- ${p}`)}return lines.join(`
|
|
4853
|
-
`)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start an async council that fans out a task to multiple opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_council to retrieve. Generates and persists a retrievable artifact.",inputSchema:{task_description:z16.string().describe("Free-form description of the task to brainstorm about. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z16.array(z16.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z16.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_brainstorm_id:z16.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. When provided, the prior brainstorm's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z16.enum(["technical","design","discovery","general"]).optional().describe("Preferred brainstorm-mode selector for new callers. 'technical' (default) is the implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped discovery questions for early/vague tasks. 'general' brainstorms from the supplied brief alone, with no indexed repository context required, unlike 'technical'/'discovery'. Takes precedence over the legacy boolean design field."),design:z16.boolean().optional().describe('Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'),lenses:z16.array(z16.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z16.boolean().optional().describe("Opt-in to trigger a second cross-examination debate round between providers (default off). When true, after round 1 completes each provider critiques the OTHER provider(s)' round-1 output, and the critique is appended to that provider's markdown under a '## Cross-examination' section.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_brainstorm_id,mode,design,lenses,debate})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_brainstorm_id&&(submitPayload.prior_brainstorm_request_id=prior_brainstorm_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let submitResp=await fetch(buildUrl("/brainstorms"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (brainstorm_id: ${submitBody.brainstorm_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`}]};if(!await pollBrainstormUntilTerminal(submitBody.brainstorm_id,effectiveRepo))return{content:[{type:"text",text:`Council timed out before reaching terminal status (brainstorm_id: ${submitBody.brainstorm_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope,task_description)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to retrieve the result envelope for a previously submitted council by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new council \u2014 use request_council first if none exists. Returns not-found when still processing.",inputSchema:{brainstorm_id:z16.string().describe("The brainstorm_id (UUID) returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({brainstorm_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/brainstorms/${brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z16.string().describe("The source branch name for the pull request"),base_branch:z16.string().describe("The target/destination branch name for the pull request"),title:z16.string().describe("The title of the pull request"),body:z16.string().optional().describe("The description/body of the pull request")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to discover checks for"),force_rerun:z16.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text=await handleResponse(resp);try{JSON.parse(text).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text=await handleResponse(resp);try{let parsed=JSON.parse(text);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text}]}});async function checkCiConfigAndDisablePoll(){try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&(pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: ci_check_config is null"))}else pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: could not read ci_check_config")}catch(err){pollCiChecksTool.disable(),console.error(`poll_ci_checks disabled: ${err}`)}}await checkCiConfigAndDisablePoll();registerTool("get_docs_dir",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, which requires shell access and may be blocked on some AI coding platforms.",inputSchema:{}},async()=>({content:[{type:"text",text:await getDocsDir()}]}));async function buildPipelineOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}async function buildChainOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,chainRecipes:CHAIN_RECIPES,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z16.array(z16.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z16.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z16.union([z16.literal(1),z16.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",rounds:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea)),(rounds===1||rounds===2)&&(mergedVariables.rounds=String(rounds));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED});return{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os16.tmpdir(),entries;try{entries=await readdir3(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path36.join(tmpDir,entry);try{let info=await stat10(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm3(fullPath,{recursive:!0,force:!0})}catch{}}}registerTool("materialize_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',inputSchema:{base_branch:z16.string().optional().describe(`Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z16.string().optional().describe("Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z16.string().optional().describe('Pass "true" to skip fetch/materialization and fall back to the local project root as-is.')}},async({base_branch,base_sha,no_refresh_base})=>{let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp3(path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path36.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink3(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}});registerTool("cleanup_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:"Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",inputSchema:{fresh_base_root:z16.string().describe("The fresh_base_root path returned by a prior materialize_fresh_base call.")}},async({fresh_base_root})=>{let allowedPrefix=path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path36.resolve(fresh_base_root),resolvedTmpDir=path36.resolve(os16.tmpdir()),isDirectChildOfTmpDir=path36.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path36.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm3(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})});ACTIVE_GROUPS.has("pipeline-authoring")&&(registerTool("list_pipelines",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"List all available pipeline recipes with their names, descriptions, and required variables. No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",inputSchema:{}},async()=>{await ensureCustomPipelinesLoaded();let list=Object.entries(PIPELINES2).map(([key,pipeline])=>({name:key,description:pipeline.description??"",variables:(pipeline.variables??[]).filter(v=>v!=="docs_dir"&&v!=="idea_hash"),source:userPipelineKeys.has(key)?"user":"bundled"}));return{content:[{type:"text",text:JSON.stringify(list,null,2)}]}}),registerTool("run_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional().describe("When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."),ttl_seconds:z16.number().int().positive().optional().describe("Override the default 24-hour idle TTL for this run. Must be a positive integer.")}},async input=>{let result=await runPipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("resume_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",inputSchema:{pipeline_run_id:z16.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),agent_result:z16.string().describe("The string the paused instruction's ## Return section asked you to produce")}},async input=>{let result=await resumePipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("list_pipeline_runs",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",inputSchema:{status:z16.enum(["running","paused","completed","failed","expired"]).optional().describe("Optional status filter")}},async input=>{let result=await listPipelineRuns(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("delete_pipeline_run",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",inputSchema:{pipeline_run_id:z16.string().describe("UUID of the pipeline run to delete.")}},async input=>{let result=await deletePipelineRun(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}));registerTool("run_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",inputSchema:{idea:z16.string().optional(),idea_file:z16.string().optional(),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional(),scheduled_at:z16.string().optional(),max_children:z16.number().int().positive().optional(),allow_duplicate:z16.boolean().optional(),agent:z16.enum(["claude"]).optional(),ttl_seconds:z16.number().int().positive().optional()}},async input=>{let{idea,idea_file,...rest}=input;if(idea!==void 0&&idea_file!==void 0)return{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:"Provide exactly one of `idea` or `idea_file`, not both."})}]};let resolved=await resolveTextOrFile(idea,idea_file,"idea");if(!resolved.ok)return resolved.errorResponse;let result=await runFullAutomation(await buildChainOrchestratorDeps(),{idea:resolved.text,...rest});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("resume_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",inputSchema:{chain_run_id:z16.string(),agent_result:z16.string()}},async input=>{let result=await resumeFullAutomation(await buildChainOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path36.posix.isAbsolute(value)||path36.win32.isAbsolute(value)||path36.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path36.resolve(await getDocsDir()),resolvedTarget=path36.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path36.sep)?{ok:!0,docsPath:path36.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options). Example: {"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z16.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path36.resolve(path36.dirname(fileURLToPath3(import.meta.url)),"../"),assetsDir;try{await stat10(path36.join(projectRootForAssets,"design-assets")),assetsDir=path36.join(projectRootForAssets,"design-assets")}catch{assetsDir=path36.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile14(path36.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile14(path36.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir12(docsPath,{recursive:!0}),await writeFile12(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,gate.handleList),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");var transport=new StdioServerTransport;await server.connect(transport);serverConnected=!0;toolSurfaceGate?.startPolling();console.error(`Bridge API MCP server ${VERSION} running on stdio`);(async()=>{await new Promise(r=>setTimeout(r,2e3));let result=await checkForUpdate();result?.updateAvailable&&server.server.sendLoggingMessage({level:"notice",logger:"bridge-api",data:`Update available: running ${result.currentVersion}, latest ${result.latestVersion} \u2014 run npx -y @bridge_gpt/mcp-server@latest --upgrade`})})().catch(()=>{});pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
|
|
4898
|
+
\u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`)}}catch{}return{content:[{type:"text",text}]}});registerTool("persist_routing_credential",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",inputSchema:{repo_name:z16.string().describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process.")}},async({repo_name})=>{let repoName=typeof repo_name=="string"?repo_name.trim():"",deps=buildCredentialStoreWriteDeps(),storePath=getPrimaryCredentialStorePath(deps);if(repoName.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,message:"Cannot persist routing credential: repo_name is required. Pass the repo name this install is configuring.",path:storePath})}]};let target=`bapi:${repoName}`,apiKey=await getResolvedApiKeyForRepo(repoName);if(apiKey.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,target,path:storePath,message:`No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`})}]};let result=await upsertBapiCredential(repoName,apiKey,deps);return result.ok?{content:[{type:"text",text:JSON.stringify({ok:!0,action:result.action,target:result.target,path:result.path,migratedFallback:result.migratedFallback,message:`Stored routing credential for ${result.target} at ${result.path}.`})}]}:{content:[{type:"text",text:JSON.stringify({ok:!1,target:result.target,path:result.path,kind:result.kind,message:`Failed to persist routing credential for ${result.target}: ${result.error} You can rerun /install-bridge or migrate manually.`})}]}});function formatDeepResearchProviderReason(meta){if(!meta)return"";let parts=[],reason=meta.incomplete_details?.reason;reason&&parts.push(`provider reason: ${reason}`);let errMsg=meta.error?.message,errCode=meta.error?.code;return(errMsg||errCode)&&(errCode&&errMsg?parts.push(`provider error: ${errCode}: ${errMsg}`):errMsg?parts.push(`provider error: ${errMsg}`):errCode&&parts.push(`provider error: ${errCode}`)),parts.length?` (${parts.join("; ")})`:""}function _safeIsoMs(value){if(!value)return null;let ms=new Date(value).getTime();return Number.isNaN(ms)?null:ms}function formatDeepResearchElapsed(createdAt,lastPollAt){let createdMs=_safeIsoMs(createdAt);if(createdMs===null)return"";let now=Date.now(),startedMs=Math.max(0,now-createdMs),startedMin=Math.floor(startedMs/6e4),lastPollMs=_safeIsoMs(lastPollAt),pollSuffix="";return lastPollMs!==null&&(pollSuffix=`, last poll ${Math.max(0,Math.floor((now-lastPollMs)/1e3))}s ago`),` (running ${startedMin}m${pollSuffix})`}function formatDeepResearchFailure(body){let kind=body.error_kind||body.error_message||"Unknown error",reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`}function formatDeepResearchStatus(body,taskId){let elapsed=formatDeepResearchElapsed(body.created_at,body.last_poll_at),reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`}registerTool("request_deep_research",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",inputSchema:{query:z16.string().describe("The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"),context:z16.string().optional().describe("Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"),ticket_number:commonFields.ticket_number.optional(),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally}},async({query,context,ticket_number,wait_for_result,save_locally})=>{let submitPayload={repo_name:REPO_NAME,query};context&&(submitPayload.context=context),ticket_number&&(submitPayload.ticket_number=ticket_number);let submitResp=await fetch(buildUrl("/deep-research"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let taskId=(await submitResp.json()).task_id;if(!wait_for_result)return{content:[{type:"text",text:`Deep research submitted (task_id: ${taskId}). Processing typically takes 2-10 minutes. Use get_deep_research with task_id ${taskId} to retrieve the result once processing completes.`}]};let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,lastStatus="queued",latestStatusBody=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3);console.error(`Deep research in progress... (elapsed: ${elapsed}s, status: ${lastStatus})`);let statusUrl=buildGetUrl(`/deep-research/${taskId}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok){let errorText=await handleResponse(statusResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error polling deep research status: ${errorText}`})}]}}let statusBody=await statusResp.json();if(lastStatus=statusBody.status,latestStatusBody=statusBody,lastStatus==="completed")break;if(lastStatus==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}if(lastStatus!=="completed"){let statusSuffix=latestStatusBody?` ${formatDeepResearchStatus(latestStatusBody,taskId)}`:"";return{content:[{type:"text",text:`Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`}]}}let resultUrl=buildGetUrl(`/deep-research/${taskId}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok){let errorText=await handleResponse(resultResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error retrieving deep research result: ${errorText}`})}]}}let resultText=await resultResp.text();if(save_locally){let slug=slugify(query),note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${taskId}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});registerTool("get_deep_research",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",inputSchema:{task_id:z16.number().describe("The task ID returned by request_deep_research."),query_slug:z16.string().optional().describe("Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."),save_locally:commonFields.save_locally}},async({task_id,query_slug,save_locally})=>{let statusUrl=buildGetUrl(`/deep-research/${task_id}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(statusBody.status==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};if(statusBody.status!=="completed")return{content:[{type:"text",text:formatDeepResearchStatus(statusBody,task_id)}]};let resultUrl=buildGetUrl(`/deep-research/${task_id}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let resultText=await resultResp.text();if(save_locally){let slug=query_slug||"research",note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${task_id}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});var BRAINSTORM_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isBrainstormTerminalStatus(status){return BRAINSTORM_TERMINAL_STATUSES.has(status)}async function pollBrainstormUntilTerminal(brainstormId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"brainstorm_id",handleValue:brainstormId,recoveryGetUrl:buildGetUrl(`/brainstorms/${brainstormId}/result`,{repo_name:repoName}),retrievalToolName:"get_council"};for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3),statusUrl=buildGetUrl(`/brainstorms/${brainstormId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${brainstormId} status poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation2=`Council ${brainstormId} stopped polling after ${MAX_CONSECUTIVE_POLL_FAILURES} consecutive connection failures.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation2,recovery)}}Date.now()-startTime>6e4&&(pollIntervalMs=3e4);continue}if(consecutiveFetchFailures=0,!statusResp.ok)return{kind:"status",envelope:latest};if(latest=await statusResp.json(),latest.rows.every(row=>isBrainstormTerminalStatus(row.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${brainstormId} timed out after ${Math.round(MAX_TIMEOUT_MS/1e3)} seconds. The task may still be processing on the server.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation,recovery)}}async function saveBrainstormResultsLocally(envelope,subject){let dir=await getDocsPath("brainstorm");return saveBrainstormResultsToDir(envelope,dir,subject)}function formatBrainstormToolResponse(envelope,savedPaths){let lines=[];lines.push(`# Council ${envelope.brainstorm_id}`),lines.push(`Repo: ${envelope.repo_name}`),lines.push("");for(let row of envelope.results)lines.push(`## ${row.provider} \u2014 status: ${row.status}`),lines.push(`error_kind: ${row.error_kind??"null"}`),row.error_message&&lines.push(`error_message: ${row.error_message}`),row.markdown&&(lines.push(""),lines.push(row.markdown)),lines.push("");if(savedPaths.length>0){lines.push("---"),lines.push("Saved files:");for(let p of savedPaths)lines.push(`- ${p}`)}return lines.join(`
|
|
4899
|
+
`)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start an async council that fans out a task to multiple opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_council to retrieve. Generates and persists a retrievable artifact.",inputSchema:{task_description:z16.string().describe("Free-form description of the task to brainstorm about. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z16.array(z16.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z16.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_brainstorm_id:z16.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. When provided, the prior brainstorm's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z16.enum(["technical","design","discovery","general"]).optional().describe("Preferred brainstorm-mode selector for new callers. 'technical' (default) is the implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped discovery questions for early/vague tasks. 'general' brainstorms from the supplied brief alone, with no indexed repository context required, unlike 'technical'/'discovery'. Takes precedence over the legacy boolean design field."),design:z16.boolean().optional().describe('Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'),lenses:z16.array(z16.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z16.boolean().optional().describe("Opt-in to trigger a second cross-examination debate round between providers (default off). When true, after round 1 completes each provider critiques the OTHER provider(s)' round-1 output, and the critique is appended to that provider's markdown under a '## Cross-examination' section.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_brainstorm_id,mode,design,lenses,debate})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_brainstorm_id&&(submitPayload.prior_brainstorm_request_id=prior_brainstorm_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let submitResp;try{submitResp=await fetch(buildUrl("/brainstorms"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request_council tool")}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (brainstorm_id: ${submitBody.brainstorm_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`}]};let pollOutcome=await pollBrainstormUntilTerminal(submitBody.brainstorm_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (brainstorm_id: ${submitBody.brainstorm_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope,task_description)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to retrieve the result envelope for a previously submitted council by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new council \u2014 use request_council first if none exists. Returns not-found when still processing.",inputSchema:{brainstorm_id:z16.string().describe("The brainstorm_id (UUID) returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({brainstorm_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/brainstorms/${brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z16.string().describe("The source branch name for the pull request"),base_branch:z16.string().describe("The target/destination branch name for the pull request"),title:z16.string().describe("The title of the pull request"),body:z16.string().optional().describe("The description/body of the pull request")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to discover checks for"),force_rerun:z16.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text=await handleResponse(resp);try{JSON.parse(text).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z16.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text=await handleResponse(resp);try{let parsed=JSON.parse(text);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text}]}});async function checkCiConfigAndDisablePoll(){try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&(pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: ci_check_config is null"))}else pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: could not read ci_check_config")}catch(err){pollCiChecksTool.disable(),console.error(`poll_ci_checks disabled: ${err}`)}}await checkCiConfigAndDisablePoll();registerTool("get_docs_dir",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, which requires shell access and may be blocked on some AI coding platforms.",inputSchema:{}},async()=>({content:[{type:"text",text:await getDocsDir()}]}));async function buildPipelineOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}async function buildChainOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,chainRecipes:CHAIN_RECIPES,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z16.array(z16.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z16.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z16.union([z16.literal(1),z16.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",rounds:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea)),(rounds===1||rounds===2)&&(mergedVariables.rounds=String(rounds));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED});return{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os16.tmpdir(),entries;try{entries=await readdir3(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path36.join(tmpDir,entry);try{let info=await stat10(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm3(fullPath,{recursive:!0,force:!0})}catch{}}}registerTool("materialize_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',inputSchema:{base_branch:z16.string().optional().describe(`Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z16.string().optional().describe("Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z16.string().optional().describe('Pass "true" to skip fetch/materialization and fall back to the local project root as-is.')}},async({base_branch,base_sha,no_refresh_base})=>{let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp3(path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path36.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink3(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm3(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}});registerTool("cleanup_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:"Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",inputSchema:{fresh_base_root:z16.string().describe("The fresh_base_root path returned by a prior materialize_fresh_base call.")}},async({fresh_base_root})=>{let allowedPrefix=path36.join(os16.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path36.resolve(fresh_base_root),resolvedTmpDir=path36.resolve(os16.tmpdir()),isDirectChildOfTmpDir=path36.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path36.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm3(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})});ACTIVE_GROUPS.has("pipeline-authoring")&&(registerTool("list_pipelines",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"List all available pipeline recipes with their names, descriptions, and required variables. No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",inputSchema:{}},async()=>{await ensureCustomPipelinesLoaded();let list=Object.entries(PIPELINES2).map(([key,pipeline])=>({name:key,description:pipeline.description??"",variables:(pipeline.variables??[]).filter(v=>v!=="docs_dir"&&v!=="idea_hash"),source:userPipelineKeys.has(key)?"user":"bundled"}));return{content:[{type:"text",text:JSON.stringify(list,null,2)}]}}),registerTool("run_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",inputSchema:{pipeline:z16.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z16.record(z16.string(),z16.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional().describe("When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."),ttl_seconds:z16.number().int().positive().optional().describe("Override the default 24-hour idle TTL for this run. Must be a positive integer.")}},async input=>{let result=await runPipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("resume_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",inputSchema:{pipeline_run_id:z16.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),agent_result:z16.string().describe("The string the paused instruction's ## Return section asked you to produce")}},async input=>{let result=await resumePipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("list_pipeline_runs",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",inputSchema:{status:z16.enum(["running","paused","completed","failed","expired"]).optional().describe("Optional status filter")}},async input=>{let result=await listPipelineRuns(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("delete_pipeline_run",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",inputSchema:{pipeline_run_id:z16.string().describe("UUID of the pipeline run to delete.")}},async input=>{let result=await deletePipelineRun(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}));registerTool("run_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",inputSchema:{idea:z16.string().optional(),idea_file:z16.string().optional(),auto_approve:z16.union([z16.boolean(),z16.literal("true"),z16.literal("false")]).optional(),scheduled_at:z16.string().optional(),max_children:z16.number().int().positive().optional(),allow_duplicate:z16.boolean().optional(),agent:z16.enum(["claude"]).optional(),ttl_seconds:z16.number().int().positive().optional()}},async input=>{let{idea,idea_file,...rest}=input;if(idea!==void 0&&idea_file!==void 0)return{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:"Provide exactly one of `idea` or `idea_file`, not both."})}]};let resolved=await resolveTextOrFile(idea,idea_file,"idea");if(!resolved.ok)return resolved.errorResponse;let result=await runFullAutomation(await buildChainOrchestratorDeps(),{idea:resolved.text,...rest});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("resume_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",inputSchema:{chain_run_id:z16.string(),agent_result:z16.string()}},async input=>{let result=await resumeFullAutomation(await buildChainOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path36.posix.isAbsolute(value)||path36.win32.isAbsolute(value)||path36.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path36.resolve(await getDocsDir()),resolvedTarget=path36.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path36.sep)?{ok:!0,docsPath:path36.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options). Example: {"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z16.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path36.resolve(path36.dirname(fileURLToPath3(import.meta.url)),"../"),assetsDir;try{await stat10(path36.join(projectRootForAssets,"design-assets")),assetsDir=path36.join(projectRootForAssets,"design-assets")}catch{assetsDir=path36.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile14(path36.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile14(path36.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir12(docsPath,{recursive:!0}),await writeFile12(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,gate.handleList),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");var transport=new StdioServerTransport;await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();console.error(`Bridge API MCP server ${VERSION} running on stdio`);(async()=>{await new Promise(r=>setTimeout(r,2e3));let result=await checkForUpdate();result?.updateAvailable&&server.server.sendLoggingMessage({level:"notice",logger:"bridge-api",data:`Update available: running ${result.currentVersion}, latest ${result.latestVersion} \u2014 run npx -y @bridge_gpt/mcp-server@latest --upgrade`})})().catch(()=>{});pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
|