@bridge_gpt/mcp-server 0.2.32 → 0.2.34
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 +51 -21
- package/build/conductor/bridge-api-client.js +37 -3
- package/build/conductor-bin.js +1 -1
- package/build/connect-github-api.js +103 -0
- package/build/connect-github.js +325 -39
- package/build/index.js +22 -10
- package/build/install-bridge.js +323 -68
- package/build/readme.generated.js +1 -1
- package/build/version.generated.js +1 -1
- package/docs/install/github-app.md +96 -8
- 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.34"}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
|
|
3
3
|
|
|
4
4
|
$ARGUMENTS
|
|
5
5
|
|
|
@@ -1659,7 +1659,7 @@ or failed (with the CLI error).
|
|
|
1659
1659
|
WHERE run_id = @run_id AND worker_id = @worker_id AND state = 'pending'
|
|
1660
1660
|
AND julianday(available_at) <= julianday('now')
|
|
1661
1661
|
ORDER BY seq ASC
|
|
1662
|
-
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(/\/+$/,""),path39=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path39}`).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 path39=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path39),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 path15 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 pathApiForPlatform3(platform){return platform==="win32"?path15.win32:path15.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=pathApiForPlatform3(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 path16 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 path16.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 path17 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(`
|
|
1662
|
+
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 formatValidationDetailItem(item){if(!item||typeof item!="object")return;let record=item,msg=record.msg;if(typeof msg!="string"||!msg.trim())return;let loc=record.loc,parts=Array.isArray(loc)?loc.filter(part=>typeof part=="string"||typeof part=="number"):[],path39=(parts[0]==="body"?parts.slice(1):parts).slice(0,CONDUCTOR_VALIDATION_LOC_MAX_PARTS).join(".");return path39?`${path39}: ${msg}`:msg}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(Array.isArray(detail))message=formatValidationDetailItem(detail[0]);else 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(/\/+$/,""),path39=apiPath.startsWith("/")?apiPath:`/${apiPath}`;return new URL(`${trimmed}${path39}`).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 path39=epicDispatchTransitionApiPath(request.dispatchKey,request.nextStatus),url=buildConductorJiraUrl(access2.baseUrl,path39),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,CONDUCTOR_VALIDATION_LOC_MAX_PARTS,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;CONDUCTOR_VALIDATION_LOC_MAX_PARTS=8;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 path15 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 pathApiForPlatform3(platform){return platform==="win32"?path15.win32:path15.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=pathApiForPlatform3(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 path16 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 path16.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 path17 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(`
|
|
1663
1663
|
`)}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(`
|
|
1664
1664
|
`)){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(`
|
|
1665
1665
|
`)}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(`
|
|
@@ -3916,7 +3916,7 @@ active \u2014 the server-side reconciler will pick it up within ~30s."
|
|
|
3916
3916
|
## Return
|
|
3917
3917
|
|
|
3918
3918
|
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.
|
|
3919
|
-
`};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**`How would you like to connect to Bridge API?`** with three numbered choices:\n\n```\n1. I have a Bridge API key\n2. I have an invite token\n3. I\'m new \u2014 set me up with just my email\n```\n\n- **1** \u2014 the existing-key flow. It asks for your **API key** (generate one on the\n Bridge API web UI **Security** page) and a **repo name** matching your server-side\n registration; everything else is derived. A `bapi_inv_\u2026` credential entered here\n instead of a full API key is automatically detected and redeemed as a **bootstrap\n invite** \u2014 it creates a brand-new project and mints your admin API key rather than\n looking up an existing repository.\n- **2** \u2014 the **bootstrap-invite** flow. It asks for the invite token you were given,\n with echo suppressed. Same as passing `--invite` (see below).\n- **3** \u2014 the **self-serve** flow, and the right answer if you have nothing yet. It\n asks for an **email**, then a name for your new Bridge project, and creates the\n workspace and your own admin API key for you. No account, no key, and no invite\n needed beforehand. Same as passing `--email you@example.com` (see below).\n\nThere is **no default**: pressing Enter selects nothing. A blank or invalid answer\nre-prompts once with a hint, then exits with guidance naming all three routes.\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 concise **capability report**\n("What Bridge can help with"), and recommends `/learn-repository` as your next step.\nIt does **not** run `/learn-repository` itself \u2014 that stays your next explicit\ninvocation. There is **no indexing question**: indexing starts automatically\nserver-side once the repository reaches full parse readiness. Add `--dry-run` to\npreview every step without writing, pinging, or spawning anything.\n\n**Were you sent a bootstrap invite?** Then you don\'t need an API key or the web UI\nat all \u2014 run the command your operator gave you:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --invite\n```\n\nThat one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap\ninvite token with **echo suppressed**, and sends it only in the request body. It\ncreates your project and mints your own admin API key in a single command.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` to derive and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically and redeemed the same way `--invite` is, skipping\n repository lookup entirely. `--invite` and `--email` remain the preferred,\n explicit entry points for a new project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. If the server is older,\n the key can\'t be uniquely resolved, or the lookup fails, `install-bridge` falls\n back to an inferred default you confirm interactively (and requires `--repo`\n when stdin is non-interactive). Whatever name is used MUST match the\n server-side repository registration.\n\n#### Self-serve email onboarding (`--email`) \u2014 no account, no key, no invite\n\nThe primary path for a **first-time user with nothing yet** \u2014 no Bridge account,\nno API key, and no pre-issued invite. Run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --email you@example.com\n```\n\n`install-bridge` requests a brand-new Bridge workspace for that email, receives an\ninvite token, and then feeds it into the **exact same** persist-before-exchange\nbootstrap protocol as `--invite` below \u2014 so the project is created and your first\nadmin key is minted in one command. The minted token is used internally and **never\nshown**.\n\nThe email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a\n**visible** interactive prompt (email is not a secret, so it is echoed as you type \u2014\nunlike the API key and the invite token, which use a hidden prompt). That prompt is\nwhat **option 3** of the bare-run chooser reaches, so\n`install-bridge --email you@example.com` and a bare `install-bridge` + `3` land in\nthe 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\n##### Self-serve retries resume automatically\n\nA self-serve run that fails part-way through \u2014 a network blip on the exchange, a\nfailed connectivity check, an interrupted credential write \u2014 saves its signup state\nunder `bootstrap-pending:<repo>` in the credential store (mode `0600`, fsync\'d, the\nsame record that already holds your `key_secret`).\n\n**Just re-run the self-serve flow.** It detects that saved attempt, prints\n`resuming your previous signup attempt for <repo>`, and re-drives the *same*\nexchange \u2014 it does **not** sign up again, so a retry never creates a second\nworkspace. This is why the self-serve record stores the minted invite token: you\nwere never shown that token, so nothing else could re-present it.\n\nDo **not** copy, display, or hand-remove that record. If the saved invite has\ngenuinely expired, the CLI says so and **asks for confirmation** before discarding\nit and starting fresh \u2014 it never discards it silently, because a record whose\nexchange already succeeded is the only trace of a live admin key.\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 \u2014 titled **`bridge doctor \u2014 read-only diagnostics`** \u2014 that diagnoses your whole Bridge install without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nThe report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **60 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
|
|
3919
|
+
`};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**`How would you like to connect to Bridge API?`** with three numbered choices:\n\n```\n1. I have an invite token\n2. I\'m new \u2014 set me up with just my email\n3. I have an API key for an existing project\n```\n\nThe two routes that **create** a project lead, because they are what a first-time\nuser needs; the existing-project route is last and says so in its own label.\n\n- **1** \u2014 the **bootstrap-invite** flow. It asks for the invite token you were given,\n with echo suppressed. Same as passing `--invite` (see below).\n- **2** \u2014 the **self-serve** flow, and the right answer if you have nothing yet. It\n asks for an **email**, then a name for your new Bridge project, and creates the\n workspace and your own admin API key for you. No account, no key, and no invite\n needed beforehand. Same as passing `--email you@example.com` (see below).\n- **3** \u2014 the existing-project flow. It asks for your **API key** (generate one on the\n Bridge API web UI **Security** page) and resolves your project from that key;\n everything else is derived.\n\nThere is **no default**: pressing Enter selects nothing. A blank or invalid answer\nre-prompts once with a hint, then exits with guidance naming all three routes.\n\n**Pasted the wrong kind of credential?** Both directions are detected from the value\'s\n**shape alone** \u2014 no probe request is ever sent \u2014 and neither switches routes silently:\n\n- A **`bapi_inv_\u2026` invite entered as an API key** is announced, then confirmed before\n it is redeemed to create a new project. A non-interactive run keeps the existing\n automatic switch, but now prints the notice.\n- An **API-key-shaped value entered as an invite** is announced and can switch to the\n existing-project route only after you confirm. A non-interactive run exits with\n guidance instead of switching, so a script is never silently redirected. The check\n happens **before** any project-name prompt, credential-store write, or network call,\n so a declined switch leaves nothing behind.\n\nA value that is neither shape (a mistyped invite, say) is **not** reclassified \u2014 it\nstays on the route you picked.\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 concise **capability report**\n("What Bridge can help with"), and recommends `/learn-repository` as your next step.\nIt does **not** run `/learn-repository` itself \u2014 that stays your next explicit\ninvocation. There is **no indexing question**: indexing starts automatically\nserver-side once the repository reaches full parse readiness. Add `--dry-run` to\npreview every step without writing, pinging, or spawning anything.\n\n**Were you sent a bootstrap invite?** Then you don\'t need an API key or the web UI\nat all \u2014 run the command your operator gave you:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --invite\n```\n\nThat one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap\ninvite token with **echo suppressed**, and sends it only in the request body. It\ncreates your project and mints your own admin API key in a single command.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` to derive and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically, **announced**, and (on a terminal) confirmed before it is\n redeemed the same way `--invite` is, skipping repository lookup entirely.\n `--invite` and `--email` remain the preferred, explicit entry points for a new\n project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. What happens when that\n lookup does not return a name is **tiered**, because the causes are not alike:\n - **The key doesn\'t identify exactly one project** \u2014 the run **stops**. It never\n guesses a name, because an accepted guess is just a `403` two steps later. Re-run\n with `--repo <name>`, or rotate the key on the **Security** page so it maps to a\n single project.\n - **The server is older, or the lookup fails transiently** \u2014 still recoverable. You\n are asked to type the registered name, with **no pre-filled default** (and\n `--repo` is required when stdin is non-interactive).\n\n Whatever name is used MUST match the 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 **option 2** of the bare-run chooser reaches, so\n`install-bridge --email you@example.com` and a bare `install-bridge` + `2` land in\nthe 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\n##### Self-serve retries resume automatically\n\nA self-serve run that fails part-way through \u2014 a network blip on the exchange, a\nfailed connectivity check, an interrupted credential write \u2014 saves its signup state\nunder `bootstrap-pending:<repo>` in the credential store (mode `0600`, fsync\'d, the\nsame record that already holds your `key_secret`).\n\n**Just re-run the self-serve flow.** It detects that saved attempt, prints\n`resuming your previous signup attempt for <repo>`, and re-drives the *same*\nexchange \u2014 it does **not** sign up again, so a retry never creates a second\nworkspace. This is why the self-serve record stores the minted invite token: you\nwere never shown that token, so nothing else could re-present it.\n\nDo **not** copy, display, or hand-remove that record. If the saved invite has\ngenuinely expired, the CLI says so and **asks for confirmation** before discarding\nit and starting fresh \u2014 it never discards it silently, because a record whose\nexchange already succeeded is the only trace of a live admin key.\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**Pasted an API key here by mistake?** The invite input recognizes a Bridge API key\nby its shape and says so **before** it asks you to name a project, writes anything to\nthe credential store, or contacts the server \u2014 so nothing is created and no invite is\nspent. On a terminal you\'re offered the existing-project route in place; a\nnon-interactive run exits with guidance to re-run with `--api-key` instead. A value\nthat is neither a `bapi_inv_\u2026` invite nor a valid key shape is left alone and\ncontinues down this path.\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 \u2014 titled **`bridge doctor \u2014 read-only diagnostics`** \u2014 that diagnoses your whole Bridge install without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nThe report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **60 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
|
|
3920
3920
|
${errors.join(`
|
|
3921
3921
|
`)}`);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}}init_commands_generated();import{writeFile as writeFile2,mkdir as mkdir2,readFile as readFile3,stat}from"fs/promises";import path5 from"path";var AGENTS={"jira-ticket-writer":{frontmatter:{name:"jira-ticket-writer",description:`Use this agent when the user describes a problem, feature request, bug, or improvement and wants a structured Jira ticket written as a markdown file. This agent performs deep codebase research before writing the ticket to ensure requirements reference existing code, patterns, and extension points.\\n\\nExamples:\\n\\n<example>\\nContext: The user describes a feature they want to add to the project.\\nuser: "We need to add rate limiting to our LLM integration so we don't exceed provider quotas"\\nassistant: "I'll use the Task tool to launch the jira-ticket-writer agent to research the codebase and create a structured Jira ticket for this feature."\\n<commentary>\\nSince the user is describing a problem/feature that needs a Jira ticket, use the jira-ticket-writer agent to research the codebase thoroughly and produce a well-structured ticket with code references.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user wants a ticket for a bug fix.\\nuser: "There's an issue where custom object iterators aren't being closed properly in some of our job scripts, can you write a ticket for that?"\\nassistant: "I'll use the Task tool to launch the jira-ticket-writer agent to investigate which job scripts have this issue and create a detailed Jira ticket."\\n<commentary>\\nThe user explicitly wants a Jira ticket written. Use the jira-ticket-writer agent so it can scan the relevant job scripts, identify the specific files and functions affected, and produce a ticket with precise code references.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user describes an improvement they want.\\nuser: "We should add support for a new LLM provider - Mistral - to our integration cartridge"\\nassistant: "Let me use the Task tool to launch the jira-ticket-writer agent to research the existing LLM integration architecture and write a comprehensive Jira ticket for adding Mistral support."\\n<commentary>\\nThe user wants a new feature added. The jira-ticket-writer agent will research the existing LLM client architecture, provider patterns, service definitions, and normalization helpers to write a ticket that references all the specific files and patterns that need to be extended.\\n</commentary>\\n</example>`,model:"opus",color:"blue"},body:`
|
|
3922
3922
|
You are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.
|
|
@@ -4765,7 +4765,7 @@ Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${error
|
|
|
4765
4765
|
`:`
|
|
4766
4766
|
|
|
4767
4767
|
`;return{action:"append",content:content+boundary+renderCodexBridgeToml(entry)}}async function readTomlHostConfig(path39,deps){try{return{state:"text",content:await deps.readFile(path39)}}catch(err){return isEnoent(err)?{state:"missing"}:{state:"read-error",message:"config could not be read"}}}async function writeCodexHostConfig(path39,entry,deps){let read=await readTomlHostConfig(path39,deps),merge=mergeCodexHostConfig(read,entry);return merge.action==="manual-required"?{status:"manual-required"}:(await deps.mkdir(dirnameOf(path39),{recursive:!0}),await deps.writeFile(path39,merge.content),{status:merge.action==="create"?"created":"direct-written",path:path39})}async function inspectJsonHostEntry(path39,target,deps){let read=await readJsonHostConfig(path39,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(path39,deps){let read=await readTomlHostConfig(path39,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 path39=resolveTargetAbsPath(target,ctx);return target.format==="toml"?inspectTomlHostEntry(path39,deps):inspectJsonHostEntry(path39,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 path39=resolveTargetAbsPath(target,deps);if(target.format==="toml")return(await writeCodexHostConfig(path39,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(path39,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)+`
|
|
4768
|
-
`}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_git_ignore_utils();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,path39,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path39}`,{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 path27 from"path";import readline2 from"readline";init_bridge_config();init_start_tickets_repo();init_credential_store();var USAGE2=`Usage: connect-github [--repo <repo_name>]
|
|
4768
|
+
`}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_git_ignore_utils();init_start_tickets_repo();init_credential_store();var TERMINAL_STATUSES=new Set(["staged","awaiting-organization-approval","connected","expired","invalid","revoked","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,path39,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path39}`,{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}}}var ALL_HANDOFF_STATUSES=new Set(["pending","staged","awaiting-organization-approval","connected","expired","revoked","verification-failed","no-repositories","conflict","failed"]);async function mintGithubHandoff(deps,repoName){let res=await postJson(deps,"/setup/github/cli/handoff",{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),installUrl=asString3(body?.install_url),expiresAt=asString3(body?.expires_at),ttlSeconds=body?.ttl_seconds;return!body||!installUrl||!expiresAt||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{installUrl,expiresAt,ttlSeconds}}}function parseHandoffSnapshot(raw){let rec=asRecord(raw);if(!rec)return null;let state=asString3(rec.state),status=asString3(rec.status);if(!state||!status||!ALL_HANDOFF_STATUSES.has(status))return null;let candidates=parseCandidates(rec.candidates??[]);return candidates===null?null:{state,status,createdAt:asNullableString(rec.created_at),expiresAt:asNullableString(rec.expires_at),revokedAt:asNullableString(rec.revoked_at),candidates,githubRepoName:asNullableString(rec.github_repo_name)}}async function listGithubHandoffs(deps,repoName){let res=await postJson(deps,"/setup/github/cli/handoffs",{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),rawHandoffs=body?.handoffs;if(!body||!Array.isArray(rawHandoffs))return{ok:!1,kind:"malformed"};let out=[];for(let raw of rawHandoffs){let parsed=parseHandoffSnapshot(raw);if(!parsed)return{ok:!1,kind:"malformed"};out.push(parsed)}return{ok:!0,value:out}}async function revokeGithubHandoff(deps,repoName,state){let res=await postJson(deps,"/setup/github/cli/revoke",{repo_name:repoName,state});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),status=asString3(body?.status);return!body||status!=="revoked"&&status!=="already-revoked"?{ok:!1,kind:"malformed"}:{ok:!0,value:status}}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 path27 from"path";import readline2 from"readline";init_bridge_config();init_start_tickets_repo();init_credential_store();var USAGE2=`Usage: connect-github [--repo <repo_name>] [--handoff | --resume | --revoke]
|
|
4769
4769
|
|
|
4770
4770
|
Connect a GitHub repository to a Bridge project from your terminal.
|
|
4771
4771
|
|
|
@@ -4773,14 +4773,26 @@ Opens the GitHub App install page in your browser, waits for you to install it,
|
|
|
4773
4773
|
then asks which repository to connect. You are never asked for a GitHub token or
|
|
4774
4774
|
password \u2014 you authenticate to GitHub in the browser.
|
|
4775
4775
|
|
|
4776
|
+
If you do not administer the GitHub organization yourself, use the delegated
|
|
4777
|
+
handoff: mint a link with --handoff, send it to whoever does, and finish with
|
|
4778
|
+
--resume once they have completed their part.
|
|
4779
|
+
|
|
4776
4780
|
Options:
|
|
4777
4781
|
--repo <repo_name> Bridge project to connect (inferred from this directory
|
|
4778
4782
|
when omitted; you will be asked to confirm).
|
|
4779
|
-
--
|
|
4783
|
+
--handoff Print a shareable install link (valid 72 hours) instead of
|
|
4784
|
+
connecting here. Does not open a browser or wait.
|
|
4785
|
+
--resume Look up an outstanding handoff and finish connecting it.
|
|
4786
|
+
Works from any machine holding this project's API key.
|
|
4787
|
+
--revoke Invalidate an outstanding handoff link.
|
|
4788
|
+
--help Show this message.
|
|
4789
|
+
|
|
4790
|
+
--handoff, --resume, and --revoke are mutually exclusive. Minting a new link does
|
|
4791
|
+
not invalidate an existing one \u2014 use --revoke for that.`,DELEGATED_MODE_FLAGS=["--handoff","--resume","--revoke"];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==="--handoff"){out.handoff=!0;continue}if(arg==="--resume"){out.resume=!0;continue}if(arg==="--revoke"){out.revoke=!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}`}}let selected=DELEGATED_MODE_FLAGS.filter(flag=>flag==="--handoff"&&out.handoff||flag==="--resume"&&out.resume||flag==="--revoke"&&out.revoke);return selected.length>1?{ok:!1,error:`Choose only one of ${DELEGATED_MODE_FLAGS.join(", ")} (got ${selected.join(", ")}).`}:{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}
|
|
4780
4792
|
`),stderr:message=>process.stderr.write(`${message}
|
|
4781
|
-
`)}}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(path27.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1, 2, or 3","(one blank or invalid answer re-prompts once, then exits with guidance). Option 1","is the existing-key flow below, option 2 is the bootstrap-invite flow, and option 3","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). That question is asked ONLY for a bare interactive run: passing","ANY flag, setting BAPI_API_KEY, or running without an interactive terminal keeps","the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or chooser option 2 or 3 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 chooser option 3 on a"," bare run 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.",""," RESUMABLE: a self-serve run that fails mid-protocol saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running the self-serve flow RESUMES that attempt \u2014"," it does not sign up again \u2014 so a retry never creates a second"," workspace. Never copy, display, or hand-remove that record; if"," the saved invite has genuinely expired the CLI asks before"," discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --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 is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -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(`
|
|
4793
|
+
`)}}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(path27.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}}function resolveHandoffRepoName(args,_deps){if(!args.repo)return{ok:!1,error:"connect-github --handoff needs an explicit project: pass --repo <repo_name>. It cannot ask you to confirm an inferred name when run non-interactively."};let validated=validateRepoName(args.repo);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 chooseCandidate(deps,candidates){if(candidates.length===1){let only=candidates[0],answer2=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();return answer2!=="y"&&answer2!=="yes"?(deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),null):only}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."),null;let selected=candidates[index-1];return deps.stderr(`Selected ${candidateLabel(selected)}.`),selected}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=await chooseCandidate(deps,candidates);if(!selected)return 1;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 runGithubHandoffMintFlow(deps,api,repoName){let minted=await mintGithubHandoff(api,repoName);return minted.ok?(deps.stdout(minted.value.installUrl),deps.stderr(""),deps.stderr("Send this link to whoever administers your GitHub organization."),deps.stderr(`It works once and expires ${formatExpiry(minted.value.expiresAt)}.`),deps.stderr("They'll finish in their browser \u2014 ask them to ping you, then run `connect-github --resume`."),0):reportNoHandoff(deps,FAILURE_MESSAGES[minted.kind])}function formatExpiry(iso){let parsed=Date.parse(iso);return Number.isNaN(parsed)?"in 72 hours":`on ${new Date(parsed).toISOString().replace("T"," ").slice(0,16)} UTC`}function reportNoHandoff(deps,detail){return deps.stderr(""),deps.stderr("No handoff link was created."),deps.stderr(detail),1}var REVOCABLE_STATUSES=new Set(["pending","awaiting-organization-approval","staged"]),HANDOFF_STATUS_MESSAGES={pending:"Nobody has completed the link yet. Ask them to open it, then run connect-github --resume again.",expired:"The link expired before it was used. Run connect-github --handoff to create a new one.",revoked:"This link was revoked. Run connect-github --handoff to create a new one.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github --handoff to create a new link.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Create a new link with connect-github --handoff and ask them to 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 install did not complete. Run connect-github --handoff to create a new link."};function handoffLabel(h){let created=h.createdAt?` \xB7 created ${h.createdAt.slice(0,16).replace("T"," ")}`:"";return`${h.status}${created}`}async function selectDelegatedHandoff(deps,handoffs){if(handoffs.length===0)return null;if(handoffs.length===1)return handoffs[0];deps.stderr(""),deps.stderr("Outstanding handoff links:"),handoffs.forEach((h,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${handoffLabel(h)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a handoff [1-${handoffs.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>handoffs.length?null:handoffs[index-1]}async function runGithubHandoffResumeFlow(deps,api,repoName){let listed=await listGithubHandoffs(api,repoName);if(!listed.ok)return reportNoConnection(deps,FAILURE_MESSAGES[listed.kind]);if(listed.value.length===0)return reportNoConnection(deps,"No handoff links exist for this project. Run connect-github --handoff to create one.");let selected=await selectDelegatedHandoff(deps,listed.value);if(!selected)return reportNoConnection(deps,"No handoff was selected.");if(selected.status==="connected")return deps.stdout(`Connected ${selected.githubRepoName??repoName}.`),0;if(selected.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("The install was sent to a GitHub organization owner for approval, and that approval has not happened yet."),deps.stderr("GitHub does not notify Bridge when it lands, so re-run connect-github --resume once the owner has approved it."),selected.expiresAt&&deps.stderr(`The link stays usable until ${formatExpiry(selected.expiresAt)}.`),0;if(selected.status!=="staged"){let message=HANDOFF_STATUS_MESSAGES[selected.status]??HANDOFF_STATUS_MESSAGES.failed;return selected.status==="pending"||selected.status==="expired"?(deps.stderr(""),deps.stderr(message),deps.stderr("If the GitHub App is already installed on that organization, GitHub may not send Bridge a callback at all. In that case follow the manual installation-ID steps in the GitHub App setup guide (docs/install/github-app.md)."),selected.status==="pending"?0:1):reportNoConnection(deps,message)}let candidates=selected.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);let chosen=await chooseCandidate(deps,candidates);if(!chosen)return 1;let confirmed=await confirmGithubConnection(api,repoName,selected.state,chosen.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 runGithubHandoffRevokeFlow(deps,api,repoName){let listed=await listGithubHandoffs(api,repoName);if(!listed.ok)return reportNoRevocation(deps,FAILURE_MESSAGES[listed.kind]);let revocable=listed.value.filter(h=>REVOCABLE_STATUSES.has(h.status));if(revocable.length===0)return reportNoRevocation(deps,"There are no handoff links left to revoke.");let selected=await selectDelegatedHandoff(deps,revocable);if(!selected)return reportNoRevocation(deps,"No handoff was selected.");let revoked=await revokeGithubHandoff(api,repoName,selected.state);return revoked.ok?(deps.stdout(revoked.value==="already-revoked"?"That handoff link was already revoked.":"Handoff link revoked. It can no longer be used."),0):reportNoRevocation(deps,FAILURE_MESSAGES[revoked.kind])}function reportNoRevocation(deps,detail){return deps.stderr(""),deps.stderr("Nothing was revoked."),deps.stderr(detail),1}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;let mintOnly=parsed.value.handoff;if(!deps.isTTY&&!mintOnly)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=mintOnly?resolveHandoffRepoName(parsed.value,deps):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 parsed.value.handoff?await runGithubHandoffMintFlow(deps,api,repo.value):parsed.value.resume?await runGithubHandoffResumeFlow(deps,api,repo.value):parsed.value.revoke?await runGithubHandoffRevokeFlow(deps,api,repo.value):await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1, 2, or 3","(one blank or invalid answer re-prompts once, then exits with guidance). Option 1","is the bootstrap-invite flow, option 2 prompts for an email and creates a","brand-new Bridge workspace for you (the self-serve flow), and option 3 is the","existing-project API-key flow below. The project-CREATING routes lead","deliberately. 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.","","Entered the wrong kind of credential? Both directions are detected by SHAPE","alone \u2014 no probe request is sent \u2014 and neither switches routes in silence:"," - a bootstrap invite (bapi_inv_\u2026) entered as an API key is announced, then"," confirmed on a terminal before it is redeemed; a non-interactive run keeps"," today's automatic switch but prints the notice."," - an API-key-shaped value entered as an invite is announced and may switch in"," place only after you confirm on a terminal \u2014 non-interactive runs exit with"," guidance instead, so a script is never silently redirected. The check runs"," BEFORE any project-name prompt, credential write, or network call.","A value that is neither shape is left on the route you chose.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is announced and instead redeemed to"," CREATE a new project and its first admin key \u2014 the same as"," --invite \u2014 so it skips repository lookup entirely (--email"," and --invite remain the explicit, preferred entry points for"," a new project). NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically. If the"," key does not identify exactly one project, the run STOPS \u2014"," it never guesses \u2014 and asks you to re-run with --repo <name>,",` or to rotate the key at ${setupUrl} (Security page).`," If instead the server is older or resolution fails"," transiently, you are asked to type the registered name, with"," NO pre-filled default (and"," --repo is required when stdin is non-interactive). In the"," existing-project 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 chooser"," option 1 or 2 above) it instead NAMES the project this run"," creates, so you are asked to name a new project \u2014 with a"," suggested default \u2014 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 chooser option 2 on a"," bare run 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.",""," RESUMABLE: a self-serve run that fails mid-protocol saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running the self-serve flow RESUMES that attempt \u2014"," it does not sign up again \u2014 so a retry never creates a second"," workspace. Never copy, display, or hand-remove that record; if"," the saved invite has genuinely expired the CLI asks before"," discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --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 is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -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(`
|
|
4782
4794
|
`)}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(`
|
|
4783
|
-
`),resolve2(answer.trim())}),muted=!0})}var INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND="npx -y @bridge_gpt/mcp-server connect-github",INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect GitHub."," This installs the Bridge GitHub App so pull requests and code review work. It opens"," github.com in your browser; no GitHub credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`],INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT="Connect GitHub? [y/N]: ";async function offerGithubConnection(repoName,baseUrl,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,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}for(let line of INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")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,lookupSelfServeBootstrapPending:lookupSelfServeBootstrapPendingCredential,discardBootstrapPending:discardBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m),debugLog:m=>{process.env.BAPI_INSTALL_DEBUG&&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(),source:"flag"};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim(),source:"env"};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered,source:"prompt"}:{ok:!1,error:`No Bridge API key or invite entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}return{ok:!1,error:`A Bridge API key or invite is required (no interactive terminal is available to prompt for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}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="How would you like to connect to Bridge API?",INSTALL_BRIDGE_ONBOARDING_CHOICES=["1. I have a Bridge API key","2. I have an invite token","3. I'm new \u2014 set me up with just my email"],INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT="Enter 1, 2, or 3: ",INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT="Enter 1, 2, or 3.",INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE="Re-run install-bridge and choose an option: pass --api-key <key> if you have a Bridge API key (or set BAPI_API_KEY), --invite if you were sent an invite token, or --email <addr> to sign up with just an email \u2014 on a bare interactive run, re-run and choose option 3 to sign up with just an email.";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{deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);for(let choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)deps.log(choice);for(let attempt=0;attempt<2;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();if(answer==="1")return{ok:!0,branch:{kind:"have-key"}};if(answer==="2")return{ok:!0,branch:{kind:"need-key",method:"bootstrap-invite"}};if(answer==="3")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};attempt===0&&deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT)}return{ok:!1,error:`No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}catch{return{ok:!1,error:`Could not read your answer from the terminal. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}}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(path28.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
|
|
4795
|
+
`),resolve2(answer.trim())}),muted=!0})}var INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND="npx -y @bridge_gpt/mcp-server connect-github",INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect GitHub."," This installs the Bridge GitHub App so pull requests and code review work. It opens"," github.com in your browser; no GitHub credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`],INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT="Connect GitHub? [y/N]: ";async function offerGithubConnection(repoName,baseUrl,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,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}for(let line of INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")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,lookupSelfServeBootstrapPending:lookupSelfServeBootstrapPendingCredential,discardBootstrapPending:discardBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m),debugLog:m=>{process.env.BAPI_INSTALL_DEBUG&&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(),source:"flag"};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim(),source:"env"};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered,source:"prompt"}:{ok:!1,error:`No Bridge API key or invite entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}return{ok:!1,error:`A Bridge API key or invite is required (no interactive terminal is available to prompt for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}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="How would you like to connect to Bridge API?",INSTALL_BRIDGE_ONBOARDING_CHOICES=["1. I have an invite token","2. I'm new \u2014 set me up with just my email","3. I have an API key for an existing project"],INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT="Enter 1, 2, or 3: ",INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT="Enter 1, 2, or 3.",INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE="Re-run install-bridge and choose an option: pass --api-key <key> if you have a Bridge API key (or set BAPI_API_KEY), --invite if you were sent an invite token, or --email <addr> to sign up with just an email \u2014 on a bare interactive run, re-run and choose option 2 to sign up with just an email.";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{deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);for(let choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)deps.log(choice);for(let attempt=0;attempt<2;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();if(answer==="1")return{ok:!0,branch:{kind:"need-key",method:"bootstrap-invite"}};if(answer==="2")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};if(answer==="3")return{ok:!0,branch:{kind:"have-key"}};attempt===0&&deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT)}return{ok:!1,error:`No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}catch{return{ok:!1,error:`Could not read your answer from the terminal. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}}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 prompt for it). It must match the server-side repository registration exactly."};if(mode!=="new-project"){let answer=(await deps.promptLine("Repo name (must match server-side registration): ")).trim();return answer.length>0?{ok:!0,value:answer}:{ok:!1,error:"No repo name provided."}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path28.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let answer=(await deps.promptLine(`Name your new Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let answer=(await deps.promptLine("Name your new Bridge project: ")).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
|
|
4784
4796
|
${promptText}
|
|
4785
4797
|
`),options.forEach((opt,idx)=>{output.write(` ${idx+1}. ${opt.label}
|
|
4786
4798
|
`)}),output.write(`Enter the number(s) of the tools you use (e.g. 1,3), then Enter.
|
|
@@ -4790,9 +4802,9 @@ ${promptText}
|
|
|
4790
4802
|
`),prompt();return}indices.push(n)}let chosen=new Set(indices);finish(options.filter((_,idx)=>chosen.has(idx+1)).map(o=>o.id))})};prompt()})}async function resolveSelectedHostPlatforms(deps,options){if(options.tools!==void 0)return options.tools;let ctx=await buildDetectionContext(deps),detected=new Set(detectDefaultPlatforms(ctx));if(deps.isTTY&&deps.promptMultiSelect){let optionList=allHostTargets().map(t=>({id:t.id,label:t.label})),chosen=await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT,optionList);return HOST_PLATFORM_ORDER.filter(id=>chosen.includes(id))}let legacy=["claude-code"];return detected.has("cursor")&&legacy.push("cursor"),detected.has("copilot-vscode")&&legacy.push("copilot-vscode"),HOST_PLATFORM_ORDER.filter(id=>legacy.includes(id))}var SELECTION_NON_LAUNCHABLE_AGENTS=new Set(["cursor-agent"]);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&&!SELECTION_NON_LAUNCHABLE_AGENTS.has(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}}var UNKNOWN_LAUNCH_TOOL_LABEL="your AI coding tool";function toolLabelForLaunchAgent(agent){return allHostTargets().find(t=>t.launchAgent===agent)?.label??UNKNOWN_LAUNCH_TOOL_LABEL}async function chooseInstallBridgeLaunchAgent(agents,deps){if(!deps.isTTY||!deps.promptLine)return null;let promptLine=deps.promptLine;try{deps.log(""),deps.log("More than one selected tool can host the configuration session:"),agents.forEach((agent,i)=>{deps.log(` ${String(i+1).padStart(2," ")}. ${toolLabelForLaunchAgent(agent)}`)});let answer=(await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>agents.length?null:agents[index-1]}catch{return null}}function buildManualInstallBridgeContinuation(kind,toolLabels){return kind==="empty-selection"?["No AI coding tools were configured, so nothing was set up for this project.","Re-run install-bridge and select at least one tool to configure it."].join(`
|
|
4791
4803
|
`):[`To finish configuring this project, open it in ${formatToolLabelPhrase(toolLabels)} that has the`,"Bridge MCP server configured, start a new session, and run /install-bridge.","Until the project is configured, your Bridge MCP tools stay limited."].join(`
|
|
4792
4804
|
`)}function formatToolLabelPhrase(labels){return labels.length===0?"an AI coding tool":labels.length===1?labels[0]:labels.length===2?`${labels[0]} and ${labels[1]}`:`${labels.slice(0,-1).join(", ")}, and ${labels[labels.length-1]}`}var INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX="Bridge can configure and set up this project for you automatically. Open a ";async function requestInstallBridgeLaunchConsent(toolLabel,deps){if(!deps.isTTY||!deps.promptLine)return"no-spawn";try{let answer=(await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `)).trim().toLowerCase();return answer==="n"||answer==="no"?"no-spawn":"spawn"}catch{return"no-spawn"}}async function buildDetectionContext(deps){let cwd=deps.cwd,homedir=deps.homedir(),posixJoin=(base,rel)=>`${base.endsWith("/")?base.slice(0,-1):base}/${rel}`,candidates=[posixJoin(cwd,".cursor"),posixJoin(cwd,".vscode"),posixJoin(cwd,".windsurf"),posixJoin(cwd,".windsurfrules"),posixJoin(homedir,".codex")],present=new Set;return await Promise.all(candidates.map(async p=>{try{await deps.stat(p),present.add(p)}catch{}})),{cwd,homedir,env:deps.env,exists:p=>present.has(p)}}function hostConfigTargetsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id]).filter(t=>t.scope==="project"&&t.format==="json").map(t=>({relPath:t.relPath,topLevelKey:t.topLevelKey}))}function labelsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id].label)}function isPlaceholderApiKey(value){if(typeof value!="string")return!0;let trimmed=value.trim();return trimmed.length===0?!0:trimmed==="YOUR_API_KEY"||trimmed.startsWith("YOUR_")}function buildInstallBridgeServerEntry(cwd,repoName,apiKey,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir,BAPI_API_KEY:apiKey};return{command:entry.command,args:entry.args,env}}function buildInstallBridgeSecretFreeServerEntry(cwd,repoName,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir};return delete env.BAPI_API_KEY,{command:entry.command,args:entry.args,env}}function formatCredentialStoreGuidance(repoName,credentialStorePath){return[" The bridge-api server resolves your key at runtime from the BAPI_API_KEY environment",` variable or the user-scoped credential store (${credentialStorePath}, target`,` bapi:${repoName}), so it stays out of this file entirely.`]}function formatSecretFreeManualMerge(relPath,topLevelKey,secretFreeEntry){let snippet=JSON.stringify({[topLevelKey]:{"bridge-api":secretFreeEntry}},null,2);return[` To configure ${relPath} by hand, MERGE this secret-free entry into the existing`," file (do not replace the file):",snippet]}function formatInvalidConfigNotice(relPath){return` ${relPath} could not be parsed safely \u2014 it was left untouched.`}async function readHostConfig(deps,fullPath){let raw;try{raw=await deps.readFile(fullPath)}catch(err){return err?.code==="ENOENT"?{state:"absent"}:{state:"invalid"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{state:"invalid"}}return parsed===null||typeof parsed!="object"||Array.isArray(parsed)?{state:"invalid"}:{state:"parsed",config:parsed}}function asRecord2(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}async function detectExistingRealKey(deps,targets){for(let target of targets){let result=await readHostConfig(deps,path28.join(deps.cwd,target.relPath));if(result.state==="invalid")return!0;if(result.state==="absent")continue;let topLevel=asRecord2(result.config[target.topLevelKey]),entry=asRecord2(topLevel?.["bridge-api"]),env=asRecord2(entry?.env);if(env&&!isPlaceholderApiKey(env.BAPI_API_KEY))return!0}return!1}function trackedConfigWarning(relPath){return`${relPath} is tracked by git \u2014 writing your API key here would commit it on your next push`}async function isTrackedProjectConfig(deps,relPath){try{let result=await deps.startTicketsDeps.runCommand("git",["ls-files","--error-unmatch","--",relPath],{cwd:deps.cwd});return result.exitCode===0?!0:(deps.debugLog(`install-bridge: git-tracked probe for ${relPath} \u2192 not tracked (exit ${result.exitCode})`),!1)}catch{return deps.debugLog(`install-bridge: git-tracked probe for ${relPath} \u2192 not tracked (probe failed)`),!1}}async function requestTrackedConfigConsent(deps,relPath){if(deps.errorLog(trackedConfigWarning(relPath)),!deps.promptLine)return!1;let answer;try{answer=await deps.promptLine(`Write your API key into ${relPath} anyway? (y/N) `)}catch{return!1}let normalized=(answer??"").trim().toLowerCase();return normalized==="y"||normalized==="yes"}async function writeHostConfigs(deps,targets,entries,trackedState,ctx){let written=[],skipped=[];for(let target of targets){let fullPath=path28.join(deps.cwd,target.relPath),read=await readHostConfig(deps,fullPath);if(read.state==="invalid"){deps.errorLog(formatInvalidConfigNotice(target.relPath));for(let line of formatSecretFreeManualMerge(target.relPath,target.topLevelKey,entries.secretFree))deps.errorLog(line);skipped.push({relPath:target.relPath,reason:"invalid"});continue}let tracked=trackedState.get(target.relPath)??!1,entry=entries.real,mode="real-key";if(tracked){let interactive=deps.isTTY&&!!deps.promptLine;if(interactive?await requestTrackedConfigConsent(deps,target.relPath):!1)entry=entries.real,mode="real-key";else{entry=entries.secretFree,mode="secret-free",interactive||deps.errorLog(trackedConfigWarning(target.relPath));for(let line of formatCredentialStoreGuidance(ctx.repoName,ctx.credentialStorePath))deps.errorLog(line);deps.errorLog(` To stop tracking it: git rm --cached -- ${target.relPath}`)}}let config=read.state==="parsed"?read.config:{},topLevel=asRecord2(config[target.topLevelKey])??{};topLevel["bridge-api"]=entry,config[target.topLevelKey]=topLevel,await deps.mkdir(path28.dirname(fullPath),{recursive:!0}),await deps.writeFile(fullPath,JSON.stringify(config,null,2)+`
|
|
4793
|
-
`,{encoding:"utf-8"}),written.push({relPath:target.relPath,mode})}return{written,skipped}}async function provisionSelectedGlobalTargets(deps,platforms,entry,needKey){let logLines=[],anyManualRequired=!1,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":anyManualRequired=!0,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 needKey&&anyManualRequired&&logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),logLines}function buildPingUrl(baseUrl,repoName){let url=new URL(`${baseUrl.replace(/\/+$/,"")}/jira/ping`);return url.searchParams.set("repo_name",repoName),url.toString()}var CONNECTIVITY_ACCESS_DENIED_FALLBACK="The Bridge API denied access to this repository (HTTP 403). Verify the repo_name and that this credential is authorized for it.",CONNECTIVITY_KEY_SOURCE_ATTRIBUTION={env:" (this key came from the BAPI_API_KEY environment variable \u2014 unset it to be prompted for a different one)",flag:" (this key came from the --api-key flag \u2014 omit it to be prompted for a different one)",prompt:""};async function verifyConnectivity(deps,baseUrl,repoName,apiKey,apiKeySource){let url=buildPingUrl(baseUrl,repoName),resp;try{resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{ok:!1,message:`Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`}}if(resp.ok)return{ok:!0};if(resp.status===401){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return{ok:!1,message:`The Bridge API rejected the credential (HTTP ${resp.status}). The API key may be invalid or expired \u2014 generate a fresh one at ${setupUrl} (Security page). (An expired token can also surface as a permission error.)`+(apiKeySource?CONNECTIVITY_KEY_SOURCE_ATTRIBUTION[apiKeySource]:"")}}if(resp.status===403){let detail;try{detail=(await resp.json())?.detail}catch{return{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return typeof detail=="string"&&detail.trim().length>0?{ok:!1,message:detail.trim()}:{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return resp.status===404?{ok:!1,message:`The Bridge API could not find repo '${repoName}' (HTTP 404). Confirm --repo matches the server-side repository registration exactly.`}:{ok:!1,message:`Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`}}function buildResolveRepoUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/resolve-repo`}async function resolveRepoViaServer(fetchImpl,baseUrl,apiKey){let url=buildResolveRepoUrl(baseUrl),resp;try{resp=await fetchImpl(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{status:"error"}}if(resp.status===404)return{status:"not-deployed"};if(resp.status===409)return{status:"unresolved"};if(!resp.ok)return{status:"error"};let body;try{body=await resp.json()}catch{return{status:"error"}}let repoName=body?.repo_name,validated=validateRepoName(repoName);return validated.ok?{status:"resolved",repoName:validated.value}:{status:"error"}}var BOOTSTRAP_KEY_SECRET_BYTES=32;function generateBootstrapKeySecret(randomBytes3){return randomBytes3(BOOTSTRAP_KEY_SECRET_BYTES).toString("base64url")}function fingerprintBootstrapInvite(token){return createHash3("sha256").update(token,"utf-8").digest("hex")}function buildBootstrapExchangeUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap`}async function exchangeBootstrapInvite(deps,baseUrl,token,repoName,keySecret){let url=buildBootstrapExchangeUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,repo_name:repoName,key_secret:keySecret}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,kind:"failed",message:`Could not reach the Bridge API at ${baseUrl} to redeem the bootstrap invite. Check BAPI_BASE_URL and your network. The invite has not been used.`}}if(resp.ok){let repo;try{repo=(await resp.json())?.repo_name}catch{return{ok:!1,kind:"failed",message:"The Bridge API returned an unreadable response to the bootstrap exchange."}}let validated=validateRepoName(repo);return validated.ok?{ok:!0,repoName:validated.value}:{ok:!1,kind:"failed",message:"The Bridge API returned an unexpected repo name for the bootstrap exchange."}}return resp.status===409?{ok:!1,kind:"repo-name-taken",message:`The repo name '${repoName}' is already taken (HTTP 409). Repo names are globally unique.`}:resp.status===401?{ok:!1,kind:"invalid-invite",message:`The Bridge API rejected the bootstrap invite (HTTP ${resp.status}).`}:{ok:!1,kind:"failed",message:`The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`}}var BOOTSTRAP_INVITE_TOKEN_PREFIX="bapi_inv_";function classifyEnteredCredential(value){return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?"invite":"api-key"}function buildSelfServeMintUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap/self-serve`}async function mintSelfServeInvite(deps,baseUrl,email){let url=buildSelfServeMintUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({invitee_email:email}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"failed"}}if(resp.ok){let token;try{token=(await resp.json())?.token}catch{return{ok:!1,category:"failed"}}return typeof token!="string"||token.trim().length===0||!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?{ok:!1,category:"failed"}:{ok:!0,token}}return resp.status===429?{ok:!1,category:"rate-limited"}:resp.status===400||resp.status===422?{ok:!1,category:"invalid"}:{ok:!1,category:"failed"}}var BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE=["The Bridge API rejected the bootstrap invite (HTTP 401).","","Either the invite is invalid, expired, or revoked \u2014 or it was ALREADY redeemed from this","machine and the local secret has since been lost (e.g. ~/.config/bridge was deleted).","","If it was already redeemed, you cannot recover it yourself:"," \u2022 Re-running will NOT work: each run without the original local secret sends a new one,"," which cannot match what the server stored, so it will keep returning 401."," \u2022 A new bootstrap invite will NOT work either: your repo name is globally unique and is"," now taken by the project you already created, so it cannot be redeemed again.","","Ask your Bridge API operator to recover it for you: they revoke the orphaned key","(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project","(POST /setup/keys), then send you that key. Run install-bridge with --api-key <that key>."].join(`
|
|
4794
|
-
`),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.)",BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE="The Bridge API rejected the stored signup invite (HTTP 401) \u2014 it is invalid, expired, or revoked. Your saved signup attempt cannot be resumed with it.";function buildBootstrapRetryAdvice(selfServeSignupMode){return selfServeSignupMode?"Re-run install-bridge and choose the email option \u2014 your previous attempt will resume.":"Re-run install-bridge with the same bootstrap invite \u2014 the redemption will replay and return the same key."}async function resolveCredentialConflictConsent(prepared,ctx){if(prepared.ok||prepared.kind!=="credential-conflict")return prepared;if(!ctx.isTTY||!ctx.promptLine)return ctx.errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),null;let answer=(await ctx.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();return answer!=="y"&&answer!=="yes"?(ctx.errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),null):(ctx.grantConsent(),ctx.retry())}function buildDryRunPreview(plan){return plan.bootstrapInvite?buildBootstrapDryRunPreview(plan):[plan.
|
|
4795
|
-
`)}var INSTALL_BRIDGE_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",INSTALL_BRIDGE_DOCTOR_POINTER=`Diagnose with: ${INSTALL_BRIDGE_DOCTOR_COMMAND}`,INSTALL_BRIDGE_UNCLASSIFIED_CAUSE="unexpected error (run with BAPI_INSTALL_DEBUG=1 for details)",INSTALL_BRIDGE_DISPATCH_STEP_LABEL="install-bridge dispatch",INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL="debug (BAPI_INSTALL_DEBUG) raw message:",INSTALL_BRIDGE_DEBUG_STACK_LABEL="debug (BAPI_INSTALL_DEBUG) raw stack:",INSTALL_BRIDGE_FILESYSTEM_CAUSES=new Map([["EACCES","permission denied"],["EPERM","permission denied"],["ENOSPC","no space left on device"],["EROFS","read-only file system"]]),INSTALL_BRIDGE_NETWORK_CODES=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN"]);function extractInstallBridgeErrorCode(error){try{if(typeof error!="object"||error===null)return;let code=error.code;return typeof code=="string"?code:void 0}catch{return}}function classifyInstallBridgeFailure(error){let code=extractInstallBridgeErrorCode(error);if(code===void 0)return INSTALL_BRIDGE_UNCLASSIFIED_CAUSE;let filesystemCause=INSTALL_BRIDGE_FILESYSTEM_CAUSES.get(code);return filesystemCause!==void 0?filesystemCause:INSTALL_BRIDGE_NETWORK_CODES.has(code)?"network error":INSTALL_BRIDGE_UNCLASSIFIED_CAUSE}function readInstallBridgeDebugValue(read){try{let value=read();return typeof value=="string"?value:value==null?void 0:String(value)}catch{return}}function buildInstallBridgeFailureLines(error,context){let lines=[`Error: install-bridge failed at: ${context.step}`,` cause: ${classifyInstallBridgeFailure(error)}`,` ${INSTALL_BRIDGE_DOCTOR_POINTER}`];if(context.resumeAdvice&&lines.push(` ${context.resumeAdvice}`),context.debugEnabled){let isError=error instanceof Error,rawMessage=readInstallBridgeDebugValue(isError?()=>error.message:()=>String(error)),rawStack=isError?readInstallBridgeDebugValue(()=>error.stack):void 0;lines.push(` ${INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL} ${rawMessage??"<unavailable>"}`),rawStack&&lines.push(` ${INSTALL_BRIDGE_DEBUG_STACK_LABEL} ${rawStack}`)}return lines}function buildInstallBridgeBaseUrlError(value){return`BAPI_BASE_URL must be an absolute http(s) URL (got: "${value}")`}function resolveInstallBridgeBaseUrl(env){let supplied=env.BAPI_BASE_URL,candidate=typeof supplied=="string"?supplied.trim():DEFAULT_BAPI_BASE_URL2;if(candidate.length===0)return{ok:!1,error:buildInstallBridgeBaseUrlError("")};let parsed;try{parsed=new URL(candidate)}catch{return{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}}return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}:{ok:!0,value:candidate}}var INSTALL_BRIDGE_STEP_LABELS={scaffold:"Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026",selfServeMint:"Step 2/5 \u2014 requesting Bridge self-serve setup\u2026",inviteRedeem:"Step 2/5 \u2014 redeeming the bootstrap invite\u2026",verifyConnectivity:"Step 2/5 \u2014 verifying connectivity\u2026",writeHostConfigs:"Step 3/5 \u2014 writing per-host MCP config\u2026",promoteCredential:"Step 4/5 \u2014 promoting the bootstrap credential\u2026",persistCredential:"Step 4/5 \u2014 persisting routing credential\u2026"};function buildInstallBridgeLaunchStepLabel(agent){return`Step 5/5 \u2014 opening a ${toolLabelForLaunchAgent(agent)} session for /install-bridge configuration + concise capability report\u2026`}var INSTALL_BRIDGE_CWD_BANNER_PREFIX="Installing Bridge into: ",INSTALL_BRIDGE_NO_GIT_WARNING="This doesn't look like a project root (no .git found)",INSTALL_BRIDGE_NO_GIT_PROMPT=`${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continue? [y/N]: `,INSTALL_BRIDGE_NO_GIT_ABORT="Aborted \u2014 run install-bridge from your project root, or re-run and confirm to continue.",INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING=`Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continuing (non-interactive).`;async function hasProjectRootMarker(deps){try{return await deps.stat(path28.join(deps.cwd,".git")),!0}catch{return!1}}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,reportFatal=(...lines)=>{for(let line of lines)errorLog(line);errorLog(` ${INSTALL_BRIDGE_DOCTOR_POINTER}`)},fatal=(...lines)=>(reportFatal(...lines),1),baseUrlResult=resolveInstallBridgeBaseUrl(deps.env),usageBaseUrl=baseUrlResult.ok?baseUrlResult.value:DEFAULT_BAPI_BASE_URL2,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(getInstallBridgeUsage(usageBaseUrl)),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage(usageBaseUrl)),1;let options=parsed.options;if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let baseUrl=baseUrlResult.value,setupUrl=buildInstallBridgeSetupUrl(baseUrl);if(deps.env={...deps.env,BAPI_BASE_URL:baseUrl},log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`),!await hasProjectRootMarker(deps))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);let 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="",apiKeySource;if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;inviteToken=inviteResult.value}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;classifyEnteredCredential(keyResult.value)==="invite"?(inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1,apiKeySource=void 0):(apiKey=keyResult.value,apiKeySource=keyResult.source)}let docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,attemptedServerResolution=!1;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{attemptedServerResolution=!0,log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(resolution.status==="resolved")repoName=resolution.repoName;else{log(`Couldn't auto-resolve your repo from the key; falling back to a guessed name \u2014 confirm it matches ${setupUrl} (setup UI).`);let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let plan={repoName,baseUrl,docsDir,launch:planLaunch,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),...bootstrapInviteMode?{bootstrapInvite:!0,exchangeUrl:buildBootstrapExchangeUrl(baseUrl)}:{},...selfServeSignupMode?{selfServeSignup:!0}:{},...attemptedServerResolution?{attemptedServerResolution:!0}:{}};if(options.dryRun){for(let line of buildDryRunPreview(plan))log(line);return 0}if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen,finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;let currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,bootstrapExchangeSucceeded=!1;try{currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,log(currentStep),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){let resumedSelfServeReplay=!1,keySecret="",reusedPendingSecret=!1;if(selfServeSignupMode){let lookup=await deps.lookupSelfServeBootstrapPending({repoName},credentialWriteDeps);if(!lookup.ok)return fatal(`Error: ${lookup.error}`);lookup.state==="resumable"&&(inviteToken=lookup.replayToken,inviteFingerprint=lookup.inviteFingerprint,keySecret=lookup.keySecret,resumedSelfServeReplay=!0,reusedPendingSecret=!0,log(` resuming your previous signup attempt for ${repoName}`))}let mintAndPrepareSelfServe=async()=>{let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?reportFatal("Error: Bridge's self-serve signup capacity for this hour is exhausted (this is a global service limit, not a problem with your machine). Try again in about an hour."):mint.category==="invalid"?reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again."):reportFatal("Error: Unable to complete self-serve setup. Check connectivity and retry."),!1;inviteToken=mint.token,inviteFingerprint=fingerprintBootstrapInvite(inviteToken);let prepareSelfServePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite,selfServeReplayToken:inviteToken},credentialWriteDeps),prep=await resolveCredentialConflictConsent(await prepareSelfServePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareSelfServePending(!0)});return prep===null?!1:prep.ok?(keySecret=prep.keySecret,reusedPendingSecret=prep.reused,log(` saved the pending credential for ${prep.target} (fsynced before the exchange)`),!0):(reportFatal(`Error: could not durably store the signup credential (${prep.kind}). ${prep.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1)};if(selfServeSignupMode&&!resumedSelfServeReplay&&(currentStep=INSTALL_BRIDGE_STEP_LABELS.selfServeMint,log(currentStep),!await mintAndPrepareSelfServe()))return 1;if(!selfServeSignupMode){inviteFingerprint=fingerprintBootstrapInvite(inviteToken),currentStep=INSTALL_BRIDGE_STEP_LABELS.inviteRedeem,log(currentStep);let prepareInvitePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite},credentialWriteDeps),prepared=await resolveCredentialConflictConsent(await prepareInvitePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareInvitePending(!0)});if(prepared===null)return 1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error}`),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;keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused,log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`)}let exchange,selfServeRefreshed=!1;for(;;){for(exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);!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)break;if(exchange.kind==="invalid-invite"&&resumedSelfServeReplay&&!selfServeRefreshed){if(!deps.isTTY||!deps.promptLine)return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE,`The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} has been left untouched. Re-run install-bridge on an interactive terminal to discard it and start a fresh signup.`);errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);let consented=!1;try{let answer=(await deps.promptLine(`Discard the saved signup at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} and start a fresh one? [y/N]: `)).trim().toLowerCase();consented=answer==="y"||answer==="yes"}catch{consented=!1}if(!consented)return errorLog("Aborted: the saved signup attempt was left unchanged (nothing was discarded)."),1;let discarded=await deps.discardBootstrapPending({repoName,inviteFingerprint},credentialWriteDeps);if(!discarded.ok)return fatal(`Error: could not discard the saved signup (${discarded.kind}). ${discarded.error}`);if(log("Requesting a fresh Bridge self-serve setup\u2026"),!await mintAndPrepareSelfServe())return 1;selfServeRefreshed=!0,resumedSelfServeReplay=!1;continue}return fatal(exchange.kind==="invalid-invite"?reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE:`Error: ${exchange.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`)}if(bootstrapExchangeSucceeded=!0,exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return fatal(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`);repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||(currentStep=INSTALL_BRIDGE_STEP_LABELS.verifyConnectivity,log(currentStep));let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey,bootstrapInviteMode?void 0:apiKeySource);if(!ping.ok)return fatal(bootstrapInviteMode?`Error: ${ping.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`:`Error: ${ping.message}`);log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir),secretFreeEntry=buildInstallBridgeSecretFreeServerEntry(deps.cwd,repoName,baseUrl,docsDir);currentStep=INSTALL_BRIDGE_STEP_LABELS.writeHostConfigs,log(currentStep);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 trackedState=new Map;for(let target of targets)trackedState.set(target.relPath,await isTrackedProjectConfig(deps,target.relPath));let writeResult=await writeHostConfigs(deps,targets,{real:entry,secretFree:secretFreeEntry},trackedState,{repoName,credentialStorePath});for(let{relPath,mode}of writeResult.written)log(mode==="secret-free"?` wrote ${relPath} (secret-free \u2014 key resolved at runtime)`:` wrote ${relPath}`);for(let{relPath}of writeResult.skipped)log(` skipped ${relPath} \u2014 existing config could not be parsed safely; left untouched`);let needKeyManual=bootstrapInviteMode?{repoName,credentialStorePath}:void 0,globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry,needKeyManual);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,needKeyManual);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(bootstrapExchangeSucceeded){currentStep=INSTALL_BRIDGE_STEP_LABELS.promoteCredential,log(currentStep);let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return fatal(`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. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);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,baseUrl,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"){currentStep=buildInstallBridgeLaunchStepLabel(finalAgentName),log(currentStep);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});if(!spawnResult.ok)return errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0;let handoffToolLabel=toolLabelForLaunchAgent(finalAgentName);return log(""),log(`install-bridge setup steps complete. A fresh ${handoffToolLabel} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} asks \u2014 configuration can't proceed until you do.`),log(`If that tab is closed or fails, open a session in ${handoffToolLabel} and run /install-bridge.`),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 at ${setupUrl} (Get Started page \u2014 install status panel) or via the session's 'Applied N of M' summary.`),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}catch(error){let resumeAdvice=bootstrapInviteMode&&bootstrapExchangeSucceeded?buildBootstrapRetryAdvice(selfServeSignupMode):void 0,lines=buildInstallBridgeFailureLines(error,{step:currentStep,debugEnabled:!!deps.env.BAPI_INSTALL_DEBUG,...resumeAdvice?{resumeAdvice}:{}});for(let line of lines)errorLog(line);return 1}}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path29 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(`
|
|
4805
|
+
`,{encoding:"utf-8"}),written.push({relPath:target.relPath,mode})}return{written,skipped}}async function provisionSelectedGlobalTargets(deps,platforms,entry,needKey){let logLines=[],anyManualRequired=!1,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":anyManualRequired=!0,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 needKey&&anyManualRequired&&logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),logLines}function buildPingUrl(baseUrl,repoName){let url=new URL(`${baseUrl.replace(/\/+$/,"")}/jira/ping`);return url.searchParams.set("repo_name",repoName),url.toString()}var CONNECTIVITY_ACCESS_DENIED_FALLBACK="The Bridge API denied access to this repository (HTTP 403). Verify the repo_name and that this credential is authorized for it.",CONNECTIVITY_KEY_SOURCE_ATTRIBUTION={env:" (this key came from the BAPI_API_KEY environment variable \u2014 unset it to be prompted for a different one)",flag:" (this key came from the --api-key flag \u2014 omit it to be prompted for a different one)",prompt:""};async function verifyConnectivity(deps,baseUrl,repoName,apiKey,apiKeySource){let url=buildPingUrl(baseUrl,repoName),resp;try{resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{ok:!1,message:`Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`}}if(resp.ok)return{ok:!0};if(resp.status===401){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return{ok:!1,message:`The Bridge API rejected the credential (HTTP ${resp.status}). The API key may be invalid or expired \u2014 generate a fresh one at ${setupUrl} (Security page). (An expired token can also surface as a permission error.)`+(apiKeySource?CONNECTIVITY_KEY_SOURCE_ATTRIBUTION[apiKeySource]:"")}}if(resp.status===403){let detail;try{detail=(await resp.json())?.detail}catch{return{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return typeof detail=="string"&&detail.trim().length>0?{ok:!1,message:detail.trim()}:{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return resp.status===404?{ok:!1,message:`The Bridge API could not find repo '${repoName}' (HTTP 404). Confirm --repo matches the server-side repository registration exactly.`}:{ok:!1,message:`Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`}}var INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR="Error: that API key does not identify exactly one Bridge project, so the repository could not be resolved from it. Re-run with --repo <name> (or set BAPI_REPO_NAME) to name the registered project explicitly.",INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE="Automatic repository resolution isn't available for this run. Enter the name your project is registered under (or re-run with --repo <name>).";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. The invite has not been used.`}}if(resp.ok){let repo;try{repo=(await resp.json())?.repo_name}catch{return{ok:!1,kind:"failed",message:"The Bridge API returned an unreadable response to the bootstrap exchange."}}let validated=validateRepoName(repo);return validated.ok?{ok:!0,repoName:validated.value}:{ok:!1,kind:"failed",message:"The Bridge API returned an unexpected repo name for the bootstrap exchange."}}return resp.status===409?{ok:!1,kind:"repo-name-taken",message:`The repo name '${repoName}' is already taken (HTTP 409). Repo names are globally unique.`}:resp.status===401?{ok:!1,kind:"invalid-invite",message:`The Bridge API rejected the bootstrap invite (HTTP ${resp.status}).`}:{ok:!1,kind:"failed",message:`The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`}}var BOOTSTRAP_INVITE_TOKEN_PREFIX="bapi_inv_";function classifyEnteredCredential(value){return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?"invite":"api-key"}var USER_ACCESS_KEY_ENCODED_LENGTH=43,USER_ACCESS_KEY_DECODED_BYTES=32,USER_ACCESS_KEY_SHAPE_PATTERN=new RegExp(`^[A-Za-z0-9_-]{${USER_ACCESS_KEY_ENCODED_LENGTH}}$`);function isHighEntropyApiKeyShape(value){if(typeof value!="string"||!USER_ACCESS_KEY_SHAPE_PATTERN.test(value))return!1;let decoded;try{decoded=Buffer.from(value,"base64url")}catch{return!1}return decoded.length!==USER_ACCESS_KEY_DECODED_BYTES?!1:decoded.toString("base64url")===value}var INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE="That looks like a bootstrap invite token, not a Bridge API key. An invite CREATES a new Bridge project and mints your first admin key, instead of connecting to a project that already exists.",INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT="Redeem it as an invite and create a new project? [y/N]: ",INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE="Redeeming it as a bootstrap invite (non-interactive \u2014 no confirmation is possible here).",INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED="Error: cancelled \u2014 nothing was created. Re-run install-bridge with an API key for an existing project (--api-key <key>, or BAPI_API_KEY), or re-run and confirm to redeem the invite and create a new project.",INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE="That looks like a Bridge API key, not a bootstrap invite token. An API key connects to a project that already exists, instead of creating a new one.",INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT="Use it to connect to your existing project instead? [y/N]: ",INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED="Error: cancelled \u2014 nothing was created and no invite was spent. Re-run install-bridge with --api-key <key> (or BAPI_API_KEY) to connect to your existing project, or supply a bootstrap invite token (bapi_inv_\u2026) to create a new one.";async function requestCredentialRouteSwitchConfirmation(deps,promptText){if(!deps.isTTY||!deps.promptLine)return!1;try{let answer=(await deps.promptLine(promptText)).trim().toLowerCase();return answer==="y"||answer==="yes"}catch{return!1}}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(`
|
|
4806
|
+
`),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.)",BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE="The Bridge API rejected the stored signup invite (HTTP 401) \u2014 it is invalid, expired, or revoked. Your saved signup attempt cannot be resumed with it.";function buildBootstrapRetryAdvice(selfServeSignupMode){return selfServeSignupMode?"Re-run install-bridge and choose the email option \u2014 your previous attempt will resume.":"Re-run install-bridge with the same bootstrap invite \u2014 the redemption will replay and return the same key."}async function resolveCredentialConflictConsent(prepared,ctx){if(prepared.ok||prepared.kind!=="credential-conflict")return prepared;if(!ctx.isTTY||!ctx.promptLine)return ctx.errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),null;let answer=(await ctx.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();return answer!=="y"&&answer!=="yes"?(ctx.errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),null):(ctx.grantConsent(),ctx.retry())}function buildDryRunPreview(plan){return plan.bootstrapInvite?buildBootstrapDryRunPreview(plan):[plan.serverRepoResolutionStatus!==void 0?"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.serverRepoResolutionStatus==="resolved"?" (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;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...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 '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/,"")}' before the agent session starts.`," It installs the Bridge GitHub App so pull requests and code review work, opens"," github.com in your browser, and shares no GitHub credential with Bridge; it",` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`];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, no email is sent or transmitted, and",` ${pendingTarget} is neither read nor written;`," a real run would first check that record and RESUME a saved attempt if one"," exists, otherwise request a fresh workspace for your email and durably store the"," returned invite token alongside the key_secret \u2014 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;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...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(path28.join(deps.cwd,".windsurf"))||await exists(path28.join(deps.cwd,".windsurfrules")),codex=await exists(path28.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 formatNeedKeyCredentialStoreLine(repoName,credentialStorePath){return`Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" \u2014 copy BAPI_API_KEY from there.`}function buildManualHostInstructions(entry,editors,needKey){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)."),needKey&&lines.push("",` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),lines.join(`
|
|
4807
|
+
`)}var INSTALL_BRIDGE_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",INSTALL_BRIDGE_DOCTOR_POINTER=`Diagnose with: ${INSTALL_BRIDGE_DOCTOR_COMMAND}`,INSTALL_BRIDGE_UNCLASSIFIED_CAUSE="unexpected error (run with BAPI_INSTALL_DEBUG=1 for details)",INSTALL_BRIDGE_DISPATCH_STEP_LABEL="install-bridge dispatch",INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL="debug (BAPI_INSTALL_DEBUG) raw message:",INSTALL_BRIDGE_DEBUG_STACK_LABEL="debug (BAPI_INSTALL_DEBUG) raw stack:",INSTALL_BRIDGE_FILESYSTEM_CAUSES=new Map([["EACCES","permission denied"],["EPERM","permission denied"],["ENOSPC","no space left on device"],["EROFS","read-only file system"]]),INSTALL_BRIDGE_NETWORK_CODES=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN"]);function extractInstallBridgeErrorCode(error){try{if(typeof error!="object"||error===null)return;let code=error.code;return typeof code=="string"?code:void 0}catch{return}}function classifyInstallBridgeFailure(error){let code=extractInstallBridgeErrorCode(error);if(code===void 0)return INSTALL_BRIDGE_UNCLASSIFIED_CAUSE;let filesystemCause=INSTALL_BRIDGE_FILESYSTEM_CAUSES.get(code);return filesystemCause!==void 0?filesystemCause:INSTALL_BRIDGE_NETWORK_CODES.has(code)?"network error":INSTALL_BRIDGE_UNCLASSIFIED_CAUSE}function readInstallBridgeDebugValue(read){try{let value=read();return typeof value=="string"?value:value==null?void 0:String(value)}catch{return}}function buildInstallBridgeFailureLines(error,context){let lines=[`Error: install-bridge failed at: ${context.step}`,` cause: ${classifyInstallBridgeFailure(error)}`,` ${INSTALL_BRIDGE_DOCTOR_POINTER}`];if(context.resumeAdvice&&lines.push(` ${context.resumeAdvice}`),context.debugEnabled){let isError=error instanceof Error,rawMessage=readInstallBridgeDebugValue(isError?()=>error.message:()=>String(error)),rawStack=isError?readInstallBridgeDebugValue(()=>error.stack):void 0;lines.push(` ${INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL} ${rawMessage??"<unavailable>"}`),rawStack&&lines.push(` ${INSTALL_BRIDGE_DEBUG_STACK_LABEL} ${rawStack}`)}return lines}function buildInstallBridgeBaseUrlError(value){return`BAPI_BASE_URL must be an absolute http(s) URL (got: "${value}")`}function resolveInstallBridgeBaseUrl(env){let supplied=env.BAPI_BASE_URL,candidate=typeof supplied=="string"?supplied.trim():DEFAULT_BAPI_BASE_URL2;if(candidate.length===0)return{ok:!1,error:buildInstallBridgeBaseUrlError("")};let parsed;try{parsed=new URL(candidate)}catch{return{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}}return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}:{ok:!0,value:candidate}}var INSTALL_BRIDGE_STEP_LABELS={scaffold:"Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026",selfServeMint:"Step 2/5 \u2014 requesting Bridge self-serve setup\u2026",inviteRedeem:"Step 2/5 \u2014 redeeming the bootstrap invite\u2026",verifyConnectivity:"Step 2/5 \u2014 verifying connectivity\u2026",writeHostConfigs:"Step 3/5 \u2014 writing per-host MCP config\u2026",promoteCredential:"Step 4/5 \u2014 promoting the bootstrap credential\u2026",persistCredential:"Step 4/5 \u2014 persisting routing credential\u2026"};function buildInstallBridgeLaunchStepLabel(agent){return`Step 5/5 \u2014 opening a ${toolLabelForLaunchAgent(agent)} session for /install-bridge configuration + concise capability report\u2026`}var INSTALL_BRIDGE_CWD_BANNER_PREFIX="Installing Bridge into: ",INSTALL_BRIDGE_NO_GIT_WARNING="This doesn't look like a project root (no .git found)",INSTALL_BRIDGE_NO_GIT_PROMPT=`${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continue? [y/N]: `,INSTALL_BRIDGE_NO_GIT_ABORT="Aborted \u2014 run install-bridge from your project root, or re-run and confirm to continue.",INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING=`Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continuing (non-interactive).`;async function hasProjectRootMarker(deps){try{return await deps.stat(path28.join(deps.cwd,".git")),!0}catch{return!1}}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,reportFatal=(...lines)=>{for(let line of lines)errorLog(line);errorLog(` ${INSTALL_BRIDGE_DOCTOR_POINTER}`)},fatal=(...lines)=>(reportFatal(...lines),1),baseUrlResult=resolveInstallBridgeBaseUrl(deps.env),usageBaseUrl=baseUrlResult.ok?baseUrlResult.value:DEFAULT_BAPI_BASE_URL2,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(getInstallBridgeUsage(usageBaseUrl)),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage(usageBaseUrl)),1;let options=parsed.options;if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let baseUrl=baseUrlResult.value,setupUrl=buildInstallBridgeSetupUrl(baseUrl);if(deps.env={...deps.env,BAPI_BASE_URL:baseUrl},log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`),!await hasProjectRootMarker(deps))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);let 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="",apiKeySource;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;if(inviteToken=inviteResult.value,classifyEnteredCredential(inviteToken)!=="invite"&&isHighEntropyApiKeyShape(inviteToken)){if(log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE),!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED),1;apiKey=inviteToken,inviteToken="",bootstrapInviteMode=!1,selfServeSignupMode=!1,apiKeySource=void 0}}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;if(classifyEnteredCredential(keyResult.value)==="invite"){if(log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE),deps.isTTY&&deps.promptLine){if(!await requestCredentialRouteSwitchConfirmation(deps,INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT))return errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED),1}else log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1,apiKeySource=void 0}else apiKey=keyResult.value,apiKeySource=keyResult.source}let docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,serverRepoResolutionStatus;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{log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(serverRepoResolutionStatus=resolution.status,resolution.status==="resolved")repoName=resolution.repoName;else{if(resolution.status==="unresolved")return fatal(INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR,` Or rotate the key at ${setupUrl} (Security page) if it should identify exactly one project.`);{log(INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE);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}:{},...serverRepoResolutionStatus!==void 0?{serverRepoResolutionStatus}:{}};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;let currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,bootstrapExchangeSucceeded=!1;try{currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,log(currentStep),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){let resumedSelfServeReplay=!1,keySecret="",reusedPendingSecret=!1;if(selfServeSignupMode){let lookup=await deps.lookupSelfServeBootstrapPending({repoName},credentialWriteDeps);if(!lookup.ok)return fatal(`Error: ${lookup.error}`);lookup.state==="resumable"&&(inviteToken=lookup.replayToken,inviteFingerprint=lookup.inviteFingerprint,keySecret=lookup.keySecret,resumedSelfServeReplay=!0,reusedPendingSecret=!0,log(` resuming your previous signup attempt for ${repoName}`))}let mintAndPrepareSelfServe=async()=>{let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?reportFatal("Error: Bridge's self-serve signup capacity for this hour is exhausted (this is a global service limit, not a problem with your machine). Try again in about an hour."):mint.category==="invalid"?reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again."):reportFatal("Error: Unable to complete self-serve setup. Check connectivity and retry."),!1;inviteToken=mint.token,inviteFingerprint=fingerprintBootstrapInvite(inviteToken);let prepareSelfServePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite,selfServeReplayToken:inviteToken},credentialWriteDeps),prep=await resolveCredentialConflictConsent(await prepareSelfServePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareSelfServePending(!0)});return prep===null?!1:prep.ok?(keySecret=prep.keySecret,reusedPendingSecret=prep.reused,log(` saved the pending credential for ${prep.target} (fsynced before the exchange)`),!0):(reportFatal(`Error: could not durably store the signup credential (${prep.kind}). ${prep.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1)};if(selfServeSignupMode&&!resumedSelfServeReplay&&(currentStep=INSTALL_BRIDGE_STEP_LABELS.selfServeMint,log(currentStep),!await mintAndPrepareSelfServe()))return 1;if(!selfServeSignupMode){inviteFingerprint=fingerprintBootstrapInvite(inviteToken),currentStep=INSTALL_BRIDGE_STEP_LABELS.inviteRedeem,log(currentStep);let prepareInvitePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite},credentialWriteDeps),prepared=await resolveCredentialConflictConsent(await prepareInvitePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareInvitePending(!0)});if(prepared===null)return 1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error}`),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;keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused,log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`)}let exchange,selfServeRefreshed=!1;for(;;){for(exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);!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)break;if(exchange.kind==="invalid-invite"&&resumedSelfServeReplay&&!selfServeRefreshed){if(!deps.isTTY||!deps.promptLine)return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE,`The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} has been left untouched. Re-run install-bridge on an interactive terminal to discard it and start a fresh signup.`);errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);let consented=!1;try{let answer=(await deps.promptLine(`Discard the saved signup at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} and start a fresh one? [y/N]: `)).trim().toLowerCase();consented=answer==="y"||answer==="yes"}catch{consented=!1}if(!consented)return errorLog("Aborted: the saved signup attempt was left unchanged (nothing was discarded)."),1;let discarded=await deps.discardBootstrapPending({repoName,inviteFingerprint},credentialWriteDeps);if(!discarded.ok)return fatal(`Error: could not discard the saved signup (${discarded.kind}). ${discarded.error}`);if(log("Requesting a fresh Bridge self-serve setup\u2026"),!await mintAndPrepareSelfServe())return 1;selfServeRefreshed=!0,resumedSelfServeReplay=!1;continue}return fatal(exchange.kind==="invalid-invite"?reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE:`Error: ${exchange.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`)}if(bootstrapExchangeSucceeded=!0,exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return fatal(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`);repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||(currentStep=INSTALL_BRIDGE_STEP_LABELS.verifyConnectivity,log(currentStep));let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey,bootstrapInviteMode?void 0:apiKeySource);if(!ping.ok)return fatal(bootstrapInviteMode?`Error: ${ping.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`:`Error: ${ping.message}`);log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir),secretFreeEntry=buildInstallBridgeSecretFreeServerEntry(deps.cwd,repoName,baseUrl,docsDir);currentStep=INSTALL_BRIDGE_STEP_LABELS.writeHostConfigs,log(currentStep);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 trackedState=new Map;for(let target of targets)trackedState.set(target.relPath,await isTrackedProjectConfig(deps,target.relPath));let writeResult=await writeHostConfigs(deps,targets,{real:entry,secretFree:secretFreeEntry},trackedState,{repoName,credentialStorePath});for(let{relPath,mode}of writeResult.written)log(mode==="secret-free"?` wrote ${relPath} (secret-free \u2014 key resolved at runtime)`:` wrote ${relPath}`);for(let{relPath}of writeResult.skipped)log(` skipped ${relPath} \u2014 existing config could not be parsed safely; left untouched`);let needKeyManual=bootstrapInviteMode?{repoName,credentialStorePath}:void 0,globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry,needKeyManual);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,needKeyManual);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(bootstrapExchangeSucceeded){currentStep=INSTALL_BRIDGE_STEP_LABELS.promoteCredential,log(currentStep);let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return fatal(`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. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);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,baseUrl,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"){currentStep=buildInstallBridgeLaunchStepLabel(finalAgentName),log(currentStep);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});if(!spawnResult.ok)return errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0;let handoffToolLabel=toolLabelForLaunchAgent(finalAgentName);return log(""),log(`install-bridge setup steps complete. A fresh ${handoffToolLabel} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} asks \u2014 configuration can't proceed until you do.`),log(`If that tab is closed or fails, open a session in ${handoffToolLabel} and run /install-bridge.`),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 at ${setupUrl} (Get Started page \u2014 install status panel) or via the session's 'Applied N of M' summary.`),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}catch(error){let resumeAdvice=bootstrapInviteMode&&bootstrapExchangeSucceeded?buildBootstrapRetryAdvice(selfServeSignupMode):void 0,lines=buildInstallBridgeFailureLines(error,{step:currentStep,debugEnabled:!!deps.env.BAPI_INSTALL_DEBUG,...resumeAdvice?{resumeAdvice}:{}});for(let line of lines)errorLog(line);return 1}}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path29 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(`
|
|
4796
4808
|
[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(path29.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:
|
|
4797
4809
|
${spawnCommand}`),0}try{let localModulePath=path29.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(`
|
|
4798
4810
|
Upgrading @bridge_gpt/mcp-server to ${targetVersion}...
|