@bridge_gpt/mcp-server 0.2.37 → 0.2.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,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.37"}});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.
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.38"}});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
 
@@ -1759,7 +1759,7 @@ ${stderr}`.matchAll(/^job\s+(\d+)\s+at\b/gim)];return matches.length>0?matches[m
1759
1759
  `);lines.push(["ID","COMMAND","RUN_AT","BACKEND","AGENT","NATIVE","LATEST","UNIT_PATH"].join(" "));for(let e of report.entries)lines.push([e.metadata.id,scheduleCommandLabel(e.metadata),e.metadata.run_at_iso,e.metadata.backend,e.metadata.agent,e.status,latestRunStatus(e.metadata)||"-",e.metadata.unit_path??"-"].join(" "));return lines.join(`
1760
1760
  `)}async function orchestrateScheduleCancel(options,deps){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,notFound:!0,error:`No schedule found with id '${options.id}'.`};if(options.agent!==void 0&&metadata.agent!==options.agent)return{ok:!1,notFound:!0,error:`Schedule '${options.id}' does not match agent filter '${options.agent}'.`};if(options.backend!==void 0&&metadata.backend!==options.backend)return{ok:!1,notFound:!0,error:`Schedule '${options.id}' does not match backend filter '${options.backend}'.`};let backend=getSchedulerBackendByName(metadata.backend);if(!backend)return{ok:!1,error:`Unknown backend '${metadata.backend}' recorded for '${options.id}'.`};let cancelResult=await backend.cancel({deps,metadata});if(!cancelResult.ok)return{ok:!1,error:cancelResult.error??"Backend cancel failed."};let canceledAtIso=new Date(deps.now?deps.now():Date.now()).toISOString();return await appendScheduleRunEvent(options.id,{status:"canceled",at:canceledAtIso},deps.homeDir,deps.platform).catch(()=>{}),await deleteScheduleMetadata(options.id,deps.homeDir,deps.platform),{ok:!0,id:options.id,backend:metadata.backend,nativeRemoved:cancelResult.nativeRemoved,stale:cancelResult.stale,metadataRemoved:!0}}function formatScheduleCancelResult(result){return result.ok?[`Schedule '${result.id}' canceled.`,` backend: ${result.backend}`,` native removed: ${result.nativeRemoved?"yes":`no${result.stale?" (stale)":""}`}`,` metadata removed: ${result.metadataRemoved?"yes":"no"}`," logs: preserved"].join(`
1761
1761
  `):`Error: ${result.error}`}async function orchestrateScheduleDoctor(deps){let platformResult=getSchedulerBackendsForPlatform(deps.platform),envPath=deps.env.PATH??deps.env.Path??"",claudeResolved=!!await resolveCommandOnPath("claude",envPath,deps),cursorResolved=!!await resolveCommandOnPath("cursor-agent",envPath,deps),npxResolved=!!await resolveCommandOnPath("npx",envPath,deps),cursorApiKeyPresent=!!deps.env.CURSOR_API_KEY,bridgeCredentialResolved=deps.bridgeCredentialResolved?.()??!!deps.env.BAPI_API_KEY;if(!platformResult.ok)return{platform:deps.platform,platformSupported:!1,candidateBackends:[],backendAvailability:[],claudeResolved,cursorResolved,npxResolved,cursorApiKeyPresent,bridgeCredentialResolved,unsupportedMessage:platformResult.error};let candidateBackends=platformResult.backends.map(b=>b.name),backendAvailability=[];for(let backend of platformResult.backends)backendAvailability.push({backend:backend.name,available:await backend.isAvailable(deps)});return{platform:deps.platform,platformSupported:!0,candidateBackends,backendAvailability,claudeResolved,cursorResolved,npxResolved,cursorApiKeyPresent,bridgeCredentialResolved}}function formatScheduleDoctorReport(report,json){if(json)return JSON.stringify(report,null,2);let lines=["schedule-run doctor (read-only diagnostics)",`Platform: ${report.platform}`];if(!report.platformSupported)lines.push(report.unsupportedMessage??unsupportedSchedulerPlatformMessage(report.platform));else{lines.push(`Candidate backends (in order): ${report.candidateBackends.join(", ")}`);for(let a of report.backendAvailability)lines.push(` ${a.available?"AVAILABLE ":"UNAVAILABLE"} ${a.backend}`)}return lines.push(`claude on PATH: ${report.claudeResolved?"yes":"no"}`),lines.push(`cursor-agent on PATH: ${report.cursorResolved?"yes":"no"}`),lines.push(`npx on PATH: ${report.npxResolved?"yes":"no"}`),lines.push(`CURSOR_API_KEY set: ${report.cursorApiKeyPresent?"yes":"no"}`),lines.push(`Bridge credential: ${report.bridgeCredentialResolved?"resolved":"not resolved"}`),lines.join(`
1762
- `)}function nowIso2(deps){return new Date(deps.now?deps.now():Date.now()).toISOString()}async function orchestrateScheduleExecute(options,deps,io){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,exitCode:1,error:`No schedule found with id '${options.id}'.`};let agentInvocation=metadata.agent_invocation??metadata.invocation;if(!agentInvocation||!agentInvocation.exe)return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:"missing agent_invocation"},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Schedule '${options.id}' has no agent invocation to run.`};await appendScheduleRunEvent(options.id,{status:"started",at:nowIso2(deps)},deps.homeDir,deps.platform).catch(()=>{});let env={...deps.env};metadata.env_path&&(env.PATH=metadata.env_path,deps.platform==="win32"&&(env.Path=metadata.env_path)),env.BRIDGE_GPT_SCHEDULE_ID=metadata.id,metadata.command&&(env.BRIDGE_GPT_COMMAND=metadata.command),metadata.args&&(env.BRIDGE_GPT_COMMAND_ARGS_JSON=JSON.stringify(metadata.args)),metadata.repo_path&&(env.BRIDGE_GPT_REPO_PATH=metadata.repo_path),metadata.agent&&(env.BRIDGE_GPT_AGENT=metadata.agent),metadata.agent_path&&(env.BRIDGE_GPT_AGENT_PATH=metadata.agent_path),metadata.idea_file&&(env.BRIDGE_GPT_IDEA_FILE=metadata.idea_file);let result;try{result=await deps.runCommand(agentInvocation.exe,agentInvocation.args,{cwd:metadata.repo_path,env})}catch(error){let msg=error instanceof Error?error.message:String(error);return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:`agent launch failed: ${msg}`},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Failed to launch agent: ${msg}`}}return result.stdout&&io.writeStdout(result.stdout),result.stderr&&io.writeStderr(result.stderr),result.exitCode===0?(await appendScheduleRunEvent(options.id,{status:"completed",at:nowIso2(deps),exit_code:0},deps.homeDir,deps.platform).catch(()=>{}),{ok:!0,exitCode:0}):(await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),exit_code:result.exitCode},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:result.exitCode})}async function runScheduleRunCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseScheduleRunArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getScheduleRunUsage()),1;let deps=overrides.deps??createDefaultScheduleRunDeps();try{switch(parsed.subcommand){case"create":{let result=await orchestrateScheduleCreate(parsed.options,deps);return result.ok?(log(formatScheduleCreateResult(result)),0):(errorLog(formatScheduleCreateResult(result)),1)}case"list":{let report=await orchestrateScheduleList(parsed.options,deps);return log(formatScheduleListResult(report,parsed.options.json)),0}case"cancel":{let result=await orchestrateScheduleCancel(parsed.options,deps);return result.ok?(log(formatScheduleCancelResult(result)),0):(errorLog(formatScheduleCancelResult(result)),1)}case"doctor":{let report=await orchestrateScheduleDoctor(deps);return log(formatScheduleDoctorReport(report,parsed.options.json)),report.platformSupported?0:1}case"_execute":{let io={writeStdout:overrides.writeStdout??(chunk=>process.stdout.write(chunk)),writeStderr:overrides.writeStderr??(chunk=>process.stderr.write(chunk))},result=await orchestrateScheduleExecute(parsed.options,deps,io);return!result.ok&&result.error&&errorLog(`Error: ${result.error}`),result.exitCode}}}catch(error){let detail=error instanceof Error?error.message:String(error);return errorLog(`Internal error: ${detail}`),errorLog("Error: schedule-run failed unexpectedly. See the message above for local diagnostics."),1}return 1}var VALID_BACKEND_NAMES,SCHEDULE_ID_PATTERN,init_schedule_run=__esm({"src/schedule-run.ts"(){"use strict";init_scheduler_backends();init_schedule_store();init_agent_launchers();init_claude();init_command_catalog();init_scheduled_prompt();VALID_BACKEND_NAMES=["launchd","task-scheduler","systemd-user","at-fallback"],SCHEDULE_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/}});function canonicalizePlanDAG(plan){let nodes=plan.nodes.map(node=>({...node,ticket_key:node.ticket_key.trim(),depends_on:[...node.depends_on].map(k=>k.trim()).sort(),...node.touched_files?{touched_files:[...node.touched_files].sort()}:{}})).sort((a,b)=>a.ticket_key.localeCompare(b.ticket_key)),edges=[...plan.edges].map(e=>({from:e.from.trim(),to:e.to.trim(),...e.kind?{kind:e.kind}:{},...e.overlap_files?{overlap_files:[...e.overlap_files].sort()}:{}})).sort((a,b)=>{let cmp=a.from.localeCompare(b.from);return cmp!==0?cmp:a.to.localeCompare(b.to)});return{plan_version:plan.plan_version,nodes,edges}}function hashPlan(plan){return stableJsonHash(canonicalizePlanDAG(plan))}var init_plan=__esm({"src/conductor/plan.ts"(){"use strict";init_git_ci_types()}});import{execFileSync as execFileSync2}from"node:child_process";import{basename}from"node:path";function runGitCommand(args,options={}){try{let stdout=execFileSync2("git",args,{cwd:options.cwd,timeout:options.timeoutMs??GIT_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:GIT_COMMAND_MAX_BUFFER,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function firstLine(result){if(!result.ok)return null;let trimmed=result.stdout.trim();return trimmed.length>0?trimmed:null}function sanitizeGitRemoteUrl(url){if(typeof url!="string")return null;let trimmed=url.trim();if(trimmed.length===0)return null;if(/^https?:\/\//i.test(trimmed))try{let parsed=new URL(trimmed);return parsed.username="",parsed.password="",parsed.toString()}catch{return trimmed.replace(/^(https?:\/\/)[^/@]*@/i,"$1")}return trimmed}function getGitWorktreeContext(options={}){let cwd=options.cwd??process.cwd(),env=options.env??process.env,topLevel=firstLine(runGitCommand(["rev-parse","--show-toplevel"],{cwd})),isWorktree=topLevel!==null,worktreePath=topLevel??cwd,gitCommonDir=firstLine(runGitCommand(["rev-parse","--git-common-dir"],{cwd})),branchRaw=firstLine(runGitCommand(["rev-parse","--abbrev-ref","HEAD"],{cwd})),branch=branchRaw===null||branchRaw==="HEAD"?null:branchRaw,headSha=normalizeSha(firstLine(runGitCommand(["rev-parse","HEAD"],{cwd}))??""),remoteOrigin=sanitizeGitRemoteUrl(firstLine(runGitCommand(["config","--get","remote.origin.url"],{cwd}))??"");return{repo:normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME)??normalizeRepoName(env.BAPI_REPO_NAME)??normalizeRepoName(basename(worktreePath))??"unknown",worktree_path:worktreePath,git_common_dir:gitCommonDir,branch,head_sha:headSha,remote_origin:remoteOrigin,is_worktree:isWorktree}}function parseCoAuthoredByTrailers(message){if(typeof message!="string"||message.length===0)return[];let out=[];for(let line of message.split(/\r?\n/)){let match=CO_AUTHOR_RE.exec(line.trim());match&&out.push({name:match[1].trim(),email:match[2].trim()})}return out}function readHeadCommitMetadata(options={}){let ref=options.ref??"HEAD",result=runGitCommand(["show","-s",`--format=${COMMIT_FORMAT}`,ref],{cwd:options.cwd});if(!result.ok)return null;let fields=result.stdout.replace(/\n$/,"").split("");if(fields.length<10)return null;let[sha,parentsRaw,authorName,authorEmail,committerName,committerEmail,authoredAt,committedAt,subject,body]=fields,parents=parentsRaw.trim().split(/\s+/).map(p=>normalizeSha(p)).filter(p=>p!==null),coAuthors=parseCoAuthoredByTrailers(body);return{sha:normalizeSha(sha),parents,author_name:authorName,author_email:authorEmail,committer_name:committerName,committer_email:committerEmail,authored_at:authoredAt,committed_at:committedAt,subject,body,co_authors:coAuthors,attribution_source:coAuthors.length>0?"co-authored-by-trailer":"commit-author"}}function parseReferenceTransactionUpdates(stdin){if(typeof stdin!="string"||stdin.length===0)return[];let out=[];for(let line of stdin.split(/\r?\n/)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length!==3)continue;let oldSha=normalizeSha(parts[0]),newSha=normalizeSha(parts[1]),ref=parts[2];oldSha===null||newSha===null||ref.length===0||REF_CONTROL_CHAR_RE.test(ref)||out.push({old_sha:oldSha,new_sha:newSha,ref})}return out}var GIT_COMMAND_TIMEOUT_MS,GIT_COMMAND_MAX_BUFFER,CO_AUTHOR_RE,COMMIT_FORMAT,REF_CONTROL_CHAR_RE,init_git_inspection=__esm({"src/conductor/git-inspection.ts"(){"use strict";init_git_ci_types();GIT_COMMAND_TIMEOUT_MS=5e3,GIT_COMMAND_MAX_BUFFER=10*1024*1024;CO_AUTHOR_RE=/^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;COMMIT_FORMAT="%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";REF_CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/}});function isPlainObject4(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function inactiveConfig(reason){return{enabled:!1,valid:!1,reason,conditions:[],config_hash:null,gate_name:DEFAULT_GATE_NAME}}function coerceConfigObject(value){if(value==null)return{kind:"unset"};if(typeof value=="string"){let trimmed=value.trim();if(trimmed.length===0)return{kind:"unset"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"invalid"}}return isPlainObject4(parsed)?Object.keys(parsed).length===0?{kind:"unset"}:{kind:"object",object:parsed}:{kind:"invalid"}}return isPlainObject4(value)?Object.keys(value).length===0?{kind:"unset"}:{kind:"object",object:value}:{kind:"invalid"}}function parseCiChecksCondition(entry){let rawChecks=entry.required_checks;if(!Array.isArray(rawChecks)||rawChecks.length===0)return null;let normalized=[],seen=new Set;for(let raw of rawChecks){let name=normalizeCheckName(raw);if(name===null||seen.has(name))return null;seen.add(name),normalized.push(name)}return{type:REQUIRED_CI_CHECKS_GREEN,required_checks:normalized}}function normalizeReviewSource(source){return REVIEW_SOURCE_ALIASES[source]??source}function parseReviewStateCondition(entry){let rawSource=entry.source;if(typeof rawSource!="string")return null;let source=normalizeReviewSource(rawSource);if(!VALID_REVIEW_SOURCES.has(source))return null;let condition={type:REVIEW_STATE,source};if(entry.require_sticky_verdict!==void 0){if(typeof entry.require_sticky_verdict!="boolean")return null;condition.require_sticky_verdict=entry.require_sticky_verdict}if(entry.require_native_decision!==void 0){if(typeof entry.require_native_decision!="boolean")return null;condition.require_native_decision=entry.require_native_decision}if(entry.min_approvals!==void 0){if(typeof entry.min_approvals!="number"||!Number.isInteger(entry.min_approvals)||entry.min_approvals<0)return null;condition.min_approvals=entry.min_approvals}if(entry.logic!==void 0){if(entry.logic!=="and")return null;condition.logic="and"}if(condition.source==="combination"){let hasSticky=condition.require_sticky_verdict===!0,hasNative=condition.require_native_decision===!0,hasMin=typeof condition.min_approvals=="number"&&condition.min_approvals>0;if(!hasSticky&&!hasNative&&!hasMin)return null}return condition}function parseConditions(object){let raw=object.conditions;if(!Array.isArray(raw)||raw.length===0)return null;let seenTypes=new Set,parsed=[];for(let entry of raw){if(!isPlainObject4(entry))return null;let type=entry.type;if(typeof type!="string"||seenTypes.has(type))return null;if(type===REQUIRED_CI_CHECKS_GREEN){let condition=parseCiChecksCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else if(type===REVIEW_STATE){let condition=parseReviewStateCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else return null}return parsed}function parseDoneGateConfig(value){let coerced=coerceConfigObject(value);if(coerced.kind==="unset")return inactiveConfig("unset");if(coerced.kind==="invalid")return inactiveConfig("malformed");let object=coerced.object;if(object.enabled!==!0)return object.enabled===!1?inactiveConfig("disabled"):inactiveConfig("invalid: 'enabled' must be the boolean true");let conditions=parseConditions(object);if(conditions===null)return inactiveConfig("invalid: conditions must be a non-empty array of valid, non-duplicate condition objects");let gateName=DEFAULT_GATE_NAME,configHash=stableJsonHash({gate_name:gateName,conditions:conditions.map(c=>{if(c.type===REQUIRED_CI_CHECKS_GREEN)return{type:c.type,required_checks:c.required_checks};let r={type:c.type,source:c.source};return c.require_sticky_verdict!==void 0&&(r.require_sticky_verdict=c.require_sticky_verdict),c.require_native_decision!==void 0&&(r.require_native_decision=c.require_native_decision),c.min_approvals!==void 0&&(r.min_approvals=c.min_approvals),c.logic!==void 0&&(r.logic=c.logic),r})});return{enabled:!0,valid:!0,reason:"active",conditions,config_hash:configHash,gate_name:gateName}}function asLowerString(value){return typeof value=="string"&&value.trim().length>0?value.trim().toLowerCase():void 0}function normalizeOneCheck(name,raw){let checkName=normalizeCheckName(name);if(checkName===null)return null;if(!isPlainObject4(raw))return{name:checkName,complete:!1,green:!1};let status=asLowerString(raw.status),conclusion=asLowerString(raw.conclusion),explicitComplete=typeof raw.complete=="boolean"?raw.complete:void 0,explicitPassed=typeof raw.passed=="boolean"?raw.passed:void 0,complete=!1;explicitComplete!==void 0?complete=explicitComplete:(conclusion!==void 0&&COMPLETE_STATES.has(conclusion)||status!==void 0&&COMPLETE_STATES.has(status))&&(complete=!0);let green=!1;complete&&(explicitPassed===!0||conclusion!==void 0&&SUCCESS_STATES.has(conclusion)||conclusion===void 0&&explicitPassed===void 0&&status!==void 0&&SUCCESS_STATES.has(status))&&(green=!0),explicitPassed===!1&&(green=!1);let state=conclusion??status??(explicitPassed===!0?"passed":void 0),check={name:checkName,complete,green};return state!==void 0&&(check.state=state),check}function normalizeCiSnapshot(response){let checks=[],byName=new Map,source=isPlainObject4(response)?response:void 0,detail=source&&isPlainObject4(source.detail)?source.detail:void 0;if(source){let rawChecks=source.checks??detail?.checks;if(Array.isArray(rawChecks))for(let entry of rawChecks){if(!isPlainObject4(entry))continue;let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}else if(isPlainObject4(rawChecks))for(let[name,value]of Object.entries(rawChecks)){let normalized=normalizeOneCheck(name,value);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}}let unknownChecks=[],rawUnknown=source?source.unknown_checks??detail?.unknown_checks:void 0;if(Array.isArray(rawUnknown))for(let raw of rawUnknown){let name=normalizeCheckName(raw);name!==null&&!unknownChecks.includes(name)&&unknownChecks.push(name)}let allComplete=checks.length>0&&checks.every(c=>c.complete),allPassed=checks.length>0&&checks.every(c=>c.green)&&unknownChecks.length===0,hashInput={checks:[...checks].sort((a,b)=>a.name.localeCompare(b.name)).map(c=>({name:c.name,complete:c.complete,green:c.green})),unknown_checks:[...unknownChecks].sort()};return{checks,unknown_checks:unknownChecks,check_state_hash:stableJsonHash(hashInput),all_complete:allComplete,all_passed:allPassed}}function normalizeReviewSnapshot(raw){if(!isPlainObject4(raw)||raw.available===!1)return null;let detail=isPlainObject4(raw.detail)?raw.detail:null;if(detail===null)return null;let reviewDecision=typeof detail.review_decision=="string"&&detail.review_decision.length>0?detail.review_decision:null,approvals=typeof detail.approvals=="number"&&Number.isInteger(detail.approvals)&&detail.approvals>=0?detail.approvals:0,rawVerdict=detail.sticky_verdict,stickyVerdict;rawVerdict===REVIEW_VERDICT_APPROVED?stickyVerdict="approved":rawVerdict===REVIEW_VERDICT_CHANGES_REQUESTED?stickyVerdict="changes_requested":rawVerdict===REVIEW_VERDICT_UNKNOWN?stickyVerdict="unknown":stickyVerdict=null;let headSha=typeof detail.head_sha=="string"&&detail.head_sha.trim().length>0?detail.head_sha.trim():null,reviewStateHash=stableJsonHash({review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha});return{review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha,review_state_hash:reviewStateHash}}function evaluateReviewCondition(condition,snapshot){if(snapshot===null)return{passed:!1,changesRequested:!1,reason:"review snapshot unavailable"};let source=condition.source;if(source==="verdict_protocol")return snapshot.sticky_verdict==="approved"?{passed:!0,changesRequested:!1,reason:"sticky verdict approved"}:snapshot.sticky_verdict==="changes_requested"?{passed:!1,changesRequested:!0,reason:"sticky verdict requests changes"}:{passed:!1,changesRequested:!1,reason:`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`};if(source==="native_review_decision"){let dec=snapshot.review_decision?.toUpperCase();return dec==="APPROVED"?{passed:!0,changesRequested:!1,reason:"native review decision approved"}:dec==="CHANGES_REQUESTED"?{passed:!1,changesRequested:!0,reason:"native review decision requests changes"}:{passed:!1,changesRequested:!1,reason:`native review decision not approved: ${snapshot.review_decision??"null"}`}}if(source==="min_approvals"){let required=typeof condition.min_approvals=="number"?condition.min_approvals:1;return snapshot.approvals>=required?{passed:!0,changesRequested:!1,reason:`approvals ${snapshot.approvals} >= ${required}`}:{passed:!1,changesRequested:!1,reason:`approvals ${snapshot.approvals} < ${required}`}}if(source==="combination"){let requireSticky=condition.require_sticky_verdict===!0,requireNative=condition.require_native_decision===!0,minApprovals=typeof condition.min_approvals=="number"?condition.min_approvals:0,failures=[],changesRequested=!1;if(requireSticky&&(snapshot.sticky_verdict==="changes_requested"&&(changesRequested=!0),snapshot.sticky_verdict!=="approved"&&failures.push(`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`)),requireNative){let dec=snapshot.review_decision?.toUpperCase();dec==="CHANGES_REQUESTED"&&(changesRequested=!0),dec!=="APPROVED"&&failures.push(`native decision not approved: ${snapshot.review_decision??"null"}`)}return minApprovals>0&&snapshot.approvals<minApprovals&&failures.push(`approvals ${snapshot.approvals} < ${minApprovals}`),failures.length>0?{passed:!1,changesRequested,reason:failures.join("; ")}:{passed:!0,changesRequested:!1,reason:"all combination sources satisfied"}}return{passed:!1,changesRequested:!1,reason:`unknown review source: ${source}`}}function failedEvaluation(reason){return{met:!1,reason}}function evaluateDoneGate(config,binding,snapshot,evaluatedAtIso,reviewSnapshot=null){if(!config.enabled||!config.valid||config.conditions.length===0)return failedEvaluation(`gate inactive: ${config.reason}`);let headSha=normalizeSha(binding.head_sha);if(headSha===null)return failedEvaluation("invalid binding: head_sha is not a valid SHA");let allFailureReasons=[],checkResults=[],ciConditionType,requiredChecks,reviewResult,byName=new Map;for(let check of snapshot.checks)byName.set(check.name,check);let unknownSet=new Set(snapshot.unknown_checks);for(let condition of config.conditions)if(condition.type===REQUIRED_CI_CHECKS_GREEN){ciConditionType=condition.type,requiredChecks=[...condition.required_checks],checkResults=[];let unmet=[];for(let name of condition.required_checks){let check=byName.get(name);if(!check){checkResults.push({name,present:!1,complete:!1,green:!1}),unmet.push(unknownSet.has(name)?`${name} (unknown)`:`${name} (missing)`);continue}checkResults.push({name,present:!0,complete:check.complete,green:check.green}),check.green||unmet.push(check.complete?`${name} (not green)`:`${name} (pending)`)}unmet.length>0&&allFailureReasons.push(`required checks not green: ${unmet.join(", ")}`)}else condition.type===REVIEW_STATE&&(reviewResult=evaluateReviewCondition(condition,reviewSnapshot),reviewResult.passed||allFailureReasons.push(`review condition not met: ${reviewResult.reason}`));if(allFailureReasons.length>0)return failedEvaluation(allFailureReasons.join("; "));let ciCheckStatus={};ciConditionType!==void 0&&(ciCheckStatus.condition_type=ciConditionType,ciCheckStatus.required_checks=requiredChecks,ciCheckStatus.check_results=checkResults);let reviewStatus={};reviewResult!==void 0&&(reviewStatus.passed=reviewResult.passed,reviewStatus.reason=reviewResult.reason);let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:headSha,gate_name:config.gate_name,config_hash:config.config_hash,evaluated_at:evaluatedAtIso,ci_check_status:ciCheckStatus};return reviewResult!==void 0&&(details.review_status=reviewStatus),{met:!0,reason:"met",gateEventData:{summary:`Done gate "${config.gate_name}" met for ${binding.subject}`,status:"met",details}}}var VALID_REVIEW_SOURCES,REVIEW_SOURCE_ALIASES,SUCCESS_STATES,COMPLETE_STATES,REVIEW_VERDICT_APPROVED,REVIEW_VERDICT_CHANGES_REQUESTED,REVIEW_VERDICT_UNKNOWN,init_done_gate=__esm({"src/conductor/done-gate.ts"(){"use strict";init_git_ci_types();VALID_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision","min_approvals","combination"]),REVIEW_SOURCE_ALIASES=Object.freeze({sticky_verdict:"verdict_protocol",claude_review_sticky:"verdict_protocol",github_review_decision:"native_review_decision"});SUCCESS_STATES=new Set(["success","passed","succeeded"]),COMPLETE_STATES=new Set(["completed","complete","success","passed","succeeded","failure","failed","error","cancelled","canceled","timed_out","action_required","neutral","skipped"]);REVIEW_VERDICT_APPROVED="approved",REVIEW_VERDICT_CHANGES_REQUESTED="changes_requested",REVIEW_VERDICT_UNKNOWN="unknown"}});import{createHash as createHash4}from"node:crypto";function makeProducerDedupeKey(dimensions){let canonical={};for(let[key,value]of Object.entries(dimensions))value!=null&&(canonical[key]=value);return stableJsonHash(canonical)}function makeStableProducerEventId(dedupeKey){let h=createHash4("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError2(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function eventAlreadyExists(dedupeKey,deps={}){let pollEvents=deps.pollEvents??(options=>pollConductorEvents(options)),sinceSeq=1;for(let page=0;page<LEDGER_SCAN_MAX_PAGES;page+=1){let result;try{result=await pollEvents({since_seq:sinceSeq,data_mode:"full",limit:LEDGER_SCAN_PAGE_LIMIT})}catch{return!1}for(let event of result.events){if(!event||typeof event!="object")continue;let data=event.data;if(data&&typeof data=="object"){let details=data.details;if(details&&typeof details=="object"&&details.dedupe_key===dedupeKey)return!0}}if(result.count===0||result.next_seq<=sinceSeq)break;sinceSeq=result.next_seq}return!1}async function emitConductorEventIfNew(input,dimensions,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,dedupeKey=makeProducerDedupeKey(dimensions);if(await eventAlreadyExists(dedupeKey,deps))return{emitted:!1,reason:"duplicate"};let eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}};try{return await emitEvent({...input,id:eventId,data}),{emitted:!0,event_id:eventId}}catch(error){if(isDuplicateConstraintError2(error))return{emitted:!1,reason:"duplicate"};throw error}}var LEDGER_SCAN_PAGE_LIMIT,LEDGER_SCAN_MAX_PAGES,init_producer_ledger=__esm({"src/conductor/producer-ledger.ts"(){"use strict";init_store();init_git_ci_types();LEDGER_SCAN_PAGE_LIMIT=500,LEDGER_SCAN_MAX_PAGES=200}});function buildReviewObservationEventInput(binding,snapshot,eventType,reason,runId=null,workerId=null){return{source:"review",type:eventType,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:REVIEW_PRODUCER_OBSERVED_VIA,data:{summary:eventType===REVIEW_PASSED?`Review passed for ${binding.subject}`:`Review changes requested for ${binding.subject}`,status:eventType===REVIEW_PASSED?"passed":"changes_requested",details:{repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_decision:snapshot.review_decision,approvals:snapshot.approvals,sticky_verdict:snapshot.sticky_verdict,review_state_hash:snapshot.review_state_hash,reason}}}}async function observeReviewWithResolved(binding,access2,gateConfig,deps={}){let fetchStatus=deps.fetchReviewStatus??fetchPrReviewStatus,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={snapshot:null,review_passed_emitted:!1,review_changes_requested_emitted:!1,reason:"observed"},reviewCondition=gateConfig.conditions.find(c=>c.type==="review_state")??null;if(reviewCondition===null)return result.reason="no-review-condition",result;let rawStatus;try{rawStatus=await fetchStatus(access2,binding.pr_number)}catch{return result.reason="review-poll-failed",result}let snapshot=normalizeReviewSnapshot(rawStatus);if(result.snapshot=snapshot,snapshot===null)return result.reason="review-snapshot-unavailable",result;let evalResult=evaluateReviewCondition(reviewCondition,snapshot),baseDimensions={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_state_hash:snapshot.review_state_hash};if(evalResult.changesRequested){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_CHANGES_REQUESTED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_CHANGES_REQUESTED,...baseDimensions});result.review_changes_requested_emitted=decision.emitted,result.reason="review changes requested"}else if(evalResult.passed){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_PASSED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_PASSED,...baseDimensions});result.review_passed_emitted=decision.emitted,result.reason="review passed"}else result.reason=`review not yet passed: ${evalResult.reason}`;return result}var REVIEW_PRODUCER_OBSERVED_VIA,init_pr_review_producer=__esm({"src/conductor/pr-review-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_bridge_api_client();init_producer_ledger();REVIEW_PRODUCER_OBSERVED_VIA="pr-review-producer"}});import{execFileSync as execFileSync3}from"node:child_process";function runGhCommand(args,options={}){try{let stdout=execFileSync3("gh",args,{cwd:options.cwd,timeout:GH_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:4194304,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function discoverPrWithGhCli(options={},deps={}){let result=(deps.runGh??runGhCommand)(GH_PR_VIEW_ARGS,{cwd:options.cwd});if(!result.ok)return null;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return null}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return null;let record=parsed,number=typeof record.number=="number"?record.number:null,state=typeof record.state=="string"?record.state:"";if(number===null||state.length===0)return null;let mergeability=parseGhPrMergeabilityFields(record),discovered={number,head_sha:normalizeSha(record.headRefOid),state,mergeable:mergeability.mergeable,mergeStateStatus:mergeability.mergeStateStatus};return typeof record.headRefName=="string"&&record.headRefName.trim().length>0&&(discovered.head_ref=record.headRefName.trim()),typeof record.baseRefName=="string"&&record.baseRefName.trim().length>0&&(discovered.base_ref=record.baseRefName.trim()),typeof record.url=="string"&&record.url.trim().length>0&&(discovered.url=record.url.trim()),discovered}function makeBinding(repo,prNumber,headSha,extra={}){let binding={repo,pr_number:prNumber,head_sha:headSha,subject:`${repo}#${prNumber}`};return extra.url!==void 0&&(binding.url=extra.url),extra.head_ref!==void 0&&(binding.head_ref=extra.head_ref),extra.base_ref!==void 0&&(binding.base_ref=extra.base_ref),binding}function resolvePrHeadBinding(input={},deps={}){let explicitRepo=input.repoName!==void 0?normalizeRepoName(input.repoName):null;if(input.prNumber!==void 0||input.headSha!==void 0){let prNumber2=normalizePrNumber(input.prNumber),headSha=normalizeSha(input.headSha);if(prNumber2===null||headSha===null)return{ok:!1,reason:"invalid explicit pr_number or head_sha"};if(input.repoName!==void 0&&explicitRepo===null)return{ok:!1,reason:"invalid explicit repo_name"};let repo2=explicitRepo??normalizeRepoName(deps.getContext?.({cwd:input.cwd,env:input.env})?.repo);return repo2===null?{ok:!1,reason:"could not resolve repo name"}:{ok:!0,binding:makeBinding(repo2,prNumber2,headSha)}}let context=(deps.getContext??getGitWorktreeContext)({cwd:input.cwd,env:input.env}),repo=explicitRepo??normalizeRepoName(context.repo),localSha=normalizeSha(context.head_sha??"");if(repo===null||localSha===null)return{ok:!1,reason:"no local repo/HEAD to bind"};let pr=discoverPrWithGhCli({cwd:input.cwd},deps);if(pr===null)return{ok:!1,reason:"gh unavailable or no PR for current branch"};if(pr.state.toUpperCase()!=="OPEN")return{ok:!1,reason:`PR is not open (state: ${pr.state})`};let prNumber=normalizePrNumber(pr.number);return prNumber===null?{ok:!1,reason:"discovered PR number is invalid"}:pr.head_sha!==null&&pr.head_sha!==localSha?{ok:!1,reason:"PR head SHA does not match local HEAD"}:{ok:!0,binding:makeBinding(repo,prNumber,localSha,{url:pr.url,head_ref:pr.head_ref,base_ref:pr.base_ref})}}var GH_COMMAND_TIMEOUT_MS,GH_PR_VIEW_ARGS,init_pr_discovery=__esm({"src/conductor/pr-discovery.ts"(){"use strict";init_git_ci_types();init_github_mergeability();init_git_inspection();GH_COMMAND_TIMEOUT_MS=5e3;GH_PR_VIEW_ARGS=["pr","view","--json","number,headRefOid,headRefName,baseRefName,url,state,mergeable,mergeStateStatus"]}});async function _fetchGateConfigDefault(access2){let setup=await fetchEffectiveSupervisorSetup(access2);if(setup.source!=="none")return setup.done_gate_config??void 0}function buildPrOpenedEventInput(binding,runId=null,workerId=null){let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};binding.head_ref!==void 0&&(details.head_ref=binding.head_ref);let data={summary:`PR ${binding.subject} observed`,status:"open",details};return binding.url!==void 0&&(data.references={url:binding.url}),{source:"git",type:"git.pr_opened",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data}}function buildCiObservationEventInput(binding,snapshot,runId=null,workerId=null){if(snapshot.checks.length===0||!snapshot.checks.every(c=>c.complete))return null;let allGreen=snapshot.checks.every(c=>c.green),type=allGreen?"ci.passed":"ci.failed",details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,checks:snapshot.checks,unknown_checks:snapshot.unknown_checks,check_state_hash:snapshot.check_state_hash};return{source:"ci",type,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{summary:allGreen?`CI passed for ${binding.subject}`:`CI failed for ${binding.subject}`,status:allGreen?"passed":"failed",details}}}function buildGateMetEventInput(binding,evaluation,runId=null,workerId=null){return!evaluation.met||!evaluation.gateEventData?null:{source:"conductor",type:"gate.met",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{...evaluation.gateEventData}}}function defaultSleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}async function observeWithResolved(binding,access2,gateConfig,deps,expectedBaseBranch){let emitConductorEventFn=deps.emitConductorEvent??emitConductorEvent,emitIfNew=deps.emitIfNew??((input,dimensions)=>emitConductorEventIfNew(input,dimensions,{emitEvent:emitConductorEventFn})),pollCi=deps.pollCi??pollCiChecksForCommit,now=deps.now??(()=>new Date().toISOString()),run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let expectedBase=typeof expectedBaseBranch=="string"?expectedBaseBranch.trim():"";if(expectedBase){let observedBase=typeof binding.base_ref=="string"?binding.base_ref.trim():"";if(observedBase!==expectedBase){let actual=observedBase.length>0?observedBase:"(unresolved)";return result.gate_met=!1,result.reason=`pr-base-mismatch: PR #${binding.pr_number} targets base '${actual}' but the run base is '${expectedBase}'. Rebuild the branch from fresh origin/${expectedBase} and cherry-pick only this ticket's commits; do not retarget the PR base in the GitHub UI.`,result}}let rawPoll;try{rawPoll=await pollCi(access2,binding.head_sha)}catch{return result.ci_status="unavailable",result.reason="ci-poll-failed",result}let snapshot=normalizeCiSnapshot(rawPoll),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)result.ci_status="pending";else{result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted}if(!gateConfig.enabled||!gateConfig.valid)return result.reason=`gate inactive: ${gateConfig.reason}`,result;let reviewSnapshot=null;try{reviewSnapshot=(await observeReviewWithResolved(binding,access2,gateConfig,{emitIfNew,env:deps.env})).snapshot}catch{reviewSnapshot=null}let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now(),reviewSnapshot);if(!evaluation.met)return result.reason=evaluation.reason,result;result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}return result.reason="gate met",result}function clampInt(value,fallback,min,max){return typeof value!="number"||!Number.isFinite(value)?fallback:Math.min(max,Math.max(min,Math.floor(value)))}async function waitForDoneGate(params={},deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,resolveAccess=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:params.worktreePath??deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault,sleep3=deps.sleep??defaultSleep2,now=deps.now??(()=>new Date().toISOString()),timeoutMs=clampInt(params.timeoutMs,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,0,WAIT_FOR_GATE_TIMEOUT_MAX_MS),pollIntervalMs=clampInt(params.pollIntervalMs,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,WAIT_FOR_GATE_TIMEOUT_MAX_MS),bindingResult=resolveBinding({repoName:params.repoName,prNumber:params.prNumber,headSha:params.headSha,cwd:params.worktreePath??deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{gate_met:!1,timed_out:!1,reason:`no binding: ${bindingResult.reason}`,repo:null,pr_number:null,head_sha:null};let binding=bindingResult.binding,accessResult=await resolveAccess();if(!accessResult.ok)return{gate_met:!1,timed_out:!1,reason:`access unavailable: ${accessResult.error}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let rawConfig;try{rawConfig=await fetchGateConfig(accessResult.access)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig);if(!gateConfig.enabled||!gateConfig.valid)return{gate_met:!1,timed_out:!1,reason:`gate inactive: ${gateConfig.reason}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let deadline=Date.now()+timeoutMs,loopDeps={...deps,now};for(;;){let observation=await observeWithResolved(binding,accessResult.access,gateConfig,loopDeps);if(observation.gate_met)return{gate_met:!0,timed_out:!1,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,gate_event_summary:observation.gate_event_summary};if(Date.now()>=deadline)return{gate_met:!1,timed_out:!0,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let remaining=deadline-Date.now();await sleep3(Math.min(pollIntervalMs,Math.max(1,remaining)))}}async function observePrCiFromPollResponse(commitRef,pollResponse,deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,now=deps.now??(()=>new Date().toISOString()),bindingResult=resolveBinding({cwd:deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{binding:null,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:`no binding: ${bindingResult.reason}`};let binding=bindingResult.binding;if(commitRef.trim().toLowerCase()!==binding.head_sha)return{binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"commit ref does not match PR head"};let run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let snapshot=normalizeCiSnapshot(pollResponse),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)return result.ci_status="pending",result;result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted;let resolveAccess=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess();if(accessResult.ok){let access2=accessResult.access;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let rawConfig;try{rawConfig=await fetchGateConfig(access2)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig),requiresReview=gateConfig.conditions.some(c=>c.type===REVIEW_STATE);if(gateConfig.enabled&&gateConfig.valid&&requiresReview&&(result.reason="review-gated config: gate.met deferred to wait_for_done_gate (poll path is CI-only)"),gateConfig.enabled&&gateConfig.valid&&!requiresReview){let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now());if(evaluation.met){result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}}}}}catch{}return result}function extractTicketKeyFromRef(headRef){if(!headRef)return null;let match=/([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);return match?match[1].toUpperCase():null}async function resolveDispatchRunIdForBinding(access2,binding,fetchImpl=fetch){let ticketKey=extractTicketKeyFromRef(binding.head_ref);if(!ticketKey)return null;let activeRuns;try{activeRuns=await fetchActiveEpicRuns(access2,fetchImpl)}catch{return null}for(let run of activeRuns)try{let dispatch=(await fetchEpicRunState(access2,run.epic_key,fetchImpl)).dispatches.find(d=>d.ticket_key===ticketKey&&d.run_id!==null);if(dispatch?.run_id)return dispatch.run_id}catch{}return null}var PRODUCER_OBSERVED_VIA,WAIT_FOR_GATE_TIMEOUT_MAX_MS,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,init_pr_ci_producer=__esm({"src/conductor/pr-ci-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_pr_review_producer();init_bridge_api_client();init_pr_discovery();init_producer_ledger();init_store();PRODUCER_OBSERVED_VIA="pr-ci-producer",WAIT_FOR_GATE_TIMEOUT_MAX_MS=12e4,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS=12e4,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS=5e3,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS=500}});function parseBoundedSupervisorInt(raw,fallback,min,max){if(raw===void 0)return fallback;let trimmed=raw.trim();if(trimmed.length===0||!/^[+-]?\d+$/.test(trimmed))return fallback;let parsed=Number.parseInt(trimmed,10);return Number.isFinite(parsed)?Math.min(max,Math.max(min,parsed)):fallback}function resolveSupervisorConfig(overrides={},env=process.env){let wake_interval_ms=clampOverride(overrides.wake_interval_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_WAKE_INTERVAL_MS,45e3,3e4,6e4),3e4,6e4),global_timeout_ms=clampOverride(overrides.global_timeout_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_GLOBAL_TIMEOUT_MS,864e5,3e5,6048e5),3e5,6048e5),escalation_cooldown_ms=clampOverride(overrides.escalation_cooldown_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_ESCALATION_COOLDOWN_MS,9e5,3e5,864e5),3e5,864e5),quiet_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_QUIET_AFTER_MS,3e5,6e4,36e5),liveness_stalled_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LIVENESS_STALLED_AFTER_MS,12e5,3e5,144e5),dead_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_DEAD_AFTER_MS,72e5,6e5,864e5),poll_limit=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT,POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2);return{wake_interval_ms,global_timeout_ms,stall_thresholds_ms:resolveStallThresholds(env),liveness:{quiet_after_ms,stalled_after_ms:liveness_stalled_after_ms,dead_after_ms},escalation_cooldown_ms,poll_limit}}function clampOverride(override,base,min,max){return override===void 0||!Number.isFinite(override)?base:Math.min(max,Math.max(min,Math.floor(override)))}function resolveStallThresholds(env){return{not_started:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_NOT_STARTED_MS,15*6e4,6e4,36e5),active:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_ACTIVE_MS,2*36e5,10*6e4,12*36e5),stalled:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_STALLED_MS,30*6e4,5*6e4,6*36e5),blocked:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_BLOCKED_MS,24*36e5,30*6e4,168*36e5),candidate_done:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_CANDIDATE_DONE_MS,30*6e4,5*6e4,6*36e5),verifying:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_VERIFYING_MS,2*36e5,10*6e4,12*36e5),unknown:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_UNKNOWN_MS,30*6e4,5*6e4,6*36e5),complete:Number.MAX_SAFE_INTEGER,failed:Number.MAX_SAFE_INTEGER}}var POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2,init_supervisor_config=__esm({"src/conductor/supervisor-config.ts"(){"use strict";POLL_LIMIT_DEFAULT2=200,POLL_LIMIT_MIN=1,POLL_LIMIT_MAX2=1e3}});import{createHash as createHash5}from"node:crypto";function normalizeDimension(value){return(value??"").trim().toLowerCase()}function makeSupervisorIdempotencyKey(meta){return[normalizeDimension(meta.run_id),normalizeDimension(meta.worker_id)||"(run)",normalizeDimension(meta.reason),normalizeDimension(meta.kind),normalizeDimension(meta.cooldown_window)].join("|")}function makeSupervisorAssessmentEventId(idempotencyKey){let h=createHash5("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError3(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitSupervisorAssessmentIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,idempotencyKey=makeSupervisorIdempotencyKey(input.idempotency),eventId=makeSupervisorAssessmentEventId(idempotencyKey),details={...input.details??{},idempotency_key:idempotencyKey,reason:input.idempotency.reason,kind:input.idempotency.kind,cooldown_window:input.idempotency.cooldown_window,classification:input.assessment.classification,confidence:input.assessment.confidence},event={id:eventId,source:"conductor-supervisor",type:"supervisor.assessment",run_id:input.run_id,worker_id:input.idempotency.worker_id??input.worker_id??null,producer:"conductor-supervisor",observed_via:"supervisor",data:{summary:`supervisor assessment: ${input.idempotency.reason}`,status:"escalated",reason:input.idempotency.reason,details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError3(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_supervisor_ledger=__esm({"src/conductor/supervisor-ledger.ts"(){"use strict";init_store()}});function buildSupervisorEscalationWorkerMessage(candidate,assessment,state){let details={reason:candidate.reason,state:candidate.state,liveness:candidate.liveness,elapsed_ms:candidate.elapsed_ms,assessment_source:"deterministic"};return{run_id:state.run_id,worker_id:candidate.worker_id,type:`supervisor.${candidate.reason}`,cause_seq:state.last_seq,payload:{summary:`supervisor escalation: ${candidate.reason}`,status:"escalated",details},source:"conductor-supervisor",producer:"worker-message-relay"}}async function sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,deps={}){let sendMessage=deps.sendMessage??sendWorkerMessage,input=buildSupervisorEscalationWorkerMessage(candidate,assessment,state);return sendMessage(input)}var init_supervisor_message_relay=__esm({"src/conductor/supervisor-message-relay.ts"(){"use strict";init_store()}});function isTerminalState(state){return TERMINAL_STATES.has(state)}function isoToMs(value){if(typeof value!="string"||value.length===0)return null;let ms=Date.parse(value);return Number.isFinite(ms)?ms:null}function msToIso(now){return new Date(now).toISOString()}function createEmptySupervisorRunState(runId,config,now){let startedIso=msToIso(now);return{run_id:runId,status:"unknown",last_seq:0,last_event_time:null,workers:{},gates:{},latest_assessment:null,escalations:[],started_at:startedIso,updated_at:startedIso,global_deadline_at:msToIso(now+config.global_timeout_ms),roster_discovered:!1}}function isValidSupervisorSummary(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)&&value.kind===SUPERVISOR_SUMMARY_KIND}function hydrateSupervisorRunStateFromSnapshot(snapshot,runId,config,now){let empty=createEmptySupervisorRunState(runId,config,now),summary=snapshot?.projection?.summary;return isValidSupervisorSummary(summary)?{...empty,status:typeof summary.status=="string"?summary.status:empty.status,last_seq:typeof summary.last_seq=="number"&&summary.last_seq>=0?summary.last_seq:empty.last_seq,last_event_time:typeof summary.last_event_time=="string"?summary.last_event_time:null,workers:isPlainRecord(summary.workers)?summary.workers:{},gates:isPlainRecord(summary.gates)?summary.gates:{},latest_assessment:summary.latest_assessment&&typeof summary.latest_assessment=="object"?summary.latest_assessment:null,escalations:Array.isArray(summary.escalations)?summary.escalations:[],started_at:typeof summary.started_at=="string"?summary.started_at:empty.started_at,global_deadline_at:typeof summary.global_deadline_at=="string"?summary.global_deadline_at:empty.global_deadline_at,roster_discovered:summary.roster_discovered===!0,run_id:runId}:empty}function isPlainRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function ensureWorkerState(state,workerId,options={}){let existing=state.workers[workerId];if(existing)return options.ticketKey&&!existing.ticket_key&&(existing.ticket_key=options.ticketKey),existing;let created={worker_id:workerId,ticket_key:options.ticketKey??null,state:options.fromRoster?"not_started":"unknown",liveness:"unknown",first_seen_seq:options.seq??null,last_event_seq:options.seq??null,last_event_time:null,last_progress_time:null,last_heartbeat_time:null,blocked_reason:null,terminal_reason:null,observed_event_types:[]};return state.workers[workerId]=created,created}function noteObservedType(worker,eventType){worker.observed_event_types.includes(eventType)||worker.observed_event_types.push(eventType)}function eventDetails(event){let details=event.data?.details;return isPlainRecord(details)?details:{}}function extractRoster(event){let candidates=[eventDetails(event).workers,event.data?.workers,isPlainRecord(event.data?.raw)?event.data.raw.workers:void 0];for(let candidate of candidates)if(Array.isArray(candidate)){let roster=[];for(let entry of candidate){if(!isPlainRecord(entry))continue;let workerId=entry.worker_id;if(typeof workerId!="string"||workerId.length===0)continue;let ticketKey=entry.ticket_key;roster.push({worker_id:workerId,ticket_key:typeof ticketKey=="string"?ticketKey:null})}if(roster.length>0)return roster}return[]}function eventStatus(event){let status=event.data?.status;return typeof status=="string"?status.trim().toLowerCase():""}function eventReason(event){let reason=event.data?.reason??eventDetails(event).reason;return typeof reason=="string"?reason.trim().toLowerCase():""}function applyConductorEventToSupervisorState(state,event,now){if(event.run_id!==state.run_id)return state;let eventType=event.type,eventTimeMs=isoToMs(event.time)??now,eventTimeIso=event.time??msToIso(now);if(typeof event.seq=="number"&&event.seq>state.last_seq&&(state.last_seq=event.seq),state.last_event_time=eventTimeIso,state.status==="unknown"&&(state.status="active"),eventType==="run.started"){let roster=extractRoster(event);roster.length>0&&(state.roster_discovered=!0);for(let member of roster){let worker2=ensureWorkerState(state,member.worker_id,{fromRoster:!0,ticketKey:member.ticket_key,seq:event.seq});noteObservedType(worker2,eventType),worker2.last_event_seq=event.seq??worker2.last_event_seq,worker2.last_event_time=eventTimeIso}return state.updated_at=msToIso(now),state}if(eventType==="supervisor.assessment"||eventType==="message.sent")return state.updated_at=msToIso(now),state;let workerId=event.worker_id;if(typeof workerId!="string"||workerId.length===0)return applyRunLevelEvent(state,event,eventType),state.updated_at=msToIso(now),state;let worker=ensureWorkerState(state,workerId,{seq:event.seq});switch(noteObservedType(worker,eventType),worker.last_event_seq=event.seq??worker.last_event_seq,worker.last_event_time=eventTimeIso,eventType){case"run.heartbeat":{worker.last_heartbeat_time=eventTimeIso,!isTerminalState(worker.state)&&worker.state!=="blocked"&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"agent.notification":{let status=eventStatus(event),reason=eventReason(event);BLOCKED_STATUS_TOKENS.has(status)||BLOCKED_STATUS_TOKENS.has(reason)?isTerminalState(worker.state)||(worker.state="blocked",worker.blocked_reason=status||reason||"blocked"):!isTerminalState(worker.state)&&worker.state==="not_started"&&(worker.state="active");break}case"tool.intent":case"worktree.changed":case"git.commit_created":{PROGRESS_EVENT_TYPES.has(eventType)&&(worker.last_progress_time=eventTimeIso,isTerminalState(worker.state)||((worker.state==="not_started"||worker.state==="unknown"||worker.state==="stalled"||worker.state==="blocked")&&(worker.state="active"),worker.blocked_reason=null));break}case"gate.met":{isTerminalState(worker.state)||(worker.state="candidate_done");break}case"ci.passed":{isTerminalState(worker.state)||(worker.state="verifying"),worker.last_progress_time=eventTimeIso;break}case"ci.failed":{let reason=eventReason(event);eventStatus(event)==="terminal"||reason==="terminal"||reason==="give_up"?(worker.state="failed",worker.terminal_reason=reason||"ci_failed"):isTerminalState(worker.state)||(worker.last_progress_time=eventTimeIso);break}case"run.stopped":{let status=eventStatus(event),reason=eventReason(event);FAILED_STATUS_TOKENS.has(status)||FAILED_STATUS_TOKENS.has(reason)?(worker.state="failed",worker.terminal_reason=reason||status||"failed"):(worker.state="complete",worker.terminal_reason=reason||status||"complete");break}case"message.delivered":case"message.acked":{!isTerminalState(worker.state)&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"merge.succeeded":{worker.state="complete",worker.terminal_reason=worker.terminal_reason||"merge_succeeded";break}case"merge.failed":break;case"merge.dry_run":break;case"merge.pending_approval":break;default:break}return state.updated_at=msToIso(now),state}function applyRunLevelEvent(state,event,eventType){switch(eventType){case"gate.met":state.gates.gate_met=!0;break;case"ci.passed":state.gates.ci="passed";break;case"ci.failed":state.gates.ci="failed";break;case"git.pr_opened":state.gates.pr_opened=!0;break;case"merge.succeeded":case"merge.failed":case"merge.dry_run":case"merge.pending_approval":state.gates.merge=eventType.slice(6);break;default:break}}function classifyWorkerLiveness(worker,config,now){if(isTerminalState(worker.state))return"alive";let lastSignalMs=mostRecentSignalMs(worker);if(lastSignalMs===null)return"unknown";let elapsed=now-lastSignalMs;return elapsed>=config.liveness.dead_after_ms?"dead":elapsed>=config.liveness.stalled_after_ms?"stalled":elapsed>=config.liveness.quiet_after_ms?"quiet":"alive"}function mostRecentSignalMs(worker){let candidates=[isoToMs(worker.last_heartbeat_time),isoToMs(worker.last_event_time),isoToMs(worker.last_progress_time)].filter(v=>v!==null);return candidates.length===0?null:Math.max(...candidates)}function stateAnchorMs(worker){return worker.state==="active"||worker.state==="verifying"?mostRecentSignalMs(worker):isoToMs(worker.last_event_time)??isoToMs(worker.last_heartbeat_time)??isoToMs(worker.last_progress_time)}function applySupervisorHousekeeping(state,config,now){for(let worker of Object.values(state.workers)){if(worker.liveness=classifyWorkerLiveness(worker,config,now),isTerminalState(worker.state)||worker.state==="stalled")continue;let threshold=config.stall_thresholds_ms[worker.state],anchor=stateAnchorMs(worker);anchor!==null&&now-anchor>=threshold&&(worker.state="stalled")}return state.updated_at=msToIso(now),state}function isSupervisorRunTerminal(state){let workers=Object.values(state.workers);return workers.length===0||!state.roster_discovered?!1:workers.every(w=>isTerminalState(w.state))}function hasSupervisorGlobalTimeoutElapsed(state,now){let deadlineMs=isoToMs(state.global_deadline_at);return deadlineMs===null?!1:now>=deadlineMs}function compactWorker(worker){return{worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness,last_event_seq:worker.last_event_seq,last_event_time:worker.last_event_time,last_progress_time:worker.last_progress_time,last_heartbeat_time:worker.last_heartbeat_time,blocked_reason:worker.blocked_reason,terminal_reason:worker.terminal_reason}}function toSupervisorProjectionInput(state){let summary={kind:SUPERVISOR_SUMMARY_KIND,run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,workers:state.workers,gates:state.gates,latest_assessment:state.latest_assessment,escalations:state.escalations,started_at:state.started_at,updated_at:state.updated_at,global_deadline_at:state.global_deadline_at,roster_discovered:state.roster_discovered};return{run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,active_workers:Object.values(state.workers).map(compactWorker),gates:state.gates,assessment:state.latest_assessment,summary}}var SUPERVISOR_SUMMARY_KIND,TERMINAL_STATES,PROGRESS_EVENT_TYPES,BLOCKED_STATUS_TOKENS,FAILED_STATUS_TOKENS,init_supervisor_state=__esm({"src/conductor/supervisor-state.ts"(){"use strict";SUPERVISOR_SUMMARY_KIND="supervisor_projection_summary",TERMINAL_STATES=new Set(["complete","failed"]);PROGRESS_EVENT_TYPES=new Set(["tool.intent","worktree.changed","git.commit_created"]),BLOCKED_STATUS_TOKENS=new Set(["blocked","waiting_for_input","needs_input"]),FAILED_STATUS_TOKENS=new Set(["failed","error","errored","aborted","cancelled","canceled"])}});function elapsedSinceSignal(worker,now){let candidates=[worker.last_event_time,worker.last_progress_time,worker.last_heartbeat_time].map(iso=>iso?Date.parse(iso):NaN).filter(v=>Number.isFinite(v));return candidates.length===0?0:Math.max(0,now-Math.max(...candidates))}function findSupervisorEscalationCandidates(state,config,now){let candidates=[];for(let worker of Object.values(state.workers)){if(worker.state==="complete"||worker.state==="failed")continue;let elapsed=elapsedSinceSignal(worker,now),baseContext={worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness};if(worker.liveness==="dead"){candidates.push({reason:"worker_dead",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});continue}switch(worker.state){case"not_started":worker.liveness!=="alive"&&candidates.push({reason:"worker_not_started",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"blocked":candidates.push({reason:"worker_blocked",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:{...baseContext,blocked_reason:worker.blocked_reason}});break;case"stalled":candidates.push({reason:"worker_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"candidate_done":worker.liveness!=="alive"&&candidates.push({reason:"candidate_done_stuck",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"verifying":worker.liveness==="stalled"&&candidates.push({reason:"verification_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;default:break}}let deadlineMs=state.global_deadline_at?Date.parse(state.global_deadline_at):NaN;return Number.isFinite(deadlineMs)&&now>=deadlineMs&&candidates.push({reason:"global_timeout",kind:ESCALATION_KIND,worker_id:null,state:null,liveness:null,elapsed_ms:Math.max(0,now-deadlineMs),context:{run_id:state.run_id,deadline_at:state.global_deadline_at,worker_count:Object.keys(state.workers).length}}),candidates}function cooldownWindowFor(now,cooldownMs){let width=cooldownMs>0?cooldownMs:1;return String(Math.floor(now/width))}function shouldEmitEscalation(state,candidate,config,now){let cooldownWindow=cooldownWindowFor(now,config.escalation_cooldown_ms);return{emit:!state.escalations.some(record=>record.reason===candidate.reason&&(record.worker_id??null)===(candidate.worker_id??null)&&record.cooldown_window===cooldownWindow&&(record.outcome==="emitted"||record.outcome==="duplicate")),cooldown_window:cooldownWindow}}function recordEscalationResult(state,candidate,cooldownWindow,idempotencyKey,outcome2,now){let record={idempotency_key:idempotencyKey,worker_id:candidate.worker_id??null,reason:candidate.reason,kind:candidate.kind,cooldown_window:cooldownWindow,outcome:outcome2,recorded_at:new Date(now).toISOString()};return state.escalations.push(record),record}function formatElapsed(ms){let totalSeconds=Math.max(0,Math.floor(ms/1e3)),hours=Math.floor(totalSeconds/3600),minutes=Math.floor(totalSeconds%3600/60),seconds=totalSeconds%60;return hours>0?`${hours}h${minutes}m`:minutes>0?`${minutes}m`:`${seconds}s`}function formatEscalationForTerminal(runId,candidate){let worker=candidate.worker_id?` worker=${candidate.worker_id}`:"",stateBit=candidate.state?` state=${candidate.state}`:"",liveBit=candidate.liveness?` liveness=${candidate.liveness}`:"",elapsed=` elapsed=${formatElapsed(candidate.elapsed_ms)}`;return`[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`}var ESCALATION_KIND,init_supervisor_escalation=__esm({"src/conductor/supervisor-escalation.ts"(){"use strict";ESCALATION_KIND="escalation"}});function buildGateIdentity(gateName,configHash){let name=gateName.trim(),hash=typeof configHash=="string"?configHash.trim():"";return hash?`${name}@${hash.toLowerCase()}`:name}function makeMergeActionKey(repo,prNumber,headSha,gateIdentity){let r=normalizeRepoName(repo),pr=normalizePrNumber(prNumber),sha=normalizeSha(headSha),gate=(gateIdentity??"").trim();if(r===null||pr===null||sha===null||gate.length===0)throw new Error("invalid merge action key component");return`merge:${r}:${pr}:${sha}:${gate}`}var init_merge_identity=__esm({"src/conductor/merge-identity.ts"(){"use strict";init_git_ci_types()}});function isPlainObject8(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeEventHeadSha(value){if(typeof value!="string")return null;let trimmed=value.trim();return/^[0-9a-f]{7,40}$/i.test(trimmed)?trimmed.toLowerCase():null}function getRawEventDetails(event){let details=event.data?.details;return isPlainObject8(details)?details:null}function parseHeadObservation(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseMergeLifecycle(event){let details=getRawEventDetails(event);return{action_key:details&&typeof details.action_key=="string"&&details.action_key.trim().length>0?details.action_key.trim():null}}function parseGateMet(event){let details=getRawEventDetails(event);if(!details)return{head_sha:null,repo:null,pr_number:null,gate_name:null,config_hash:null,required_checks:[]};let gateName=typeof details.gate_name=="string"&&details.gate_name.trim().length>0?details.gate_name.trim():null,configHash=typeof details.config_hash=="string"&&details.config_hash.trim().length>0?details.config_hash.trim():null,ciCheckStatus=isPlainObject8(details.ci_check_status)?details.ci_check_status:null,requiredChecks=(Array.isArray(details.required_checks)?details.required_checks:ciCheckStatus&&Array.isArray(ciCheckStatus.required_checks)?ciCheckStatus.required_checks:[]).filter(c=>typeof c=="string"&&c.trim().length>0);return{head_sha:normalizeSha(details.head_sha),repo:normalizeRepoName(details.repo),pr_number:normalizePrNumber(details.pr_number),gate_name:gateName,config_hash:configHash,required_checks:requiredChecks}}function parseSpecReview(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseEmpty(){return EMPTY_DETAILS}function getEventDetails(event,expectedType){if(event.type!==expectedType)return null;let parser=EVENT_PARSERS[expectedType];return parser(event)}function getMergeIdentity(event){if(event.type!=="gate.met")return null;let details=getEventDetails(event,"gate.met");if(details===null)return null;let{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName}=details;if(repo===null||prNumber===null||headSha===null||gateName===null)return null;let gateIdentity=buildGateIdentity(gateName,details.config_hash),actionKey=makeMergeActionKey(repo,prNumber,headSha,gateIdentity);return{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName,config_hash:details.config_hash,required_checks:details.required_checks,gate_identity:gateIdentity,action_key:actionKey,gate_event:{id:typeof event.id=="string"?event.id:void 0,seq:typeof event.seq=="number"?event.seq:void 0,time:typeof event.time=="string"?event.time:void 0}}}var EMPTY_DETAILS,EVENT_PARSERS,init_event_accessors=__esm({"src/conductor/event-accessors.ts"(){"use strict";init_git_ci_types();init_merge_identity();EMPTY_DETAILS=Object.freeze({});EVENT_PARSERS={"run.started":parseEmpty,"run.heartbeat":parseEmpty,"run.stopped":parseEmpty,"agent.notification":parseEmpty,"tool.intent":parseEmpty,"worktree.changed":parseEmpty,"git.commit_created":parseEmpty,"git.pr_opened":parseHeadObservation,"ci.passed":parseHeadObservation,"ci.failed":parseHeadObservation,"gate.met":parseGateMet,"supervisor.assessment":parseEmpty,"message.sent":parseEmpty,"message.delivered":parseEmpty,"message.acked":parseEmpty,"merge.dry_run":parseMergeLifecycle,"merge.attempted":parseMergeLifecycle,"merge.succeeded":parseHeadObservation,"merge.failed":parseMergeLifecycle,"merge.conflict":parseHeadObservation,"merge.pending_approval":parseMergeLifecycle,"review.passed":parseHeadObservation,"review.changes_requested":parseHeadObservation,"spec_review.passed":parseSpecReview,"spec_review.changes_requested":parseSpecReview,"parse.triggered":parseEmpty,"parse.succeeded":parseEmpty,"parse.failed":parseEmpty}}});import{createHash as createHash6}from"node:crypto";function extractMergeActionIdentityFromGateEvent(event){return getMergeIdentity(event)}function makeMergeEventId(eventType,actionKey){let h=createHash6("sha256").update(`${eventType}:${actionKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}async function lookupMergeEventByActionKey(eventType,actionKey,deps){let db=await(deps.openDb??(()=>openReadonlyConductorDatabaseIfExists()))();if(!db)return!1;try{return db.prepare(`SELECT 1 FROM events
1762
+ `)}function nowIso2(deps){return new Date(deps.now?deps.now():Date.now()).toISOString()}async function orchestrateScheduleExecute(options,deps,io){let metadata=await readScheduleMetadata(options.id,deps.homeDir,deps.platform);if(!metadata)return{ok:!1,exitCode:1,error:`No schedule found with id '${options.id}'.`};let agentInvocation=metadata.agent_invocation??metadata.invocation;if(!agentInvocation||!agentInvocation.exe)return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:"missing agent_invocation"},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Schedule '${options.id}' has no agent invocation to run.`};await appendScheduleRunEvent(options.id,{status:"started",at:nowIso2(deps)},deps.homeDir,deps.platform).catch(()=>{});let env={...deps.env};metadata.env_path&&(env.PATH=metadata.env_path,deps.platform==="win32"&&(env.Path=metadata.env_path)),env.BRIDGE_GPT_SCHEDULE_ID=metadata.id,metadata.command&&(env.BRIDGE_GPT_COMMAND=metadata.command),metadata.args&&(env.BRIDGE_GPT_COMMAND_ARGS_JSON=JSON.stringify(metadata.args)),metadata.repo_path&&(env.BRIDGE_GPT_REPO_PATH=metadata.repo_path),metadata.agent&&(env.BRIDGE_GPT_AGENT=metadata.agent),metadata.agent_path&&(env.BRIDGE_GPT_AGENT_PATH=metadata.agent_path),metadata.idea_file&&(env.BRIDGE_GPT_IDEA_FILE=metadata.idea_file);let result;try{result=await deps.runCommand(agentInvocation.exe,agentInvocation.args,{cwd:metadata.repo_path,env})}catch(error){let msg=error instanceof Error?error.message:String(error);return await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),message:`agent launch failed: ${msg}`},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:1,error:`Failed to launch agent: ${msg}`}}return result.stdout&&io.writeStdout(result.stdout),result.stderr&&io.writeStderr(result.stderr),result.exitCode===0?(await appendScheduleRunEvent(options.id,{status:"completed",at:nowIso2(deps),exit_code:0},deps.homeDir,deps.platform).catch(()=>{}),{ok:!0,exitCode:0}):(await appendScheduleRunEvent(options.id,{status:"failed",at:nowIso2(deps),exit_code:result.exitCode},deps.homeDir,deps.platform).catch(()=>{}),{ok:!1,exitCode:result.exitCode})}async function runScheduleRunCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseScheduleRunArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getScheduleRunUsage()),1;let deps=overrides.deps??createDefaultScheduleRunDeps();try{switch(parsed.subcommand){case"create":{let result=await orchestrateScheduleCreate(parsed.options,deps);return result.ok?(log(formatScheduleCreateResult(result)),0):(errorLog(formatScheduleCreateResult(result)),1)}case"list":{let report=await orchestrateScheduleList(parsed.options,deps);return log(formatScheduleListResult(report,parsed.options.json)),0}case"cancel":{let result=await orchestrateScheduleCancel(parsed.options,deps);return result.ok?(log(formatScheduleCancelResult(result)),0):(errorLog(formatScheduleCancelResult(result)),1)}case"doctor":{let report=await orchestrateScheduleDoctor(deps);return log(formatScheduleDoctorReport(report,parsed.options.json)),report.platformSupported?0:1}case"_execute":{let io={writeStdout:overrides.writeStdout??(chunk=>process.stdout.write(chunk)),writeStderr:overrides.writeStderr??(chunk=>process.stderr.write(chunk))},result=await orchestrateScheduleExecute(parsed.options,deps,io);return!result.ok&&result.error&&errorLog(`Error: ${result.error}`),result.exitCode}}}catch(error){let detail=error instanceof Error?error.message:String(error);return errorLog(`Internal error: ${detail}`),errorLog("Error: schedule-run failed unexpectedly. See the message above for local diagnostics."),1}return 1}var VALID_BACKEND_NAMES,SCHEDULE_ID_PATTERN,init_schedule_run=__esm({"src/schedule-run.ts"(){"use strict";init_scheduler_backends();init_schedule_store();init_agent_launchers();init_claude();init_command_catalog();init_scheduled_prompt();VALID_BACKEND_NAMES=["launchd","task-scheduler","systemd-user","at-fallback"],SCHEDULE_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/}});function canonicalizePlanDAG(plan){let nodes=plan.nodes.map(node=>({...node,ticket_key:node.ticket_key.trim(),depends_on:[...node.depends_on].map(k=>k.trim()).sort(),...node.touched_files?{touched_files:[...node.touched_files].sort()}:{}})).sort((a,b)=>a.ticket_key.localeCompare(b.ticket_key)),edges=[...plan.edges].map(e=>({from:e.from.trim(),to:e.to.trim(),...e.kind?{kind:e.kind}:{},...e.overlap_files?{overlap_files:[...e.overlap_files].sort()}:{}})).sort((a,b)=>{let cmp=a.from.localeCompare(b.from);return cmp!==0?cmp:a.to.localeCompare(b.to)});return{plan_version:plan.plan_version,nodes,edges}}function hashPlan(plan){return stableJsonHash(canonicalizePlanDAG(plan))}var init_plan=__esm({"src/conductor/plan.ts"(){"use strict";init_git_ci_types()}});import{execFileSync as execFileSync2}from"node:child_process";import{basename}from"node:path";function runGitCommand(args,options={}){try{let stdout=execFileSync2("git",args,{cwd:options.cwd,timeout:options.timeoutMs??GIT_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:GIT_COMMAND_MAX_BUFFER,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function firstLine(result){if(!result.ok)return null;let trimmed=result.stdout.trim();return trimmed.length>0?trimmed:null}function sanitizeGitRemoteUrl(url){if(typeof url!="string")return null;let trimmed=url.trim();if(trimmed.length===0)return null;if(/^https?:\/\//i.test(trimmed))try{let parsed=new URL(trimmed);return parsed.username="",parsed.password="",parsed.toString()}catch{return trimmed.replace(/^(https?:\/\/)[^/@]*@/i,"$1")}return trimmed}function getGitWorktreeContext(options={}){let cwd=options.cwd??process.cwd(),env=options.env??process.env,topLevel=firstLine(runGitCommand(["rev-parse","--show-toplevel"],{cwd})),isWorktree=topLevel!==null,worktreePath=topLevel??cwd,gitCommonDir=firstLine(runGitCommand(["rev-parse","--git-common-dir"],{cwd})),branchRaw=firstLine(runGitCommand(["rev-parse","--abbrev-ref","HEAD"],{cwd})),branch=branchRaw===null||branchRaw==="HEAD"?null:branchRaw,headSha=normalizeSha(firstLine(runGitCommand(["rev-parse","HEAD"],{cwd}))??""),remoteOrigin=sanitizeGitRemoteUrl(firstLine(runGitCommand(["config","--get","remote.origin.url"],{cwd}))??"");return{repo:normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME)??normalizeRepoName(env.BAPI_REPO_NAME)??normalizeRepoName(basename(worktreePath))??"unknown",worktree_path:worktreePath,git_common_dir:gitCommonDir,branch,head_sha:headSha,remote_origin:remoteOrigin,is_worktree:isWorktree}}function parseCoAuthoredByTrailers(message){if(typeof message!="string"||message.length===0)return[];let out=[];for(let line of message.split(/\r?\n/)){let match=CO_AUTHOR_RE.exec(line.trim());match&&out.push({name:match[1].trim(),email:match[2].trim()})}return out}function readHeadCommitMetadata(options={}){let ref=options.ref??"HEAD",result=runGitCommand(["show","-s",`--format=${COMMIT_FORMAT}`,ref],{cwd:options.cwd});if(!result.ok)return null;let fields=result.stdout.replace(/\n$/,"").split("");if(fields.length<10)return null;let[sha,parentsRaw,authorName,authorEmail,committerName,committerEmail,authoredAt,committedAt,subject,body]=fields,parents=parentsRaw.trim().split(/\s+/).map(p=>normalizeSha(p)).filter(p=>p!==null),coAuthors=parseCoAuthoredByTrailers(body);return{sha:normalizeSha(sha),parents,author_name:authorName,author_email:authorEmail,committer_name:committerName,committer_email:committerEmail,authored_at:authoredAt,committed_at:committedAt,subject,body,co_authors:coAuthors,attribution_source:coAuthors.length>0?"co-authored-by-trailer":"commit-author"}}function parseReferenceTransactionUpdates(stdin){if(typeof stdin!="string"||stdin.length===0)return[];let out=[];for(let line of stdin.split(/\r?\n/)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length!==3)continue;let oldSha=normalizeSha(parts[0]),newSha=normalizeSha(parts[1]),ref=parts[2];oldSha===null||newSha===null||ref.length===0||REF_CONTROL_CHAR_RE.test(ref)||out.push({old_sha:oldSha,new_sha:newSha,ref})}return out}var GIT_COMMAND_TIMEOUT_MS,GIT_COMMAND_MAX_BUFFER,CO_AUTHOR_RE,COMMIT_FORMAT,REF_CONTROL_CHAR_RE,init_git_inspection=__esm({"src/conductor/git-inspection.ts"(){"use strict";init_git_ci_types();GIT_COMMAND_TIMEOUT_MS=5e3,GIT_COMMAND_MAX_BUFFER=10*1024*1024;CO_AUTHOR_RE=/^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;COMMIT_FORMAT="%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";REF_CONTROL_CHAR_RE=/[\u0000-\u001F\u007F]/}});function isPlainObject4(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function inactiveConfig(reason){return{enabled:!1,valid:!1,reason,conditions:[],config_hash:null,gate_name:DEFAULT_GATE_NAME}}function coerceConfigObject(value){if(value==null)return{kind:"unset"};if(typeof value=="string"){let trimmed=value.trim();if(trimmed.length===0)return{kind:"unset"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"invalid"}}return isPlainObject4(parsed)?Object.keys(parsed).length===0?{kind:"unset"}:{kind:"object",object:parsed}:{kind:"invalid"}}return isPlainObject4(value)?Object.keys(value).length===0?{kind:"unset"}:{kind:"object",object:value}:{kind:"invalid"}}function parseCiChecksCondition(entry){let rawChecks=entry.required_checks;if(!Array.isArray(rawChecks)||rawChecks.length===0)return null;let normalized=[],seen=new Set;for(let raw of rawChecks){let name=normalizeCheckName(raw);if(name===null||seen.has(name))return null;seen.add(name),normalized.push(name)}return{type:REQUIRED_CI_CHECKS_GREEN,required_checks:normalized}}function normalizeReviewSource(source){return REVIEW_SOURCE_ALIASES[source]??source}function parseReviewStateCondition(entry){let rawSource=entry.source;if(typeof rawSource!="string")return null;let source=normalizeReviewSource(rawSource);if(!VALID_REVIEW_SOURCES.has(source))return null;let condition={type:REVIEW_STATE,source};if(entry.require_sticky_verdict!==void 0){if(typeof entry.require_sticky_verdict!="boolean")return null;condition.require_sticky_verdict=entry.require_sticky_verdict}if(entry.require_native_decision!==void 0){if(typeof entry.require_native_decision!="boolean")return null;condition.require_native_decision=entry.require_native_decision}if(entry.min_approvals!==void 0){if(typeof entry.min_approvals!="number"||!Number.isInteger(entry.min_approvals)||entry.min_approvals<0)return null;condition.min_approvals=entry.min_approvals}if(entry.logic!==void 0){if(entry.logic!=="and")return null;condition.logic="and"}if(condition.source==="combination"){let hasSticky=condition.require_sticky_verdict===!0,hasNative=condition.require_native_decision===!0,hasMin=typeof condition.min_approvals=="number"&&condition.min_approvals>0;if(!hasSticky&&!hasNative&&!hasMin)return null}return condition}function parseConditions(object){let raw=object.conditions;if(!Array.isArray(raw)||raw.length===0)return null;let seenTypes=new Set,parsed=[];for(let entry of raw){if(!isPlainObject4(entry))return null;let type=entry.type;if(typeof type!="string"||seenTypes.has(type))return null;if(type===REQUIRED_CI_CHECKS_GREEN){let condition=parseCiChecksCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else if(type===REVIEW_STATE){let condition=parseReviewStateCondition(entry);if(condition===null)return null;seenTypes.add(type),parsed.push(condition)}else return null}return parsed}function parseDoneGateConfig(value){let coerced=coerceConfigObject(value);if(coerced.kind==="unset")return inactiveConfig("unset");if(coerced.kind==="invalid")return inactiveConfig("malformed");let object=coerced.object;if(object.enabled!==!0)return object.enabled===!1?inactiveConfig("disabled"):inactiveConfig("invalid: 'enabled' must be the boolean true");let conditions=parseConditions(object);if(conditions===null)return inactiveConfig("invalid: conditions must be a non-empty array of valid, non-duplicate condition objects");let gateName=DEFAULT_GATE_NAME,configHash=stableJsonHash({gate_name:gateName,conditions:conditions.map(c=>{if(c.type===REQUIRED_CI_CHECKS_GREEN)return{type:c.type,required_checks:c.required_checks};let r={type:c.type,source:c.source};return c.require_sticky_verdict!==void 0&&(r.require_sticky_verdict=c.require_sticky_verdict),c.require_native_decision!==void 0&&(r.require_native_decision=c.require_native_decision),c.min_approvals!==void 0&&(r.min_approvals=c.min_approvals),c.logic!==void 0&&(r.logic=c.logic),r})});return{enabled:!0,valid:!0,reason:"active",conditions,config_hash:configHash,gate_name:gateName}}function asLowerString(value){return typeof value=="string"&&value.trim().length>0?value.trim().toLowerCase():void 0}function normalizeOneCheck(name,raw){let checkName=normalizeCheckName(name);if(checkName===null)return null;if(!isPlainObject4(raw))return{name:checkName,complete:!1,green:!1};let status=asLowerString(raw.status),conclusion=asLowerString(raw.conclusion),explicitComplete=typeof raw.complete=="boolean"?raw.complete:void 0,explicitPassed=typeof raw.passed=="boolean"?raw.passed:void 0,complete=!1;explicitComplete!==void 0?complete=explicitComplete:(conclusion!==void 0&&COMPLETE_STATES.has(conclusion)||status!==void 0&&COMPLETE_STATES.has(status))&&(complete=!0);let green=!1;complete&&(explicitPassed===!0||conclusion!==void 0&&SUCCESS_STATES.has(conclusion)||conclusion===void 0&&explicitPassed===void 0&&status!==void 0&&SUCCESS_STATES.has(status))&&(green=!0),explicitPassed===!1&&(green=!1);let state=conclusion??status??(explicitPassed===!0?"passed":void 0),check={name:checkName,complete,green};return state!==void 0&&(check.state=state),check}function normalizeCiSnapshot(response){let checks=[],byName=new Map,source=isPlainObject4(response)?response:void 0,detail=source&&isPlainObject4(source.detail)?source.detail:void 0;if(source){let rawChecks=source.checks??detail?.checks;if(Array.isArray(rawChecks))for(let entry of rawChecks){if(!isPlainObject4(entry))continue;let normalized=normalizeOneCheck(entry.name,entry);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}else if(isPlainObject4(rawChecks))for(let[name,value]of Object.entries(rawChecks)){let normalized=normalizeOneCheck(name,value);normalized&&!byName.has(normalized.name)&&(byName.set(normalized.name,normalized),checks.push(normalized))}}let unknownChecks=[],rawUnknown=source?source.unknown_checks??detail?.unknown_checks:void 0;if(Array.isArray(rawUnknown))for(let raw of rawUnknown){let name=normalizeCheckName(raw);name!==null&&!unknownChecks.includes(name)&&unknownChecks.push(name)}let allComplete=checks.length>0&&checks.every(c=>c.complete),allPassed=checks.length>0&&checks.every(c=>c.green)&&unknownChecks.length===0,hashInput={checks:[...checks].sort((a,b)=>a.name.localeCompare(b.name)).map(c=>({name:c.name,complete:c.complete,green:c.green})),unknown_checks:[...unknownChecks].sort()};return{checks,unknown_checks:unknownChecks,check_state_hash:stableJsonHash(hashInput),all_complete:allComplete,all_passed:allPassed}}function normalizeReviewSnapshot(raw){if(!isPlainObject4(raw)||raw.available===!1)return null;let detail=isPlainObject4(raw.detail)?raw.detail:null;if(detail===null)return null;let reviewDecision=typeof detail.review_decision=="string"&&detail.review_decision.length>0?detail.review_decision:null,approvals=typeof detail.approvals=="number"&&Number.isInteger(detail.approvals)&&detail.approvals>=0?detail.approvals:0,rawVerdict=detail.sticky_verdict,stickyVerdict;rawVerdict===REVIEW_VERDICT_APPROVED?stickyVerdict="approved":rawVerdict===REVIEW_VERDICT_CHANGES_REQUESTED?stickyVerdict="changes_requested":rawVerdict===REVIEW_VERDICT_UNKNOWN?stickyVerdict="unknown":stickyVerdict=null;let headSha=typeof detail.head_sha=="string"&&detail.head_sha.trim().length>0?detail.head_sha.trim():null,reviewStateHash=stableJsonHash({review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha});return{review_decision:reviewDecision,approvals,sticky_verdict:stickyVerdict,head_sha:headSha,review_state_hash:reviewStateHash}}function evaluateReviewCondition(condition,snapshot){if(snapshot===null)return{passed:!1,changesRequested:!1,reason:"review snapshot unavailable"};let source=condition.source;if(source==="verdict_protocol")return snapshot.sticky_verdict==="approved"?{passed:!0,changesRequested:!1,reason:"sticky verdict approved"}:snapshot.sticky_verdict==="changes_requested"?{passed:!1,changesRequested:!0,reason:"sticky verdict requests changes"}:{passed:!1,changesRequested:!1,reason:`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`};if(source==="native_review_decision"){let dec=snapshot.review_decision?.toUpperCase();return dec==="APPROVED"?{passed:!0,changesRequested:!1,reason:"native review decision approved"}:dec==="CHANGES_REQUESTED"?{passed:!1,changesRequested:!0,reason:"native review decision requests changes"}:{passed:!1,changesRequested:!1,reason:`native review decision not approved: ${snapshot.review_decision??"null"}`}}if(source==="min_approvals"){let required=typeof condition.min_approvals=="number"?condition.min_approvals:1;return snapshot.approvals>=required?{passed:!0,changesRequested:!1,reason:`approvals ${snapshot.approvals} >= ${required}`}:{passed:!1,changesRequested:!1,reason:`approvals ${snapshot.approvals} < ${required}`}}if(source==="combination"){let requireSticky=condition.require_sticky_verdict===!0,requireNative=condition.require_native_decision===!0,minApprovals=typeof condition.min_approvals=="number"?condition.min_approvals:0,failures=[],changesRequested=!1;if(requireSticky&&(snapshot.sticky_verdict==="changes_requested"&&(changesRequested=!0),snapshot.sticky_verdict!=="approved"&&failures.push(`sticky verdict not approved: ${snapshot.sticky_verdict??"null"}`)),requireNative){let dec=snapshot.review_decision?.toUpperCase();dec==="CHANGES_REQUESTED"&&(changesRequested=!0),dec!=="APPROVED"&&failures.push(`native decision not approved: ${snapshot.review_decision??"null"}`)}return minApprovals>0&&snapshot.approvals<minApprovals&&failures.push(`approvals ${snapshot.approvals} < ${minApprovals}`),failures.length>0?{passed:!1,changesRequested,reason:failures.join("; ")}:{passed:!0,changesRequested:!1,reason:"all combination sources satisfied"}}return{passed:!1,changesRequested:!1,reason:`unknown review source: ${source}`}}function failedEvaluation(reason){return{met:!1,reason}}function evaluateDoneGate(config,binding,snapshot,evaluatedAtIso,reviewSnapshot=null){if(!config.enabled||!config.valid||config.conditions.length===0)return failedEvaluation(`gate inactive: ${config.reason}`);let headSha=normalizeSha(binding.head_sha);if(headSha===null)return failedEvaluation("invalid binding: head_sha is not a valid SHA");let allFailureReasons=[],checkResults=[],ciConditionType,requiredChecks,reviewResult,byName=new Map;for(let check of snapshot.checks)byName.set(check.name,check);let unknownSet=new Set(snapshot.unknown_checks);for(let condition of config.conditions)if(condition.type===REQUIRED_CI_CHECKS_GREEN){ciConditionType=condition.type,requiredChecks=[...condition.required_checks],checkResults=[];let unmet=[];for(let name of condition.required_checks){let check=byName.get(name);if(!check){checkResults.push({name,present:!1,complete:!1,green:!1}),unmet.push(unknownSet.has(name)?`${name} (unknown)`:`${name} (missing)`);continue}checkResults.push({name,present:!0,complete:check.complete,green:check.green}),check.green||unmet.push(check.complete?`${name} (not green)`:`${name} (pending)`)}unmet.length>0&&allFailureReasons.push(`required checks not green: ${unmet.join(", ")}`)}else condition.type===REVIEW_STATE&&(reviewResult=evaluateReviewCondition(condition,reviewSnapshot),reviewResult.passed||allFailureReasons.push(`review condition not met: ${reviewResult.reason}`));if(allFailureReasons.length>0)return failedEvaluation(allFailureReasons.join("; "));let ciCheckStatus={};ciConditionType!==void 0&&(ciCheckStatus.condition_type=ciConditionType,ciCheckStatus.required_checks=requiredChecks,ciCheckStatus.check_results=checkResults);let reviewStatus={};reviewResult!==void 0&&(reviewStatus.passed=reviewResult.passed,reviewStatus.reason=reviewResult.reason);let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:headSha,gate_name:config.gate_name,config_hash:config.config_hash,evaluated_at:evaluatedAtIso,ci_check_status:ciCheckStatus};return reviewResult!==void 0&&(details.review_status=reviewStatus),{met:!0,reason:"met",gateEventData:{summary:`Done gate "${config.gate_name}" met for ${binding.subject}`,status:"met",details}}}var VALID_REVIEW_SOURCES,REVIEW_SOURCE_ALIASES,SUCCESS_STATES,COMPLETE_STATES,REVIEW_VERDICT_APPROVED,REVIEW_VERDICT_CHANGES_REQUESTED,REVIEW_VERDICT_UNKNOWN,init_done_gate=__esm({"src/conductor/done-gate.ts"(){"use strict";init_git_ci_types();VALID_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision","min_approvals","combination"]),REVIEW_SOURCE_ALIASES=Object.freeze({sticky_verdict:"verdict_protocol",claude_review_sticky:"verdict_protocol",github_review_decision:"native_review_decision"});SUCCESS_STATES=new Set(["success","passed","succeeded"]),COMPLETE_STATES=new Set(["completed","complete","success","passed","succeeded","failure","failed","error","cancelled","canceled","timed_out","action_required","neutral","skipped"]);REVIEW_VERDICT_APPROVED="approved",REVIEW_VERDICT_CHANGES_REQUESTED="changes_requested",REVIEW_VERDICT_UNKNOWN="unknown"}});import{createHash as createHash4}from"node:crypto";function makeProducerDedupeKey(dimensions){let canonical={};for(let[key,value]of Object.entries(dimensions))value!=null&&(canonical[key]=value);return stableJsonHash(canonical)}function makeStableProducerEventId(dedupeKey){let h=createHash4("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError2(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function eventAlreadyExists(dedupeKey,deps={}){let pollEvents=deps.pollEvents??(options=>pollConductorEvents(options)),sinceSeq=1;for(let page=0;page<LEDGER_SCAN_MAX_PAGES;page+=1){let result;try{result=await pollEvents({since_seq:sinceSeq,data_mode:"full",limit:LEDGER_SCAN_PAGE_LIMIT})}catch{return!1}for(let event of result.events){if(!event||typeof event!="object")continue;let data=event.data;if(data&&typeof data=="object"){let details=data.details;if(details&&typeof details=="object"&&details.dedupe_key===dedupeKey)return!0}}if(result.count===0||result.next_seq<=sinceSeq)break;sinceSeq=result.next_seq}return!1}async function emitConductorEventIfNew(input,dimensions,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,dedupeKey=makeProducerDedupeKey(dimensions);if(await eventAlreadyExists(dedupeKey,deps))return{emitted:!1,reason:"duplicate"};let eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}};try{return await emitEvent({...input,id:eventId,data}),{emitted:!0,event_id:eventId}}catch(error){if(isDuplicateConstraintError2(error))return{emitted:!1,reason:"duplicate"};throw error}}var LEDGER_SCAN_PAGE_LIMIT,LEDGER_SCAN_MAX_PAGES,init_producer_ledger=__esm({"src/conductor/producer-ledger.ts"(){"use strict";init_store();init_git_ci_types();LEDGER_SCAN_PAGE_LIMIT=500,LEDGER_SCAN_MAX_PAGES=200}});function buildReviewObservationEventInput(binding,snapshot,eventType,reason,runId=null,workerId=null){return{source:"review",type:eventType,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:REVIEW_PRODUCER_OBSERVED_VIA,data:{summary:eventType===REVIEW_PASSED?`Review passed for ${binding.subject}`:`Review changes requested for ${binding.subject}`,status:eventType===REVIEW_PASSED?"passed":"changes_requested",details:{repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_decision:snapshot.review_decision,approvals:snapshot.approvals,sticky_verdict:snapshot.sticky_verdict,review_state_hash:snapshot.review_state_hash,reason}}}}async function observeReviewWithResolved(binding,access2,gateConfig,deps={}){let fetchStatus=deps.fetchReviewStatus??fetchPrReviewStatus,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={snapshot:null,review_passed_emitted:!1,review_changes_requested_emitted:!1,reason:"observed"},reviewCondition=gateConfig.conditions.find(c=>c.type==="review_state")??null;if(reviewCondition===null)return result.reason="no-review-condition",result;let rawStatus;try{rawStatus=await fetchStatus(access2,binding.pr_number)}catch{return result.reason="review-poll-failed",result}let snapshot=normalizeReviewSnapshot(rawStatus);if(result.snapshot=snapshot,snapshot===null)return result.reason="review-snapshot-unavailable",result;let evalResult=evaluateReviewCondition(reviewCondition,snapshot),baseDimensions={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,review_state_hash:snapshot.review_state_hash};if(evalResult.changesRequested){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_CHANGES_REQUESTED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_CHANGES_REQUESTED,...baseDimensions});result.review_changes_requested_emitted=decision.emitted,result.reason="review changes requested"}else if(evalResult.passed){let event=buildReviewObservationEventInput(binding,snapshot,REVIEW_PASSED,evalResult.reason,run_id,worker_id),decision=await emitIfNew(event,{event_type:REVIEW_PASSED,...baseDimensions});result.review_passed_emitted=decision.emitted,result.reason="review passed"}else result.reason=`review not yet passed: ${evalResult.reason}`;return result}var REVIEW_PRODUCER_OBSERVED_VIA,init_pr_review_producer=__esm({"src/conductor/pr-review-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_bridge_api_client();init_producer_ledger();REVIEW_PRODUCER_OBSERVED_VIA="pr-review-producer"}});import{execFileSync as execFileSync3}from"node:child_process";function runGhCommand(args,options={}){try{let stdout=execFileSync3("gh",args,{cwd:options.cwd,timeout:GH_COMMAND_TIMEOUT_MS,encoding:"utf-8",maxBuffer:4194304,stdio:["ignore","pipe","ignore"]});return{ok:!0,stdout:typeof stdout=="string"?stdout:""}}catch{return{ok:!1,stdout:""}}}function discoverPrWithGhCli(options={},deps={}){let result=(deps.runGh??runGhCommand)(GH_PR_VIEW_ARGS,{cwd:options.cwd});if(!result.ok)return null;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return null}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return null;let record=parsed,number=typeof record.number=="number"?record.number:null,state=typeof record.state=="string"?record.state:"";if(number===null||state.length===0)return null;let mergeability=parseGhPrMergeabilityFields(record),discovered={number,head_sha:normalizeSha(record.headRefOid),state,mergeable:mergeability.mergeable,mergeStateStatus:mergeability.mergeStateStatus};return typeof record.headRefName=="string"&&record.headRefName.trim().length>0&&(discovered.head_ref=record.headRefName.trim()),typeof record.baseRefName=="string"&&record.baseRefName.trim().length>0&&(discovered.base_ref=record.baseRefName.trim()),typeof record.url=="string"&&record.url.trim().length>0&&(discovered.url=record.url.trim()),discovered}function makeBinding(repo,prNumber,headSha,extra={}){let binding={repo,pr_number:prNumber,head_sha:headSha,subject:`${repo}#${prNumber}`};return extra.url!==void 0&&(binding.url=extra.url),extra.head_ref!==void 0&&(binding.head_ref=extra.head_ref),extra.base_ref!==void 0&&(binding.base_ref=extra.base_ref),binding}function resolvePrHeadBinding(input={},deps={}){let explicitRepo=input.repoName!==void 0?normalizeRepoName(input.repoName):null;if(input.prNumber!==void 0||input.headSha!==void 0){let prNumber2=normalizePrNumber(input.prNumber),headSha=normalizeSha(input.headSha);if(prNumber2===null||headSha===null)return{ok:!1,reason:"invalid explicit pr_number or head_sha"};if(input.repoName!==void 0&&explicitRepo===null)return{ok:!1,reason:"invalid explicit repo_name"};let repo2=explicitRepo??normalizeRepoName(deps.getContext?.({cwd:input.cwd,env:input.env})?.repo);return repo2===null?{ok:!1,reason:"could not resolve repo name"}:{ok:!0,binding:makeBinding(repo2,prNumber2,headSha)}}let context=(deps.getContext??getGitWorktreeContext)({cwd:input.cwd,env:input.env}),repo=explicitRepo??normalizeRepoName(context.repo),localSha=normalizeSha(context.head_sha??"");if(repo===null||localSha===null)return{ok:!1,reason:"no local repo/HEAD to bind"};let pr=discoverPrWithGhCli({cwd:input.cwd},deps);if(pr===null)return{ok:!1,reason:"gh unavailable or no PR for current branch"};if(pr.state.toUpperCase()!=="OPEN")return{ok:!1,reason:`PR is not open (state: ${pr.state})`};let prNumber=normalizePrNumber(pr.number);return prNumber===null?{ok:!1,reason:"discovered PR number is invalid"}:pr.head_sha!==null&&pr.head_sha!==localSha?{ok:!1,reason:"PR head SHA does not match local HEAD"}:{ok:!0,binding:makeBinding(repo,prNumber,localSha,{url:pr.url,head_ref:pr.head_ref,base_ref:pr.base_ref})}}var GH_COMMAND_TIMEOUT_MS,GH_PR_VIEW_ARGS,init_pr_discovery=__esm({"src/conductor/pr-discovery.ts"(){"use strict";init_git_ci_types();init_github_mergeability();init_git_inspection();GH_COMMAND_TIMEOUT_MS=5e3;GH_PR_VIEW_ARGS=["pr","view","--json","number,headRefOid,headRefName,baseRefName,url,state,mergeable,mergeStateStatus"]}});async function _fetchGateConfigDefault(access2){let setup=await fetchEffectiveSupervisorSetup(access2);if(setup.source!=="none")return setup.done_gate_config??void 0}function buildPrOpenedEventInput(binding,runId=null,workerId=null){let details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};binding.head_ref!==void 0&&(details.head_ref=binding.head_ref);let data={summary:`PR ${binding.subject} observed`,status:"open",details};return binding.url!==void 0&&(data.references={url:binding.url}),{source:"git",type:"git.pr_opened",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data}}function buildCiObservationEventInput(binding,snapshot,runId=null,workerId=null){if(snapshot.checks.length===0||!snapshot.checks.every(c=>c.complete))return null;let allGreen=snapshot.checks.every(c=>c.green),type=allGreen?"ci.passed":"ci.failed",details={repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,checks:snapshot.checks,unknown_checks:snapshot.unknown_checks,check_state_hash:snapshot.check_state_hash};return{source:"ci",type,subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{summary:allGreen?`CI passed for ${binding.subject}`:`CI failed for ${binding.subject}`,status:allGreen?"passed":"failed",details}}}function buildGateMetEventInput(binding,evaluation,runId=null,workerId=null){return!evaluation.met||!evaluation.gateEventData?null:{source:"conductor",type:"gate.met",subject:binding.subject,run_id:runId,worker_id:workerId,producer:GIT_CI_PRODUCER,observed_via:PRODUCER_OBSERVED_VIA,data:{...evaluation.gateEventData}}}function defaultSleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}async function observeWithResolved(binding,access2,gateConfig,deps,expectedBaseBranch){let emitConductorEventFn=deps.emitConductorEvent??emitConductorEvent,emitIfNew=deps.emitIfNew??((input,dimensions)=>emitConductorEventIfNew(input,dimensions,{emitEvent:emitConductorEventFn})),pollCi=deps.pollCi??pollCiChecksForCommit,now=deps.now??(()=>new Date().toISOString()),run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let expectedBase=typeof expectedBaseBranch=="string"?expectedBaseBranch.trim():"";if(expectedBase){let observedBase=typeof binding.base_ref=="string"?binding.base_ref.trim():"";if(observedBase!==expectedBase){let actual=observedBase.length>0?observedBase:"(unresolved)";return result.gate_met=!1,result.reason=`pr-base-mismatch: PR #${binding.pr_number} targets base '${actual}' but the run base is '${expectedBase}'. Rebuild the branch from fresh origin/${expectedBase} and cherry-pick only this ticket's commits; do not retarget the PR base in the GitHub UI.`,result}}let rawPoll;try{rawPoll=await pollCi(access2,binding.head_sha)}catch{return result.ci_status="unavailable",result.reason="ci-poll-failed",result}let snapshot=normalizeCiSnapshot(rawPoll),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)result.ci_status="pending";else{result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted}if(!gateConfig.enabled||!gateConfig.valid)return result.reason=`gate inactive: ${gateConfig.reason}`,result;let reviewSnapshot=null;try{reviewSnapshot=(await observeReviewWithResolved(binding,access2,gateConfig,{emitIfNew,env:deps.env})).snapshot}catch{reviewSnapshot=null}let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now(),reviewSnapshot);if(!evaluation.met)return result.reason=evaluation.reason,result;result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}return result.reason="gate met",result}function clampInt(value,fallback,min,max){return typeof value!="number"||!Number.isFinite(value)?fallback:Math.min(max,Math.max(min,Math.floor(value)))}async function waitForDoneGate(params={},deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,resolveAccess=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:params.worktreePath??deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault,sleep3=deps.sleep??defaultSleep2,now=deps.now??(()=>new Date().toISOString()),timeoutMs=clampInt(params.timeoutMs,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,0,WAIT_FOR_GATE_TIMEOUT_MAX_MS),pollIntervalMs=clampInt(params.pollIntervalMs,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,WAIT_FOR_GATE_TIMEOUT_MAX_MS),bindingResult=resolveBinding({repoName:params.repoName,prNumber:params.prNumber,headSha:params.headSha,cwd:params.worktreePath??deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{gate_met:!1,timed_out:!1,reason:`no binding: ${bindingResult.reason}`,repo:null,pr_number:null,head_sha:null};let binding=bindingResult.binding,accessResult=await resolveAccess();if(!accessResult.ok)return{gate_met:!1,timed_out:!1,reason:`access unavailable: ${accessResult.error}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let rawConfig;try{rawConfig=await fetchGateConfig(accessResult.access)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig);if(!gateConfig.enabled||!gateConfig.valid)return{gate_met:!1,timed_out:!1,reason:`gate inactive: ${gateConfig.reason}`,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let deadline=Date.now()+timeoutMs,loopDeps={...deps,now};for(;;){let observation=await observeWithResolved(binding,accessResult.access,gateConfig,loopDeps);if(observation.gate_met)return{gate_met:!0,timed_out:!1,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,gate_event_summary:observation.gate_event_summary};if(Date.now()>=deadline)return{gate_met:!1,timed_out:!0,reason:observation.reason,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha};let remaining=deadline-Date.now();await sleep3(Math.min(pollIntervalMs,Math.max(1,remaining)))}}async function observePrCiFromPollResponse(commitRef,pollResponse,deps={}){let resolveBinding=deps.resolveBinding??resolvePrHeadBinding,emitIfNew=deps.emitIfNew??emitConductorEventIfNew,now=deps.now??(()=>new Date().toISOString()),bindingResult=resolveBinding({cwd:deps.cwd,env:deps.env},deps.bindingDeps??{});if(!bindingResult.ok)return{binding:null,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:`no binding: ${bindingResult.reason}`};let binding=bindingResult.binding;if(commitRef.trim().toLowerCase()!==binding.head_sha)return{binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"commit ref does not match PR head"};let run_id=deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim()||null,worker_id=deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim()||null,result={binding,pr_opened_emitted:!1,ci_status:null,ci_emitted:!1,gate_met:!1,gate_emitted:!1,reason:"observed"},prDecision=await emitIfNew(buildPrOpenedEventInput(binding,run_id,worker_id),{event_type:"git.pr_opened",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha});result.pr_opened_emitted=prDecision.emitted;let snapshot=normalizeCiSnapshot(pollResponse),ciEvent=buildCiObservationEventInput(binding,snapshot,run_id,worker_id);if(ciEvent===null)return result.ci_status="pending",result;result.ci_status=ciEvent.type==="ci.passed"?"passed":"failed";let ciDecision=await emitIfNew(ciEvent,{event_type:ciEvent.type,repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,ci_check_hash:snapshot.check_state_hash});result.ci_emitted=ciDecision.emitted;let resolveAccess=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess();if(accessResult.ok){let access2=accessResult.access;if(run_id===null&&deps.resolveRunId)try{run_id=await deps.resolveRunId(access2,binding)??null}catch{run_id=null}let rawConfig;try{rawConfig=await fetchGateConfig(access2)}catch{rawConfig=void 0}let gateConfig=parseDoneGateConfig(rawConfig),requiresReview=gateConfig.conditions.some(c=>c.type===REVIEW_STATE);if(gateConfig.enabled&&gateConfig.valid&&requiresReview&&(result.reason="review-gated config: gate.met deferred to wait_for_done_gate (poll path is CI-only)"),gateConfig.enabled&&gateConfig.valid&&!requiresReview){let evaluation=evaluateDoneGate(gateConfig,binding,snapshot,now());if(evaluation.met){result.gate_met=!0;let gateEvent=buildGateMetEventInput(binding,evaluation,run_id,worker_id);if(gateEvent!==null){let gateDecision=await emitIfNew(gateEvent,{event_type:"gate.met",repo:binding.repo,pr_number:binding.pr_number,head_sha:binding.head_sha,config_hash:gateConfig.config_hash??void 0});result.gate_emitted=gateDecision.emitted,result.gate_event_summary=gateEvent.data?.summary}}}}}catch{}return result}function extractTicketKeyFromRef(headRef){if(!headRef)return null;let match=/([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);return match?match[1].toUpperCase():null}async function resolveDispatchRunIdForBinding(access2,binding,fetchImpl=fetch){let ticketKey=extractTicketKeyFromRef(binding.head_ref);if(!ticketKey)return null;let activeRuns;try{activeRuns=await fetchActiveEpicRuns(access2,fetchImpl)}catch{return null}for(let run of activeRuns)try{let dispatch=(await fetchEpicRunState(access2,run.epic_key,fetchImpl)).dispatches.find(d=>d.ticket_key===ticketKey&&d.run_id!==null);if(dispatch?.run_id)return dispatch.run_id}catch{}return null}var PRODUCER_OBSERVED_VIA,WAIT_FOR_GATE_TIMEOUT_MAX_MS,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS,init_pr_ci_producer=__esm({"src/conductor/pr-ci-producer.ts"(){"use strict";init_git_ci_types();init_done_gate();init_pr_review_producer();init_bridge_api_client();init_pr_discovery();init_producer_ledger();init_store();PRODUCER_OBSERVED_VIA="pr-ci-producer",WAIT_FOR_GATE_TIMEOUT_MAX_MS=12e4,WAIT_FOR_GATE_TIMEOUT_DEFAULT_MS=12e4,WAIT_FOR_GATE_POLL_INTERVAL_DEFAULT_MS=5e3,WAIT_FOR_GATE_POLL_INTERVAL_MIN_MS=500}});function parseBoundedSupervisorInt(raw,fallback,min,max){if(raw===void 0)return fallback;let trimmed=raw.trim();if(trimmed.length===0||!/^[+-]?\d+$/.test(trimmed))return fallback;let parsed=Number.parseInt(trimmed,10);return Number.isFinite(parsed)?Math.min(max,Math.max(min,parsed)):fallback}function resolveSupervisorConfig(overrides={},env=process.env){let wake_interval_ms=clampOverride(overrides.wake_interval_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_WAKE_INTERVAL_MS,45e3,3e4,6e4),3e4,6e4),global_timeout_ms=clampOverride(overrides.global_timeout_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_GLOBAL_TIMEOUT_MS,864e5,3e5,6048e5),3e5,6048e5),escalation_cooldown_ms=clampOverride(overrides.escalation_cooldown_ms,parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_ESCALATION_COOLDOWN_MS,9e5,3e5,864e5),3e5,864e5),quiet_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_QUIET_AFTER_MS,3e5,6e4,36e5),liveness_stalled_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LIVENESS_STALLED_AFTER_MS,12e5,3e5,144e5),dead_after_ms=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_DEAD_AFTER_MS,72e5,6e5,864e5),poll_limit=parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT,POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2);return{wake_interval_ms,global_timeout_ms,stall_thresholds_ms:resolveStallThresholds(env),liveness:{quiet_after_ms,stalled_after_ms:liveness_stalled_after_ms,dead_after_ms},escalation_cooldown_ms,poll_limit}}function clampOverride(override,base,min,max){return override===void 0||!Number.isFinite(override)?base:Math.min(max,Math.max(min,Math.floor(override)))}function resolveStallThresholds(env){return{not_started:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_NOT_STARTED_MS,15*6e4,6e4,36e5),active:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_ACTIVE_MS,2*36e5,10*6e4,12*36e5),stalled:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_STALLED_MS,30*6e4,5*6e4,6*36e5),blocked:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_BLOCKED_MS,24*36e5,30*6e4,168*36e5),candidate_done:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_CANDIDATE_DONE_MS,30*6e4,5*6e4,6*36e5),verifying:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_VERIFYING_MS,2*36e5,10*6e4,12*36e5),unknown:parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_STALL_UNKNOWN_MS,30*6e4,5*6e4,6*36e5),complete:Number.MAX_SAFE_INTEGER,failed:Number.MAX_SAFE_INTEGER}}var POLL_LIMIT_DEFAULT2,POLL_LIMIT_MIN,POLL_LIMIT_MAX2,init_supervisor_config=__esm({"src/conductor/supervisor-config.ts"(){"use strict";POLL_LIMIT_DEFAULT2=200,POLL_LIMIT_MIN=1,POLL_LIMIT_MAX2=1e3}});import{createHash as createHash5}from"node:crypto";function normalizeDimension(value){return(value??"").trim().toLowerCase()}function makeSupervisorIdempotencyKey(meta){return[normalizeDimension(meta.run_id),normalizeDimension(meta.worker_id)||"(run)",normalizeDimension(meta.reason),normalizeDimension(meta.kind),normalizeDimension(meta.cooldown_window)].join("|")}function makeSupervisorAssessmentEventId(idempotencyKey){let h=createHash5("sha256").update(`supervisor.assessment:${idempotencyKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}function isDuplicateConstraintError3(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitSupervisorAssessmentIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,idempotencyKey=makeSupervisorIdempotencyKey(input.idempotency),eventId=makeSupervisorAssessmentEventId(idempotencyKey),details={...input.details??{},idempotency_key:idempotencyKey,reason:input.idempotency.reason,kind:input.idempotency.kind,cooldown_window:input.idempotency.cooldown_window,classification:input.assessment.classification,confidence:input.assessment.confidence},event={id:eventId,source:"conductor-supervisor",type:"supervisor.assessment",run_id:input.run_id,worker_id:input.idempotency.worker_id??input.worker_id??null,producer:"conductor-supervisor",observed_via:"supervisor",data:{summary:`supervisor assessment: ${input.idempotency.reason}`,status:"escalated",reason:input.idempotency.reason,details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError3(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_supervisor_ledger=__esm({"src/conductor/supervisor-ledger.ts"(){"use strict";init_store()}});function buildSupervisorEscalationWorkerMessage(candidate,assessment,state){let details={reason:candidate.reason,state:candidate.state,liveness:candidate.liveness,elapsed_ms:candidate.elapsed_ms,assessment_source:"deterministic"};return{run_id:state.run_id,worker_id:candidate.worker_id,type:`supervisor.${candidate.reason}`,cause_seq:state.last_seq,payload:{summary:`supervisor escalation: ${candidate.reason}`,status:"escalated",details},source:"conductor-supervisor",producer:"worker-message-relay"}}async function sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,deps={}){let sendMessage=deps.sendMessage??sendWorkerMessage,input=buildSupervisorEscalationWorkerMessage(candidate,assessment,state);return sendMessage(input)}var init_supervisor_message_relay=__esm({"src/conductor/supervisor-message-relay.ts"(){"use strict";init_store()}});function isTerminalState(state){return TERMINAL_STATES.has(state)}function isoToMs(value){if(typeof value!="string"||value.length===0)return null;let ms=Date.parse(value);return Number.isFinite(ms)?ms:null}function msToIso(now){return new Date(now).toISOString()}function createEmptySupervisorRunState(runId,config,now){let startedIso=msToIso(now);return{run_id:runId,status:"unknown",last_seq:0,last_event_time:null,workers:{},gates:{},latest_assessment:null,escalations:[],started_at:startedIso,updated_at:startedIso,global_deadline_at:msToIso(now+config.global_timeout_ms),roster_discovered:!1}}function isValidSupervisorSummary(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)&&value.kind===SUPERVISOR_SUMMARY_KIND}function hydrateSupervisorRunStateFromSnapshot(snapshot,runId,config,now){let empty=createEmptySupervisorRunState(runId,config,now),summary=snapshot?.projection?.summary;return isValidSupervisorSummary(summary)?{...empty,status:typeof summary.status=="string"?summary.status:empty.status,last_seq:typeof summary.last_seq=="number"&&summary.last_seq>=0?summary.last_seq:empty.last_seq,last_event_time:typeof summary.last_event_time=="string"?summary.last_event_time:null,workers:isPlainRecord(summary.workers)?summary.workers:{},gates:isPlainRecord(summary.gates)?summary.gates:{},latest_assessment:summary.latest_assessment&&typeof summary.latest_assessment=="object"?summary.latest_assessment:null,escalations:Array.isArray(summary.escalations)?summary.escalations:[],started_at:typeof summary.started_at=="string"?summary.started_at:empty.started_at,global_deadline_at:typeof summary.global_deadline_at=="string"?summary.global_deadline_at:empty.global_deadline_at,roster_discovered:summary.roster_discovered===!0,run_id:runId}:empty}function isPlainRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function ensureWorkerState(state,workerId,options={}){let existing=state.workers[workerId];if(existing)return options.ticketKey&&!existing.ticket_key&&(existing.ticket_key=options.ticketKey),existing;let created={worker_id:workerId,ticket_key:options.ticketKey??null,state:options.fromRoster?"not_started":"unknown",liveness:"unknown",first_seen_seq:options.seq??null,last_event_seq:options.seq??null,last_event_time:null,last_progress_time:null,last_heartbeat_time:null,blocked_reason:null,terminal_reason:null,observed_event_types:[]};return state.workers[workerId]=created,created}function noteObservedType(worker,eventType){worker.observed_event_types.includes(eventType)||worker.observed_event_types.push(eventType)}function eventDetails(event){let details=event.data?.details;return isPlainRecord(details)?details:{}}function extractRoster(event){let candidates=[eventDetails(event).workers,event.data?.workers,isPlainRecord(event.data?.raw)?event.data.raw.workers:void 0];for(let candidate of candidates)if(Array.isArray(candidate)){let roster=[];for(let entry of candidate){if(!isPlainRecord(entry))continue;let workerId=entry.worker_id;if(typeof workerId!="string"||workerId.length===0)continue;let ticketKey=entry.ticket_key;roster.push({worker_id:workerId,ticket_key:typeof ticketKey=="string"?ticketKey:null})}if(roster.length>0)return roster}return[]}function eventStatus(event){let status=event.data?.status;return typeof status=="string"?status.trim().toLowerCase():""}function eventReason(event){let reason=event.data?.reason??eventDetails(event).reason;return typeof reason=="string"?reason.trim().toLowerCase():""}function applyConductorEventToSupervisorState(state,event,now){if(event.run_id!==state.run_id)return state;let eventType=event.type,eventTimeMs=isoToMs(event.time)??now,eventTimeIso=event.time??msToIso(now);if(typeof event.seq=="number"&&event.seq>state.last_seq&&(state.last_seq=event.seq),state.last_event_time=eventTimeIso,state.status==="unknown"&&(state.status="active"),eventType==="run.started"){let roster=extractRoster(event);roster.length>0&&(state.roster_discovered=!0);for(let member of roster){let worker2=ensureWorkerState(state,member.worker_id,{fromRoster:!0,ticketKey:member.ticket_key,seq:event.seq});noteObservedType(worker2,eventType),worker2.last_event_seq=event.seq??worker2.last_event_seq,worker2.last_event_time=eventTimeIso}return state.updated_at=msToIso(now),state}if(eventType==="supervisor.assessment"||eventType==="message.sent")return state.updated_at=msToIso(now),state;let workerId=event.worker_id;if(typeof workerId!="string"||workerId.length===0)return applyRunLevelEvent(state,event,eventType),state.updated_at=msToIso(now),state;let worker=ensureWorkerState(state,workerId,{seq:event.seq});switch(noteObservedType(worker,eventType),worker.last_event_seq=event.seq??worker.last_event_seq,worker.last_event_time=eventTimeIso,eventType){case"run.heartbeat":{worker.last_heartbeat_time=eventTimeIso,!isTerminalState(worker.state)&&worker.state!=="blocked"&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"agent.notification":{let status=eventStatus(event),reason=eventReason(event);BLOCKED_STATUS_TOKENS.has(status)||BLOCKED_STATUS_TOKENS.has(reason)?isTerminalState(worker.state)||(worker.state="blocked",worker.blocked_reason=status||reason||"blocked"):!isTerminalState(worker.state)&&worker.state==="not_started"&&(worker.state="active");break}case"tool.intent":case"worktree.changed":case"git.commit_created":{PROGRESS_EVENT_TYPES.has(eventType)&&(worker.last_progress_time=eventTimeIso,isTerminalState(worker.state)||((worker.state==="not_started"||worker.state==="unknown"||worker.state==="stalled"||worker.state==="blocked")&&(worker.state="active"),worker.blocked_reason=null));break}case"gate.met":{isTerminalState(worker.state)||(worker.state="candidate_done");break}case"ci.passed":{isTerminalState(worker.state)||(worker.state="verifying"),worker.last_progress_time=eventTimeIso;break}case"ci.failed":{let reason=eventReason(event);eventStatus(event)==="terminal"||reason==="terminal"||reason==="give_up"?(worker.state="failed",worker.terminal_reason=reason||"ci_failed"):isTerminalState(worker.state)||(worker.last_progress_time=eventTimeIso);break}case"run.stopped":{let status=eventStatus(event),reason=eventReason(event);FAILED_STATUS_TOKENS.has(status)||FAILED_STATUS_TOKENS.has(reason)?(worker.state="failed",worker.terminal_reason=reason||status||"failed"):(worker.state="complete",worker.terminal_reason=reason||status||"complete");break}case"message.delivered":case"message.acked":{!isTerminalState(worker.state)&&(worker.state==="not_started"||worker.state==="unknown")&&(worker.state="active");break}case"merge.succeeded":{worker.state="complete",worker.terminal_reason=worker.terminal_reason||"merge_succeeded";break}case"merge.failed":break;case"merge.dry_run":break;case"merge.pending_approval":break;default:break}return state.updated_at=msToIso(now),state}function applyRunLevelEvent(state,event,eventType){switch(eventType){case"gate.met":state.gates.gate_met=!0;break;case"ci.passed":state.gates.ci="passed";break;case"ci.failed":state.gates.ci="failed";break;case"git.pr_opened":state.gates.pr_opened=!0;break;case"merge.succeeded":case"merge.failed":case"merge.dry_run":case"merge.pending_approval":state.gates.merge=eventType.slice(6);break;default:break}}function classifyWorkerLiveness(worker,config,now){if(isTerminalState(worker.state))return"alive";let lastSignalMs=mostRecentSignalMs(worker);if(lastSignalMs===null)return"unknown";let elapsed=now-lastSignalMs;return elapsed>=config.liveness.dead_after_ms?"dead":elapsed>=config.liveness.stalled_after_ms?"stalled":elapsed>=config.liveness.quiet_after_ms?"quiet":"alive"}function mostRecentSignalMs(worker){let candidates=[isoToMs(worker.last_heartbeat_time),isoToMs(worker.last_event_time),isoToMs(worker.last_progress_time)].filter(v=>v!==null);return candidates.length===0?null:Math.max(...candidates)}function stateAnchorMs(worker){return worker.state==="active"||worker.state==="verifying"?mostRecentSignalMs(worker):isoToMs(worker.last_event_time)??isoToMs(worker.last_heartbeat_time)??isoToMs(worker.last_progress_time)}function applySupervisorHousekeeping(state,config,now){for(let worker of Object.values(state.workers)){if(worker.liveness=classifyWorkerLiveness(worker,config,now),isTerminalState(worker.state)||worker.state==="stalled")continue;let threshold=config.stall_thresholds_ms[worker.state],anchor=stateAnchorMs(worker);anchor!==null&&now-anchor>=threshold&&(worker.state="stalled")}return state.updated_at=msToIso(now),state}function isSupervisorRunTerminal(state){let workers=Object.values(state.workers);return workers.length===0||!state.roster_discovered?!1:workers.every(w=>isTerminalState(w.state))}function hasSupervisorGlobalTimeoutElapsed(state,now){let deadlineMs=isoToMs(state.global_deadline_at);return deadlineMs===null?!1:now>=deadlineMs}function compactWorker(worker){return{worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness,last_event_seq:worker.last_event_seq,last_event_time:worker.last_event_time,last_progress_time:worker.last_progress_time,last_heartbeat_time:worker.last_heartbeat_time,blocked_reason:worker.blocked_reason,terminal_reason:worker.terminal_reason}}function toSupervisorProjectionInput(state){let summary={kind:SUPERVISOR_SUMMARY_KIND,run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,workers:state.workers,gates:state.gates,latest_assessment:state.latest_assessment,escalations:state.escalations,started_at:state.started_at,updated_at:state.updated_at,global_deadline_at:state.global_deadline_at,roster_discovered:state.roster_discovered};return{run_id:state.run_id,status:state.status,last_seq:state.last_seq,last_event_time:state.last_event_time,active_workers:Object.values(state.workers).map(compactWorker),gates:state.gates,assessment:state.latest_assessment,summary}}var SUPERVISOR_SUMMARY_KIND,TERMINAL_STATES,PROGRESS_EVENT_TYPES,BLOCKED_STATUS_TOKENS,FAILED_STATUS_TOKENS,init_supervisor_state=__esm({"src/conductor/supervisor-state.ts"(){"use strict";SUPERVISOR_SUMMARY_KIND="supervisor_projection_summary",TERMINAL_STATES=new Set(["complete","failed"]);PROGRESS_EVENT_TYPES=new Set(["tool.intent","worktree.changed","git.commit_created"]),BLOCKED_STATUS_TOKENS=new Set(["blocked","waiting_for_input","needs_input"]),FAILED_STATUS_TOKENS=new Set(["failed","error","errored","aborted","cancelled","canceled"])}});function elapsedSinceSignal(worker,now){let candidates=[worker.last_event_time,worker.last_progress_time,worker.last_heartbeat_time].map(iso=>iso?Date.parse(iso):NaN).filter(v=>Number.isFinite(v));return candidates.length===0?0:Math.max(0,now-Math.max(...candidates))}function findSupervisorEscalationCandidates(state,config,now){let candidates=[];for(let worker of Object.values(state.workers)){if(worker.state==="complete"||worker.state==="failed")continue;let elapsed=elapsedSinceSignal(worker,now),baseContext={worker_id:worker.worker_id,ticket_key:worker.ticket_key,state:worker.state,liveness:worker.liveness};if(worker.liveness==="dead"){candidates.push({reason:"worker_dead",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});continue}switch(worker.state){case"not_started":worker.liveness!=="alive"&&candidates.push({reason:"worker_not_started",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"blocked":candidates.push({reason:"worker_blocked",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:{...baseContext,blocked_reason:worker.blocked_reason}});break;case"stalled":candidates.push({reason:"worker_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"candidate_done":worker.liveness!=="alive"&&candidates.push({reason:"candidate_done_stuck",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;case"verifying":worker.liveness==="stalled"&&candidates.push({reason:"verification_stalled",kind:ESCALATION_KIND,worker_id:worker.worker_id,state:worker.state,liveness:worker.liveness,elapsed_ms:elapsed,context:baseContext});break;default:break}}let deadlineMs=state.global_deadline_at?Date.parse(state.global_deadline_at):NaN;return Number.isFinite(deadlineMs)&&now>=deadlineMs&&candidates.push({reason:"global_timeout",kind:ESCALATION_KIND,worker_id:null,state:null,liveness:null,elapsed_ms:Math.max(0,now-deadlineMs),context:{run_id:state.run_id,deadline_at:state.global_deadline_at,worker_count:Object.keys(state.workers).length}}),candidates}function cooldownWindowFor(now,cooldownMs){let width=cooldownMs>0?cooldownMs:1;return String(Math.floor(now/width))}function shouldEmitEscalation(state,candidate,config,now){let cooldownWindow=cooldownWindowFor(now,config.escalation_cooldown_ms);return{emit:!state.escalations.some(record=>record.reason===candidate.reason&&(record.worker_id??null)===(candidate.worker_id??null)&&record.cooldown_window===cooldownWindow&&(record.outcome==="emitted"||record.outcome==="duplicate")),cooldown_window:cooldownWindow}}function recordEscalationResult(state,candidate,cooldownWindow,idempotencyKey,outcome2,now){let record={idempotency_key:idempotencyKey,worker_id:candidate.worker_id??null,reason:candidate.reason,kind:candidate.kind,cooldown_window:cooldownWindow,outcome:outcome2,recorded_at:new Date(now).toISOString()};return state.escalations.push(record),record}function formatElapsed(ms){let totalSeconds=Math.max(0,Math.floor(ms/1e3)),hours=Math.floor(totalSeconds/3600),minutes=Math.floor(totalSeconds%3600/60),seconds=totalSeconds%60;return hours>0?`${hours}h${minutes}m`:minutes>0?`${minutes}m`:`${seconds}s`}function formatEscalationForTerminal(runId,candidate){let worker=candidate.worker_id?` worker=${candidate.worker_id}`:"",stateBit=candidate.state?` state=${candidate.state}`:"",liveBit=candidate.liveness?` liveness=${candidate.liveness}`:"",elapsed=` elapsed=${formatElapsed(candidate.elapsed_ms)}`;return`[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`}var ESCALATION_KIND,init_supervisor_escalation=__esm({"src/conductor/supervisor-escalation.ts"(){"use strict";ESCALATION_KIND="escalation"}});function buildGateIdentity(gateName,configHash){let name=gateName.trim(),hash=typeof configHash=="string"?configHash.trim():"";return hash?`${name}@${hash.toLowerCase()}`:name}function makeMergeActionKey(repo,prNumber,headSha,gateIdentity){let r=normalizeRepoName(repo),pr=normalizePrNumber(prNumber),sha=normalizeSha(headSha),gate=(gateIdentity??"").trim();if(r===null||pr===null||sha===null||gate.length===0)throw new Error("invalid merge action key component");return`merge:${r}:${pr}:${sha}:${gate}`}var init_merge_identity=__esm({"src/conductor/merge-identity.ts"(){"use strict";init_git_ci_types()}});function isPlainObject9(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeEventHeadSha(value){if(typeof value!="string")return null;let trimmed=value.trim();return/^[0-9a-f]{7,40}$/i.test(trimmed)?trimmed.toLowerCase():null}function getRawEventDetails(event){let details=event.data?.details;return isPlainObject9(details)?details:null}function parseHeadObservation(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseMergeLifecycle(event){let details=getRawEventDetails(event);return{action_key:details&&typeof details.action_key=="string"&&details.action_key.trim().length>0?details.action_key.trim():null}}function parseGateMet(event){let details=getRawEventDetails(event);if(!details)return{head_sha:null,repo:null,pr_number:null,gate_name:null,config_hash:null,required_checks:[]};let gateName=typeof details.gate_name=="string"&&details.gate_name.trim().length>0?details.gate_name.trim():null,configHash=typeof details.config_hash=="string"&&details.config_hash.trim().length>0?details.config_hash.trim():null,ciCheckStatus=isPlainObject9(details.ci_check_status)?details.ci_check_status:null,requiredChecks=(Array.isArray(details.required_checks)?details.required_checks:ciCheckStatus&&Array.isArray(ciCheckStatus.required_checks)?ciCheckStatus.required_checks:[]).filter(c=>typeof c=="string"&&c.trim().length>0);return{head_sha:normalizeSha(details.head_sha),repo:normalizeRepoName(details.repo),pr_number:normalizePrNumber(details.pr_number),gate_name:gateName,config_hash:configHash,required_checks:requiredChecks}}function parseSpecReview(event){let details=getRawEventDetails(event);return{head_sha:details?normalizeEventHeadSha(details.head_sha):null}}function parseEmpty(){return EMPTY_DETAILS}function getEventDetails(event,expectedType){if(event.type!==expectedType)return null;let parser=EVENT_PARSERS[expectedType];return parser(event)}function getMergeIdentity(event){if(event.type!=="gate.met")return null;let details=getEventDetails(event,"gate.met");if(details===null)return null;let{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName}=details;if(repo===null||prNumber===null||headSha===null||gateName===null)return null;let gateIdentity=buildGateIdentity(gateName,details.config_hash),actionKey=makeMergeActionKey(repo,prNumber,headSha,gateIdentity);return{repo,pr_number:prNumber,head_sha:headSha,gate_name:gateName,config_hash:details.config_hash,required_checks:details.required_checks,gate_identity:gateIdentity,action_key:actionKey,gate_event:{id:typeof event.id=="string"?event.id:void 0,seq:typeof event.seq=="number"?event.seq:void 0,time:typeof event.time=="string"?event.time:void 0}}}var EMPTY_DETAILS,EVENT_PARSERS,init_event_accessors=__esm({"src/conductor/event-accessors.ts"(){"use strict";init_git_ci_types();init_merge_identity();EMPTY_DETAILS=Object.freeze({});EVENT_PARSERS={"run.started":parseEmpty,"run.heartbeat":parseEmpty,"run.stopped":parseEmpty,"agent.notification":parseEmpty,"tool.intent":parseEmpty,"worktree.changed":parseEmpty,"git.commit_created":parseEmpty,"git.pr_opened":parseHeadObservation,"ci.passed":parseHeadObservation,"ci.failed":parseHeadObservation,"gate.met":parseGateMet,"supervisor.assessment":parseEmpty,"message.sent":parseEmpty,"message.delivered":parseEmpty,"message.acked":parseEmpty,"merge.dry_run":parseMergeLifecycle,"merge.attempted":parseMergeLifecycle,"merge.succeeded":parseHeadObservation,"merge.failed":parseMergeLifecycle,"merge.conflict":parseHeadObservation,"merge.pending_approval":parseMergeLifecycle,"review.passed":parseHeadObservation,"review.changes_requested":parseHeadObservation,"spec_review.passed":parseSpecReview,"spec_review.changes_requested":parseSpecReview,"parse.triggered":parseEmpty,"parse.succeeded":parseEmpty,"parse.failed":parseEmpty}}});import{createHash as createHash6}from"node:crypto";function extractMergeActionIdentityFromGateEvent(event){return getMergeIdentity(event)}function makeMergeEventId(eventType,actionKey){let h=createHash6("sha256").update(`${eventType}:${actionKey}`).digest("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`}async function lookupMergeEventByActionKey(eventType,actionKey,deps){let db=await(deps.openDb??(()=>openReadonlyConductorDatabaseIfExists()))();if(!db)return!1;try{return db.prepare(`SELECT 1 FROM events
1763
1763
  WHERE type = ?
1764
1764
  AND json_extract(data_json, '$.details.action_key') = ?
1765
1765
  LIMIT 1`).get(eventType,actionKey)!==void 0}finally{db.close()}}async function hasTerminalMergeSucceeded(actionKey,deps={}){return lookupMergeEventByActionKey("merge.succeeded",actionKey,deps)}async function hasMergeDryRun(actionKey,deps={}){return lookupMergeEventByActionKey("merge.dry_run",actionKey,deps)}async function hasMergePendingApproval(actionKey,deps={}){return lookupMergeEventByActionKey("merge.pending_approval",actionKey,deps)}function isDuplicateConstraintError4(error){if(!error||typeof error!="object")return!1;let code=error.code;if(typeof code=="string"&&code.startsWith("SQLITE_CONSTRAINT"))return!0;let message=error.message;if(typeof message=="string"){let lowered=message.toLowerCase();if(lowered.includes("unique constraint")||lowered.includes("constraint failed"))return!0}return!1}async function emitMergeLedgerEventIfNew(input,deps={}){let emitEvent=deps.emitEvent??emitConductorEvent,eventId=makeMergeEventId(input.type,input.action_key),event={id:eventId,source:"conductor-supervisor",type:input.type,run_id:input.run_id??null,worker_id:input.worker_id??null,producer:"conductor-merge",observed_via:"supervisor",data:{summary:input.summary??`${input.type} ${input.action_key}`,status:input.status,reason:input.reason??void 0,details:input.details}};try{let result=await emitEvent(event);return{emitted:!0,event_id:eventId,event:result.event}}catch(error){if(isDuplicateConstraintError4(error))return{emitted:!1,reason:"duplicate"};throw error}}var init_merge_ledger=__esm({"src/conductor/merge-ledger.ts"(){"use strict";init_event_accessors();init_merge_identity();init_store()}});function buildMergeRequestFromGateEvent(identity){return{repo_name:identity.repo,pr_number:identity.pr_number,expected_head_sha:identity.head_sha,gate:{name:identity.gate_name,config_hash:identity.config_hash,required_checks:identity.required_checks},action_key:identity.action_key,gate_event:identity.gate_event}}function mapApiErrorReason(error){if(error instanceof ConductorBridgeApiError)switch(error.kind){case"timeout":return"api_timeout";case"network":return"api_network";case"unauthorized":return"api_unauthorized";case"server":case"http":return"api_unavailable";case"invalid-input":return"api_invalid"}return"api_network"}function resolveAttributionWorkerId(event,identity,resolve2){if(typeof event.worker_id=="string"&&event.worker_id.trim().length>0)return event.worker_id.trim();if(resolve2){let resolved=resolve2(event,identity);if(typeof resolved=="string"&&resolved.trim().length>0)return resolved.trim()}return null}async function processGateMetMerge(access2,event,deps={}){let extract=deps.extractIdentity??extractMergeActionIdentityFromGateEvent,checkTerminal=deps.hasTerminal??hasTerminalMergeSucceeded,checkDryRun=deps.hasDryRun??hasMergeDryRun,checkPendingApproval=deps.hasPendingApproval??hasMergePendingApproval,mergeFn=deps.merge??mergePullRequestForGate,identity=extract(event);if(!identity)return{processed:!1,reason:"ineligible"};let actionKey=identity.action_key,attributionWorkerId=resolveAttributionWorkerId(event,identity,deps.resolveWorkerIdForGateEvent);if(await checkTerminal(actionKey))return{processed:!1,reason:"already_succeeded"};let baseDetails={action_key:actionKey,repo:identity.repo,pr_number:identity.pr_number,expected_head_sha:identity.head_sha,gate:identity.gate_identity},response;try{response=await mergeFn(access2,buildMergeRequestFromGateEvent(identity))}catch(error){let reason=mapApiErrorReason(error);return await emitMergeLedgerEventIfNew({type:"merge.failed",action_key:actionKey,status:"failed",reason,details:baseDetails,run_id:event.run_id??null,worker_id:attributionWorkerId,summary:`merge.failed ${reason}`},{emitEvent:deps.emitEvent}),{processed:!0,outcome:"api_error",reason}}let emitted=[];for(let ledgerEvent of response.ledger_events??[]){if(ledgerEvent.type==="merge.dry_run"&&await checkDryRun(actionKey)){emitted.push({type:ledgerEvent.type,emitted:!1});continue}if(ledgerEvent.type==="merge.pending_approval"&&await checkPendingApproval(actionKey)){emitted.push({type:ledgerEvent.type,emitted:!1});continue}let result=await emitMergeLedgerEventIfNew({type:ledgerEvent.type,action_key:actionKey,status:ledgerEvent.status,reason:ledgerEvent.reason??null,details:ledgerEvent.details??baseDetails,run_id:event.run_id??null,worker_id:attributionWorkerId},{emitEvent:deps.emitEvent});emitted.push({type:ledgerEvent.type,emitted:result.emitted})}return{processed:!0,outcome:response.status,emitted}}var init_supervisor_merge=__esm({"src/conductor/supervisor-merge.ts"(){"use strict";init_bridge_api_client();init_merge_ledger()}});async function dispatchSupervisorNotification(epicRunId,candidate,assessment,idempotencyKey){let result=await resolveConductorBridgeApiAccess();if(!result.ok)return;let{access:access2}=result,url=buildConductorJiraUrl(access2.baseUrl,`/epic-runs/${encodeURIComponent(epicRunId)}/notifications`),payload={repo_name:access2.repoName,idempotency_key:idempotencyKey,summary:{reason:candidate.reason,kind:candidate.kind,worker_id:candidate.worker_id??null,elapsed_ms:candidate.elapsed_ms,ticket_key:candidate.context?.ticket_key??null,classification:assessment.classification}},headers={"X-API-Key":access2.apiKey,"Content-Type":"application/json"};try{await fetchConductorJsonPostWithTimeout(url,headers,JSON.stringify(payload),CONDUCTOR_FETCH_TIMEOUT_MS,fetch)}catch{}}var init_supervisor_notification=__esm({"src/conductor/supervisor-notification.ts"(){"use strict";init_bridge_api_client()}});var supervisor_runtime_exports={};__export(supervisor_runtime_exports,{runSupervisor:()=>runSupervisor});function deterministicAssessment(candidate){return{classification:"stuck",confidence:1,reason:candidate.reason}}function terminalStatus(state){return Object.values(state.workers).some(w=>w.state==="failed")?"failed":"complete"}async function processEscalations(state,config,deps){let now=deps.now(),candidates=findSupervisorEscalationCandidates(state,config,now);for(let candidate of candidates){let decision=shouldEmitEscalation(state,candidate,config,now);if(!decision.emit)continue;let idempotency={run_id:state.run_id,worker_id:candidate.worker_id,reason:candidate.reason,kind:candidate.kind,cooldown_window:decision.cooldown_window},idempotencyKey=makeSupervisorIdempotencyKey(idempotency),assessment=deterministicAssessment(candidate);state.latest_assessment=assessment;let outcome2="skipped";try{outcome2=(await deps.emitAssessment({run_id:state.run_id,worker_id:candidate.worker_id,assessment,details:{elapsed_ms:candidate.elapsed_ms,...candidate.context},idempotency})).emitted?"emitted":"duplicate"}catch{outcome2="skipped"}if(recordEscalationResult(state,candidate,decision.cooldown_window,idempotencyKey,outcome2,now),(outcome2==="emitted"||outcome2==="duplicate")&&candidate.worker_id)try{await sendSupervisorEscalationWorkerMessageIfNew(candidate,assessment,state,{sendMessage:deps.sendWorkerMessage})}catch{}if(outcome2==="emitted"&&(deps.log(formatEscalationForTerminal(state.run_id,candidate)),deps.dispatchNotification))try{await deps.dispatchNotification(state.run_id,candidate,assessment,idempotencyKey)}catch{}}}async function runSupervisor(options,deps={}){let runId=typeof options.run_id=="string"?options.run_id.trim():"";if(runId.length===0)throw new ConductorValidationError("Supervisor requires exactly one non-empty --run-id.");let config=options.config??resolveSupervisorConfig(options.overrides??{}),now=deps.now??(()=>Date.now()),log=deps.log??(m=>process.stdout.write(`${m}
@@ -4565,7 +4565,7 @@ closed:
4565
4565
  ## Return
4566
4566
 
4567
4567
  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.
4568
- `};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## Install\n\nFrom your **project root**, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\nThat is the whole setup command. It works whether or not you already have a Bridge\naccount \u2014 it will ask.\n\n> **npm shows a different command in its sidebar. Ignore it.** The\n> `npm i @bridge_gpt/mcp-server` box on this page is generated by npm from the\n> package name; it is not the setup command, and no packaging option lets the\n> publisher change or remove it. Installing the package that way does not connect\n> anything. Use the command above.\n\n**What it changes in your project**\n\n- Adds slash commands and agent definitions for your editor (`.claude/commands/`,\n `.cursor/commands/`, and the equivalents your editor uses).\n- Registers a `bridge-api` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates `.bridge/` for your project manifest and pipeline definitions.\n- Writes nothing outside your project root except your Bridge credential, which is\n stored for you so you never have to paste it again.\n\n**Prerequisites**\n\n- **Node.js 18 or newer** (`node --version`), which is what provides `npx`.\n- **A project directory** \u2014 run the command from the folder your editor opens, the\n one containing `package.json` or your repository root.\n- **An MCP-capable editor**: Claude Code, GitHub Copilot, Cursor, Windsurf, or\n OpenAI Codex.\n- **No Bridge account needed.** The installer can create one for you from just an\n email address.\n\n## Contents\n\n- [Install](#install)\n- [Installing, step by step](#installing-step-by-step)\n- [What to expect](#what-to-expect)\n- [Troubleshooting](#troubleshooting)\n- [Quick start details](#quick-start-details)\n- [Usage Documentation](#usage-documentation)\n - [Regularly useful](#regularly-useful)\n - [Occasionally useful](#occasionally-useful)\n - [Now and then](#now-and-then)\n - [Workflow commands](#workflow-commands)\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## Installing, step by step\n\n**1. Open a terminal in your project root.** This matters: the installer writes\nyour slash commands and MCP config relative to the directory you run it from. If\nyou run it in your home directory, your editor will not find any of it.\n\n**2. Run the command.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\n**3. Answer the sign-in question.** On a first run it asks whether you already have\na token:\n\n```\n1. Yes, I have received a token\n2. No, I need one\n```\n\n- Choose **2** if you have nothing yet. It asks for your email address and a name\n for your new Bridge project, then creates both for you.\n- Choose **1** if someone gave you a token \u2014 either a Bridge API key or an invite\n code. Paste it at the hidden prompt; you do not have to say which kind it is,\n because the installer recognizes it. Nothing is echoed as you type.\n\nThere is no default answer, so pressing Enter alone selects nothing. If you would\nrather not be asked, pass the answer up front instead \u2014 see\n[Choosing how you sign in](#quick-start-details).\n\n**4. Pick which editors to configure.** The installer detects the MCP hosts on your\nmachine and asks which ones to set up. Pick every editor you actually use for this\nproject; you can re-run the command later to add another.\n\n**5. Reload your MCP host.** Editors read their MCP configuration at startup, so a\nfreshly written config is not live until you reload. Restart the editor, or use its\n"reload MCP servers" action. In Claude Code you will also be asked to trust the\nproject\'s `.mcp.json` the first time.\n\n**6. Finish in the agent session the installer opens.** The last thing the\ninstaller does is open a fresh agent session running `/install-bridge`, which reads\nyour codebase, fills in the remaining project settings, and prints a short report\nof what Bridge can help with. Let it finish.\n\n**7. Follow the next step the session shows you, if it shows one.** The installer\nasks the server what should happen next and shows that command only when there is\none to show \u2014 most often `/learn-repository`, which it recommends when the project\nstill needs its architecture, testing, review, and correctness standards documented\nand your key can run it. The installer deliberately does not run it for you. Those\nstandards are what make every later plan, critique, and review match how your\nproject actually works, and they only need to be gathered once per project \u2014 the\nresult is shared with everyone on the team. If the session shows no next step,\nthere is nothing for you to run.\n\nWant to see what would happen without changing anything? Add `--dry-run`.\n\n## What to expect\n\n**Files that appear in your project**\n\n| Path | What it is | Commit it? |\n|---|---|---|\n| `.claude/commands/`, `.cursor/commands/` | The slash commands your editor runs | Yes |\n| `.claude/agents/` and editor equivalents | Agent definitions used by those commands | Yes |\n| `.bridge/config` | Your project manifest \u2014 the repository name and which MCP targets to provision. Deliberately secret-free | Yes |\n| `.bridge/pipelines/`, `.bridge/instructions/` | Editable pipeline definitions | Yes |\n| `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` | MCP registrations for your editor. These can carry your key, so the installer git-ignores them | No |\n\nThe installer tells you which of these are safe to commit and never recommends\ncommitting anything that can hold a credential.\n\n**Prompts you will see.** A sign-in question on a first bare run, a hidden prompt\nfor a token (or a visible one for an email), a project name for a brand-new\nproject, and a picker for which editors to configure. Nothing else prompts.\n\n**A fresh agent session opens at the end.** This is expected \u2014 it is how the\ninstaller finishes configuring the project. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Your key is remembered.** You paste a credential at most once. Later runs, and\nthe tooling that spawns its own shells, find it on their own.\n\n**A next step, when the project needs one.** The session closes with whatever\ncommand the server says comes next, and stays quiet when there is nothing to\nrecommend. `/learn-repository` is the usual one: it is recommended when the project\nstill needs its conventions documented and your key can run it. It is never\nautomatic \u2014 until someone runs it, Bridge\'s agents work from your code alone rather\nthan from your project\'s documented conventions.\n\n**Indexing happens on its own.** There is no "index my repository?" question. Once\nyour project has the settings it needs, indexing starts server-side. You never have\nto ask for it.\n\n## Troubleshooting\n\n**"My editor doesn\'t see any Bridge tools."** Two usual causes. First, the config\nwas written somewhere your editor is not looking \u2014 re-run the installer from the\ndirectory your editor actually opens, and check that a `bridge-api` entry exists in\nthat project\'s MCP config. Second, the editor has not been reloaded since the file\nwas written; restart it. In Claude Code, also confirm you accepted the trust prompt\nfor the project\'s `.mcp.json`.\n\n**"I ran it in the wrong folder."** Nothing is broken. Delete the `.bridge/`,\n`.claude/`, and `.mcp.json` entries that were created there and re-run the command\nfrom the right directory.\n\n**"It seems to hang with no output."** If you ran the bare command\n(`npx -y @bridge_gpt/mcp-server`) with no subcommand, you started the MCP *server*,\nnot the installer. It is waiting for an editor to connect over stdio, which is\nexactly what it should do when your editor launches it \u2014 but from a terminal it\nlooks like a hang. It prints a line saying so. Press Ctrl-C and run\n`npx -y @bridge_gpt/mcp-server install` instead. The explicit spelling\n`npx -y @bridge_gpt/mcp-server serve` starts the server on purpose.\n\n**"It can\'t reach Bridge" or "my key was rejected."** The installer checks\nconnectivity before it saves anything, so a failure here has changed nothing. A\nrejected key means the credential is not valid for that project \u2014 check the project\nname you gave, and generate a fresh key on the Bridge web UI\'s **Security** page if\nneeded. A network failure usually means a proxy or VPN is in the way.\n\n**"Which repository name should I use?"** The one registered with Bridge. If you\nhave an existing key, the installer usually resolves it for you; when it cannot, it\nasks, and `--repo <name>` answers it up front.\n\n**Still stuck? Ask the installer to diagnose itself.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server doctor\n```\n\n`doctor` is strictly read-only. It reports what it found \u2014 configs, registrations,\ncredential availability, prerequisites \u2014 and changes nothing.\n\n## Quick start details\n\n<details>\n<summary><strong>Choosing how you sign in</strong></summary>\n\nThree routes lead to the same place. The interactive question above picks one for\nyou; these flags pick it up front and skip the question entirely.\n\n**No account yet \u2014 sign up with an email.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --email you@example.com\n```\n\nCreates a brand-new Bridge project for that address and your first admin key in one\ncommand. No account, no key, and no invite needed beforehand. The address labels\nyour new workspace and may receive a setup message; delivery is best-effort, so\nnothing waits on it. The email is visible as you type (it is not a secret) and is\nnever written to a log. This is the same route as answering **2** at the prompt.\n\n**You were sent an invite code.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --invite\n```\n\nRedeems the invite, creates your project, and mints your first admin key. Run it\n*without* a value, as shown: the installer then asks for the code at a hidden\nprompt, so the code never lands in your shell history. `--invite <code>` and the\n`BAPI_INVITE` environment variable exist for scripting, but both expose the code to\nyour shell history and to the process list.\n\n**Your team already has a project and gave you an API key.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --api-key <key>\n```\n\nOr omit the flag and paste the key at the hidden prompt. Generate a key on the\nBridge web UI\'s **Security** page (**Create New Key**, role **Admin**) and copy it\nimmediately \u2014 it is shown once. `BAPI_API_KEY` works too.\n\nIf you paste an invite code where a key was expected, or a key where an invite was\nexpected, the installer recognizes the mismatch and tells you before anything is\ncreated or spent.\n\n</details>\n\n<details>\n<summary><strong>Installer flags</strong></summary>\n\n| Flag | What it does |\n|---|---|\n| `--email <addr>` | Sign up for a new Bridge project with just an email address |\n| `--invite [code]` | Redeem an invite code. Omit the value for the hidden prompt (recommended) |\n| `--api-key <key>` | Use an existing Bridge API key |\n| `--repo <name>` | Name the registered repository instead of resolving or asking for it |\n| `--tools <list>` | Configure specific MCP hosts without the picker (e.g. `claude-code,cursor`) |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step (default `claude`) |\n| `--dry-run` | Preview every step without writing, contacting Bridge, or opening anything |\n| `--force` | Overwrite an existing stored key without asking |\n| `-h`, `--help` | Full usage |\n\n`--email`, `--invite`, and `--api-key` are mutually exclusive \u2014 each names a\ndifferent way to arrive, and the installer will not guess between them.\n\n</details>\n\n<details>\n<summary><strong>Setting up an MCP host by hand</strong></summary>\n\nThe installer configures your editors for you. Do this only if you would rather\nwrite the config yourself, or if you use a host it cannot write automatically.\n\nScaffold the project files without configuring anything:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\nThen add a `bridge-api` entry to your host\'s MCP config, filling in your repository\nname and API key. Add `"serve"` as the last launcher argument, as shown \u2014 it is the\nexplicit way to say "start the MCP server."\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server", "serve"],\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", "serve"],\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", "serve"],\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", "serve"],\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", "serve"]\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](#environment-variables)).\n</details>\n\nAfter saving, reload your editor and ask your assistant to call the `ping` tool to\nconfirm the connection.\n\nAn entry with no trailing `serve` still starts the server \u2014 bare invocation means\n"server" permanently, and nothing rewrites an existing config to add the token.\n\n</details>\n\n<details>\n<summary><strong>Upgrading Bridge</strong></summary>\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest --upgrade\n```\n\n`upgrade` fetches the latest published version, refreshes your scaffolded slash\ncommands, agents, and pipelines, updates the version pin in your MCP config, and\nopens a session so you can reconnect. It is also available as the\n`/upgrade-bridge` slash command.\n\nUse the `@latest` form. It applies to the short-lived *upgrader* process: without\nit, npx may reuse a cached older copy of the package and "upgrade" you with the\nbuild you are trying to replace. The exact `MAJOR.MINOR.PATCH` pin the upgrader\nwrites into your MCP config is deliberately different \u2014 host configs stay pinned\nto an exact release so a project\'s server is reproducible.\n\n`upgrade` reports **per config file**, because a project can have several\n(`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) and they can disagree:\n\n```\nLauncher pins:\n .mcp.json: 0.2.16 -> 0.2.36\n .cursor/mcp.json: already 0.2.36\n```\n\nWhen every applicable launcher pin was already at the target, it prints\n`Already up-to-date.` \u2014 that status comes from comparing your configs, not from\nthe version of the CLI process. A non-zero exit means the upgrade did **not**\nconverge (an unreadable config, a launcher carrying a version range the upgrader\nmust not rewrite, a competing local install it could not remove, or a pin that\nfailed post-write verification); nothing is reported as complete in that case.\n\nThe server checks for updates on startup. The check is cached for a day and never\nblocks startup. When a newer version is known, it surfaces in two places you do\nnot have to go looking for: a one-line warning on the server\'s **stderr**, and a\nshort advisory attached to the ordinary `tools/list` response so the agent in the\nsession can see that some tools may be missing or renamed in the older build.\nNeither requires calling `ping` or `doctor`.\n\nRe-running `install` on an already-configured project is safe: it refreshes the\nscaffolded files without overwriting your stored credential unless you pass\n`--force`.\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**, **how to use it**, and its **flags**. 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<!-- The three tier sections below are GENERATED from api/library/config/mcp_tool_catalog.json,\n the authoritative tool catalog, by scripts/sync_mcp_server_readme.py. Edit the curated\n metadata in scripts/sync_mcp_tool_catalog.py \u2014 never the JSON artifact and never the\n text between the markers. Generation order is: sync_mcp_tool_catalog.py, then\n sync_mcp_server_readme.py, then `cd mcp_server && npm run build` (which bundles this\n file into readme.generated.ts, served as the MCP resource bridge-api://readme).\n Everything outside the marker pair \u2014 including the sections below it \u2014 is hand-written. -->\n\n<!-- BEGIN GENERATED: mcp-tool-documentation (managed by scripts/sync_mcp_server_readme.py \u2014 DO NOT EDIT BY HAND) -->\n### Regularly useful\n\nThe tools worth knowing for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions and a critique plus an alternate-model second opinion, then evaluates the findings and produces a decision page for accepting or rejecting them.\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). For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every `/review-ticket` flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.\n- **Flags:** `--auto` auto-accept findings and skip the approval gates \xB7 `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the difficulty-adaptive review policy decide.\n\n**2. Council**\n- **What it does:** Fans your problem out to two different models and returns their approaches, in technical, design, discovery, or general mode.\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 still needs figuring out before a real ticket exists, general for a quick brief-driven pass 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." 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 so we can collect the questions stakeholders need to answer first."\n- **Flags:** `mode` selects one of four modes: `technical` (the default \u2014 implementation/architecture approaches), `design` (UI/UX and visual direction), `discovery` (stakeholder discovery questions, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), and `general` (brief-driven ideation from your task description alone). `technical` and `discovery` are codebase-grounded and need an indexed repository; `general` needs no code index at all, so it works immediately after install. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n\n**3. Get Council**\n- **What it does:** Retrieves a council that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When a council was already run and you want to reread it without paying to regenerate it.\n- **How to use it:** Ask your agent to pull up the council already generated for the ticket.\n- **Flags:** None.\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 \u2014 libraries, best practices, standards \u2014 that you do not already have.\n- **How to use it:** `/bridge-research <question>`\n- **Flags:** None.\n\n**5. Get Deep Research**\n- **What it does:** Retrieves a research report that was already generated.\n- **When it\'s useful:** (Architecture | Refinement) When the report already exists and you want to reread it without re-running the research.\n- **How to use it:** Ask your agent to retrieve the research report already generated.\n- **Flags:** None.\n\n**6. Upload Ticket**\n- **What it does:** Creates a real Jira issue from a drafted ticket, including child tickets under an epic; your agent should confirm with you before creating it.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into your tracker so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket; it should confirm with you before creating the live issue.\n- **Flags:** Name the issue type (Bug / Story / Task / Epic) and, for a child ticket under an epic, the parent key.\n\n### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket that references real files in your codebase.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before \u2014 or instead of \u2014 auto-implementing it.\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. Get Plan**\n- **What it does:** Retrieves an implementation plan that was already generated for a ticket.\n- **When it\'s useful:** (Implementation) When the plan already exists and you want to read it without regenerating it.\n- **How to use it:** Ask your agent to fetch the implementation plan already generated for the ticket.\n- **Flags:** None.\n\n**3. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket, or debugging guidance when the ticket is a bug.\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`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Get Clarifying Questions**\n- **What it does:** Retrieves clarifying questions that were already generated for a ticket.\n- **When it\'s useful:** (Refinement) When the questions were already generated and you want to reread them rather than pay to regenerate them.\n- **How to use it:** Ask your agent for the clarifying questions already generated for the ticket.\n- **Flags:** None.\n\n**5. Critique Ticket**\n- **What it does:** Critiques a ticket against your project\'s standards and lists the deviations and improvements it found.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before anyone works it.\n- **How to use it:** `/critique-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**6. Get Ticket Critique**\n- **What it does:** Retrieves a ticket critique that was already generated.\n- **When it\'s useful:** (Refinement) When the critique already exists and you want to reread its findings without regenerating it.\n- **How to use it:** Ask your agent for the critique already generated for the ticket.\n- **Flags:** None.\n\n**7. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a technical design document, a functional spec, or a product requirements document.\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**8. Get Doc**\n- **What it does:** Retrieves a design document that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When the document already exists and you want to reread it without regenerating it.\n- **How to use it:** Ask your agent for the ticket\'s design document, naming which type you want.\n- **Flags:** None.\n\n**9. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family, without saving an artifact.\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 production."\n- **Flags:** Pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**10. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model, spending 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- **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**11. Request PRD**\n- **What it does:** Generates a product requirements document for a ticket covering the problem, the goals, and the success metrics.\n- **When it\'s useful:** (Architecture | Refinement) When a piece of work needs its problem, goals, and success metrics written down before anyone designs a solution.\n- **How to use it:** `/create-doc BAPI-123 --doc-type prd`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**12. Get PRD**\n- **What it does:** Retrieves a product requirements document that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When the requirements document already exists and you want to reread it without regenerating it.\n- **How to use it:** Ask your agent to retrieve the ticket\'s product requirements document.\n- **Flags:** None.\n\n**13. Full Automation**\n- **What it does:** Drives the whole chain from a raw idea through tickets and reviews to implementation sessions.\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 it creates tickets, spawns worktrees, and carries scheduling flags free text cannot).\n- **Flags:** `--require-approval` re-enable the approval gates; the chain runs end to end by default \xB7 `--max-children <n>` cap how many child tickets an epic decomposes into.\n\n**14. Resume Full Automation**\n- **What it does:** Resumes a full-automation chain that was started earlier.\n- **When it\'s useful:** (Automation) When an earlier chain stopped at an approval gate or was interrupted, and you want it continued rather than restarted.\n- **How to use it:** Ask your agent to resume the full-automation chain, naming the run to continue.\n- **Flags:** None.\n\n**15. Update Ticket Description**\n- **What it does:** Rewrites a ticket\'s description with AI, using the ticket\'s own content and its reference material. A rewrite that changes more than 60% of the description is held for review instead of applied.\n- **When it\'s useful:** (Refinement) When a ticket has accumulated comments, attachments, or links and its description no longer reflects them.\n- **How to use it:** Ask your agent \u2014 "Update the description for BAPI-123."\n- **Flags:** None. Poll the ticket\'s state for the outcome; if the update was held for review, read the proposal instead of applying it blind.\n\n### Now and then\n\nUseful once in a while.\n\n**1. Reimplement Ticket**\n- **What it does:** Gathers the context and attachments added since the last pass so a targeted follow-up change can be made.\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- **Flags:** None.\n\n**2. Get Reimplement Context**\n- **What it does:** Retrieves the follow-up context that was already gathered for a ticket.\n- **When it\'s useful:** (Implementation) When the follow-up context was already gathered and you want to read it without gathering it again.\n- **How to use it:** Ask your agent for the follow-up context already gathered for the ticket.\n- **Flags:** None.\n\n**3. Update Ticket**\n- **What it does:** Rewrites a ticket\'s description, fully replacing what is there today.\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 it fully overwrites the live description, which is hard to reverse).\n- **Flags:** None.\n\n**4. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description.\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- **Flags:** None.\n\n**5. Search Tickets**\n- **What it does:** Searches across the tickets in your project.\n- **When it\'s useful:** (Refinement) When you need to find tickets by project, status, or wording rather than by key.\n- **How to use it:** Ask your agent \u2014 "Search our project for open tickets mentioning rate limiting."\n- **Flags:** Narrow the search by project, status, issue type, or free text.\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a ticket.\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 is rotated."\n- **Flags:** A long comment can be attached as a file instead of inlined.\n\n**7. Read Comments**\n- **What it does:** Reads the comment thread on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When the discussion on a ticket matters and you want the agent to read it before acting.\n- **How to use it:** Ask your agent \u2014 "Read the comments on BAPI-123 and summarize what was decided."\n- **Flags:** None.\n\n**8. Ticket Attachments**\n- **What it does:** Downloads files from a ticket to your disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files or 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," or "Attach build-log.txt to BAPI-123."\n- **Flags:** Choose the direction (download from the ticket, or upload to it) and, for a download, where the files should land.\n\n**9. Estimate Ticket**\n- **What it does:** Estimates the development effort for one ticket. Use Estimate Epic instead for a whole epic or a named group of tickets.\n- **When it\'s useful:** (Refinement) When you need a size for a single ticket before committing to it.\n- **How to use it:** Ask your agent \u2014 "Estimate BAPI-123."\n- **Flags:** Ask for a fresh estimate to regenerate rather than reuse a stored one.\n\n**10. Estimate Epic**\n- **What it does:** Estimates an epic, or an explicit group of tickets you name.\n- **When it\'s useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.\n- **How to use it:** `/estimate-epic BAPI-123`\n- **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.\n<!-- END GENERATED: mcp-tool-documentation -->\n\n### Workflow commands\n\nSlash commands that drive several tools at once. They are agent workflows rather than single MCP tools, so they are documented here by hand.\n\n**1. 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 \xB7 `--rounds=1|2` forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override.\n\n**2. 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**3. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket 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` 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\n**4. 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>` \u2014 or ask your agent, *"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**5. 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- **Flags:** None.\n\n**6. 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- **Flags:** None.\n\n**7. 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- **Flags:** None.\n\n**8. 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`\n- **Flags:** `--unit-only` skip the E2E suite \xB7 `--skip-e2e` same, phrased the other way.\n\n**9. 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- **Flags:** None.\n\n**10. 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- **Flags:** None.\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. An oversized response is saved in full to `BAPI_DOCS_DIR/sfcc/` and replaced by a parseable JSON descriptor \u2014 `truncated: true`, the `saved_path` it was written to, and the `page` metadata (`returned`, `total` when OCAPI supplied one, `has_more`) \u2014 so the collection metadata survives even though the data itself is on disk. If that save fails, the complete payload is returned inline instead, still as parseable JSON.\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 system object type\'s definition.\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 authoritative tool catalog covers **92 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`, `get_ticket_state_tree` (live repo-wide lifecycle + dependency tree; read-only, no mutation parameter)\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';import{readdir,readFile}from"fs/promises";import path 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,base.instruction_file=step.instruction_file}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. Recipes are re-entrant: a recovery run may begin again at step 1, and a step whose tool reports a server-side reuse (e.g. reused: true) has completed successfully \u2014 treat that as the expected fast path, not a failure, and continue. A step can succeed (no on_error handling applies) while its own returned content is a JSON envelope shaped like error: "GATEWAY_TIMEOUT", status: 504, and a recovery_get field \u2014 recognize that shape and read it as "server-side processing may still be running", not as a failure or an invitation to retry. Poll the named retrieval tool (or the recovery_get URL) with the same artifact identifier until a terminal response is reached, and never reissue the original request tool. Only fall back to on_error handling if that retrieval itself terminally fails. 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 readFile(path.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 readFile(path.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:
4568
+ `};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## Install\n\nFrom your **project root**, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\nThat is the whole setup command. It works whether or not you already have a Bridge\naccount \u2014 it will ask.\n\n> **npm shows a different command in its sidebar. Ignore it.** The\n> `npm i @bridge_gpt/mcp-server` box on this page is generated by npm from the\n> package name; it is not the setup command, and no packaging option lets the\n> publisher change or remove it. Installing the package that way does not connect\n> anything. Use the command above.\n\n**What it changes in your project**\n\n- Adds slash commands and agent definitions for your editor (`.claude/commands/`,\n `.cursor/commands/`, and the equivalents your editor uses).\n- Registers a `bridge-api` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates `.bridge/` for your project manifest and pipeline definitions.\n- Writes nothing outside your project root except your Bridge credential, which is\n stored for you so you never have to paste it again.\n\n**Prerequisites**\n\n- **Node.js 18 or newer** (`node --version`), which is what provides `npx`.\n- **A project directory** \u2014 run the command from the folder your editor opens, the\n one containing `package.json` or your repository root.\n- **An MCP-capable editor**: Claude Code, GitHub Copilot, Cursor, Windsurf, or\n OpenAI Codex.\n- **No Bridge account needed.** The installer can create one for you from just an\n email address.\n\n## Contents\n\n- [Install](#install)\n- [Installing, step by step](#installing-step-by-step)\n- [What to expect](#what-to-expect)\n- [Troubleshooting](#troubleshooting)\n- [Quick start details](#quick-start-details)\n- [Usage Documentation](#usage-documentation)\n - [Regularly useful](#regularly-useful)\n - [Occasionally useful](#occasionally-useful)\n - [Now and then](#now-and-then)\n - [Workflow commands](#workflow-commands)\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## Installing, step by step\n\n**1. Open a terminal in your project root.** This matters: the installer writes\nyour slash commands and MCP config relative to the directory you run it from. If\nyou run it in your home directory, your editor will not find any of it.\n\n**2. Run the command.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install\n```\n\n**3. Answer the sign-in question.** On a first run it asks whether you already have\na token:\n\n```\n1. Yes, I have received a token\n2. No, I need one\n```\n\n- Choose **2** if you have nothing yet. It asks for your email address and a name\n for your new Bridge project, then creates both for you.\n- Choose **1** if someone gave you a token \u2014 either a Bridge API key or an invite\n code. Paste it at the hidden prompt; you do not have to say which kind it is,\n because the installer recognizes it. Nothing is echoed as you type.\n\nThere is no default answer, so pressing Enter alone selects nothing. If you would\nrather not be asked, pass the answer up front instead \u2014 see\n[Choosing how you sign in](#quick-start-details).\n\n**4. Pick which editors to configure.** The installer detects the MCP hosts on your\nmachine and asks which ones to set up. Pick every editor you actually use for this\nproject; you can re-run the command later to add another.\n\n**5. Reload your MCP host.** Editors read their MCP configuration at startup, so a\nfreshly written config is not live until you reload. Restart the editor, or use its\n"reload MCP servers" action. In Claude Code you will also be asked to trust the\nproject\'s `.mcp.json` the first time.\n\n**6. Finish in the agent session the installer opens.** The last thing the\ninstaller does is open a fresh agent session running `/install-bridge`, which reads\nyour codebase, fills in the remaining project settings, and prints a short report\nof what Bridge can help with. Let it finish.\n\n**7. Follow the next step the session shows you, if it shows one.** The installer\nasks the server what should happen next and shows that command only when there is\none to show \u2014 most often `/learn-repository`, which it recommends when the project\nstill needs its architecture, testing, review, and correctness standards documented\nand your key can run it. The installer deliberately does not run it for you. Those\nstandards are what make every later plan, critique, and review match how your\nproject actually works, and they only need to be gathered once per project \u2014 the\nresult is shared with everyone on the team. If the session shows no next step,\nthere is nothing for you to run.\n\nWant to see what would happen without changing anything? Add `--dry-run`.\n\n## What to expect\n\n**Files that appear in your project**\n\n| Path | What it is | Commit it? |\n|---|---|---|\n| `.claude/commands/`, `.cursor/commands/` | The slash commands your editor runs | Yes |\n| `.claude/agents/` and editor equivalents | Agent definitions used by those commands | Yes |\n| `.bridge/config` | Your project manifest \u2014 the repository name and which MCP targets to provision. Deliberately secret-free | Yes |\n| `.bridge/pipelines/`, `.bridge/instructions/` | Editable pipeline definitions | Yes |\n| `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` | MCP registrations for your editor. These can carry your key, so the installer git-ignores them | No |\n\nThe installer tells you which of these are safe to commit and never recommends\ncommitting anything that can hold a credential.\n\n**Prompts you will see.** A sign-in question on a first bare run, a hidden prompt\nfor a token (or a visible one for an email), a project name for a brand-new\nproject, and a picker for which editors to configure. Nothing else prompts.\n\n**A fresh agent session opens at the end.** This is expected \u2014 it is how the\ninstaller finishes configuring the project. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Your key is remembered.** You paste a credential at most once. Later runs, and\nthe tooling that spawns its own shells, find it on their own.\n\n**A next step, when the project needs one.** The session closes with whatever\ncommand the server says comes next, and stays quiet when there is nothing to\nrecommend. `/learn-repository` is the usual one: it is recommended when the project\nstill needs its conventions documented and your key can run it. It is never\nautomatic \u2014 until someone runs it, Bridge\'s agents work from your code alone rather\nthan from your project\'s documented conventions.\n\n**Indexing happens on its own.** There is no "index my repository?" question. Once\nyour project has the settings it needs, indexing starts server-side. You never have\nto ask for it.\n\n## Troubleshooting\n\n**"My editor doesn\'t see any Bridge tools."** Two usual causes. First, the config\nwas written somewhere your editor is not looking \u2014 re-run the installer from the\ndirectory your editor actually opens, and check that a `bridge-api` entry exists in\nthat project\'s MCP config. Second, the editor has not been reloaded since the file\nwas written; restart it. In Claude Code, also confirm you accepted the trust prompt\nfor the project\'s `.mcp.json`.\n\n**"I ran it in the wrong folder."** Nothing is broken. Delete the `.bridge/`,\n`.claude/`, and `.mcp.json` entries that were created there and re-run the command\nfrom the right directory.\n\n**"It seems to hang with no output."** If you ran the bare command\n(`npx -y @bridge_gpt/mcp-server`) with no subcommand, you started the MCP *server*,\nnot the installer. It is waiting for an editor to connect over stdio, which is\nexactly what it should do when your editor launches it \u2014 but from a terminal it\nlooks like a hang. It prints a line saying so. Press Ctrl-C and run\n`npx -y @bridge_gpt/mcp-server install` instead. The explicit spelling\n`npx -y @bridge_gpt/mcp-server serve` starts the server on purpose.\n\n**"It can\'t reach Bridge" or "my key was rejected."** The installer checks\nconnectivity before it saves anything, so a failure here has changed nothing. A\nrejected key means the credential is not valid for that project \u2014 check the project\nname you gave, and generate a fresh key on the Bridge web UI\'s **Security** page if\nneeded. A network failure usually means a proxy or VPN is in the way.\n\n**"Which repository name should I use?"** The one registered with Bridge. If you\nhave an existing key, the installer usually resolves it for you; when it cannot, it\nasks, and `--repo <name>` answers it up front.\n\n**Still stuck? Ask the installer to diagnose itself.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server doctor\n```\n\n`doctor` is strictly read-only. It reports what it found \u2014 configs, registrations,\ncredential availability, prerequisites \u2014 and changes nothing.\n\n## Quick start details\n\n<details>\n<summary><strong>Choosing how you sign in</strong></summary>\n\nThree routes lead to the same place. The interactive question above picks one for\nyou; these flags pick it up front and skip the question entirely.\n\n**No account yet \u2014 sign up with an email.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --email you@example.com\n```\n\nCreates a brand-new Bridge project for that address and your first admin key in one\ncommand. No account, no key, and no invite needed beforehand. The address labels\nyour new workspace and may receive a setup message; delivery is best-effort, so\nnothing waits on it. The email is visible as you type (it is not a secret) and is\nnever written to a log. This is the same route as answering **2** at the prompt.\n\n**You were sent an invite code.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --invite\n```\n\nRedeems the invite, creates your project, and mints your first admin key. Run it\n*without* a value, as shown: the installer then asks for the code at a hidden\nprompt, so the code never lands in your shell history. `--invite <code>` and the\n`BAPI_INVITE` environment variable exist for scripting, but both expose the code to\nyour shell history and to the process list.\n\n**Your team already has a project and gave you an API key.**\n\n```bash\nnpx -y @bridge_gpt/mcp-server install --api-key <key>\n```\n\nOr omit the flag and paste the key at the hidden prompt. Generate a key on the\nBridge web UI\'s **Security** page (**Create New Key**, role **Admin**) and copy it\nimmediately \u2014 it is shown once. `BAPI_API_KEY` works too.\n\nIf you paste an invite code where a key was expected, or a key where an invite was\nexpected, the installer recognizes the mismatch and tells you before anything is\ncreated or spent.\n\n</details>\n\n<details>\n<summary><strong>Installer flags</strong></summary>\n\n| Flag | What it does |\n|---|---|\n| `--email <addr>` | Sign up for a new Bridge project with just an email address |\n| `--invite [code]` | Redeem an invite code. Omit the value for the hidden prompt (recommended) |\n| `--api-key <key>` | Use an existing Bridge API key |\n| `--repo <name>` | Name the registered repository instead of resolving or asking for it |\n| `--tools <list>` | Configure specific MCP hosts without the picker (e.g. `claude-code,cursor`) |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step (default `claude`) |\n| `--dry-run` | Preview every step without writing, contacting Bridge, or opening anything |\n| `--force` | Overwrite an existing stored key without asking |\n| `-h`, `--help` | Full usage |\n\n`--email`, `--invite`, and `--api-key` are mutually exclusive \u2014 each names a\ndifferent way to arrive, and the installer will not guess between them.\n\n</details>\n\n<details>\n<summary><strong>Setting up an MCP host by hand</strong></summary>\n\nThe installer configures your editors for you. Do this only if you would rather\nwrite the config yourself, or if you use a host it cannot write automatically.\n\nScaffold the project files without configuring anything:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\nThen add a `bridge-api` entry to your host\'s MCP config, filling in your repository\nname and API key. Add `"serve"` as the last launcher argument, as shown \u2014 it is the\nexplicit way to say "start the MCP server."\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server", "serve"],\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", "serve"],\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", "serve"],\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", "serve"],\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", "serve"]\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](#environment-variables)).\n</details>\n\nAfter saving, reload your editor and ask your assistant to call the `ping` tool to\nconfirm the connection.\n\nAn entry with no trailing `serve` still starts the server \u2014 bare invocation means\n"server" permanently, and nothing rewrites an existing config to add the token.\n\n</details>\n\n<details>\n<summary><strong>Upgrading Bridge</strong></summary>\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest --upgrade\n```\n\n`upgrade` fetches the latest published version, refreshes your scaffolded slash\ncommands, agents, and pipelines, updates the version pin in your MCP config, and\nopens a session so you can reconnect. It is also available as the\n`/upgrade-bridge` slash command.\n\nUse the `@latest` form. It applies to the short-lived *upgrader* process: without\nit, npx may reuse a cached older copy of the package and "upgrade" you with the\nbuild you are trying to replace. The exact `MAJOR.MINOR.PATCH` pin the upgrader\nwrites into your MCP config is deliberately different \u2014 host configs stay pinned\nto an exact release so a project\'s server is reproducible.\n\n`upgrade` reports **per config file**, because a project can have several\n(`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) and they can disagree:\n\n```\nLauncher pins:\n .mcp.json: 0.2.16 -> 0.2.36\n .cursor/mcp.json: already 0.2.36\n```\n\nWhen every applicable launcher pin was already at the target, it prints\n`Already up-to-date.` \u2014 that status comes from comparing your configs, not from\nthe version of the CLI process. A non-zero exit means the upgrade did **not**\nconverge (an unreadable config, a launcher carrying a version range the upgrader\nmust not rewrite, a competing local install it could not remove, or a pin that\nfailed post-write verification); nothing is reported as complete in that case.\n\nThe server checks for updates on startup. The check is cached for a day and never\nblocks startup. When a newer version is known, it surfaces in two places you do\nnot have to go looking for: a one-line warning on the server\'s **stderr**, and a\nshort advisory attached to the ordinary `tools/list` response so the agent in the\nsession can see that some tools may be missing or renamed in the older build.\nNeither requires calling `ping` or `doctor`.\n\nRe-running `install` on an already-configured project is safe: it refreshes the\nscaffolded files without overwriting your stored credential unless you pass\n`--force`.\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**, **how to use it**, and its **flags**. 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<!-- The three tier sections below are GENERATED from api/library/config/mcp_tool_catalog.json,\n the authoritative tool catalog, by scripts/sync_mcp_server_readme.py. Edit the curated\n metadata in scripts/sync_mcp_tool_catalog.py \u2014 never the JSON artifact and never the\n text between the markers. Generation order is: sync_mcp_tool_catalog.py, then\n sync_mcp_server_readme.py, then `cd mcp_server && npm run build` (which bundles this\n file into readme.generated.ts, served as the MCP resource bridge-api://readme).\n Everything outside the marker pair \u2014 including the sections below it \u2014 is hand-written. -->\n\n<!-- BEGIN GENERATED: mcp-tool-documentation (managed by scripts/sync_mcp_server_readme.py \u2014 DO NOT EDIT BY HAND) -->\n### Regularly useful\n\nThe tools worth knowing for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions and a critique plus an alternate-model second opinion, then evaluates the findings and produces a decision page for accepting or rejecting them.\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). For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every `/review-ticket` flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.\n- **Flags:** `--auto` auto-accept findings and skip the approval gates \xB7 `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the difficulty-adaptive review policy decide.\n\n**2. Council**\n- **What it does:** Fans your problem out to two different models and returns their approaches, in technical, design, discovery, or general mode.\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 still needs figuring out before a real ticket exists, general for a quick brief-driven pass 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." 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 so we can collect the questions stakeholders need to answer first."\n- **Flags:** `mode` selects one of four modes: `technical` (the default \u2014 implementation/architecture approaches), `design` (UI/UX and visual direction), `discovery` (stakeholder discovery questions, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), and `general` (brief-driven ideation from your task description alone). `technical` and `discovery` are codebase-grounded and need an indexed repository; `general` needs no code index at all, so it works immediately after install. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n\n**3. Get Council**\n- **What it does:** Retrieves a council that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When a council was already run and you want to reread it without paying to regenerate it.\n- **How to use it:** Ask your agent to pull up the council already generated for the ticket.\n- **Flags:** None.\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 \u2014 libraries, best practices, standards \u2014 that you do not already have.\n- **How to use it:** `/bridge-research <question>`\n- **Flags:** None.\n\n**5. Get Deep Research**\n- **What it does:** Retrieves a research report that was already generated.\n- **When it\'s useful:** (Architecture | Refinement) When the report already exists and you want to reread it without re-running the research.\n- **How to use it:** Ask your agent to retrieve the research report already generated.\n- **Flags:** None.\n\n**6. Upload Ticket**\n- **What it does:** Creates a real Jira issue from a drafted ticket, including child tickets under an epic; your agent should confirm with you before creating it.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into your tracker so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket; it should confirm with you before creating the live issue.\n- **Flags:** Name the issue type (Bug / Story / Task / Epic) and, for a child ticket under an epic, the parent key.\n\n### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket that references real files in your codebase.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before \u2014 or instead of \u2014 auto-implementing it.\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. Get Plan**\n- **What it does:** Retrieves an implementation plan that was already generated for a ticket.\n- **When it\'s useful:** (Implementation) When the plan already exists and you want to read it without regenerating it.\n- **How to use it:** Ask your agent to fetch the implementation plan already generated for the ticket.\n- **Flags:** None.\n\n**3. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket, or debugging guidance when the ticket is a bug.\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`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Get Clarifying Questions**\n- **What it does:** Retrieves clarifying questions that were already generated for a ticket.\n- **When it\'s useful:** (Refinement) When the questions were already generated and you want to reread them rather than pay to regenerate them.\n- **How to use it:** Ask your agent for the clarifying questions already generated for the ticket.\n- **Flags:** None.\n\n**5. Critique Ticket**\n- **What it does:** Critiques a ticket against your project\'s standards and lists the deviations and improvements it found.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before anyone works it.\n- **How to use it:** `/critique-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**6. Get Ticket Critique**\n- **What it does:** Retrieves a ticket critique that was already generated.\n- **When it\'s useful:** (Refinement) When the critique already exists and you want to reread its findings without regenerating it.\n- **How to use it:** Ask your agent for the critique already generated for the ticket.\n- **Flags:** None.\n\n**7. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a technical design document, a functional spec, or a product requirements document.\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**8. Get Doc**\n- **What it does:** Retrieves a design document that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When the document already exists and you want to reread it without regenerating it.\n- **How to use it:** Ask your agent for the ticket\'s design document, naming which type you want.\n- **Flags:** None.\n\n**9. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family, without saving an artifact.\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 production."\n- **Flags:** Pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**10. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model, spending 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- **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**11. Request PRD**\n- **What it does:** Generates a product requirements document for a ticket covering the problem, the goals, and the success metrics.\n- **When it\'s useful:** (Architecture | Refinement) When a piece of work needs its problem, goals, and success metrics written down before anyone designs a solution.\n- **How to use it:** `/create-doc BAPI-123 --doc-type prd`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**12. Get PRD**\n- **What it does:** Retrieves a product requirements document that was already generated for a ticket.\n- **When it\'s useful:** (Architecture | Refinement) When the requirements document already exists and you want to reread it without regenerating it.\n- **How to use it:** Ask your agent to retrieve the ticket\'s product requirements document.\n- **Flags:** None.\n\n**13. Full Automation**\n- **What it does:** Drives the whole chain from a raw idea through tickets and reviews to implementation sessions.\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 it creates tickets, spawns worktrees, and carries scheduling flags free text cannot).\n- **Flags:** `--require-approval` re-enable the approval gates; the chain runs end to end by default \xB7 `--max-children <n>` cap how many child tickets an epic decomposes into.\n\n**14. Resume Full Automation**\n- **What it does:** Resumes a full-automation chain that was started earlier.\n- **When it\'s useful:** (Automation) When an earlier chain stopped at an approval gate or was interrupted, and you want it continued rather than restarted.\n- **How to use it:** Ask your agent to resume the full-automation chain, naming the run to continue.\n- **Flags:** None.\n\n**15. Update Ticket Description**\n- **What it does:** Rewrites a ticket\'s description with AI, using the ticket\'s own content and its reference material. A rewrite that changes more than 60% of the description is held for review instead of applied.\n- **When it\'s useful:** (Refinement) When a ticket has accumulated comments, attachments, or links and its description no longer reflects them.\n- **How to use it:** Ask your agent \u2014 "Update the description for BAPI-123."\n- **Flags:** None. Poll the ticket\'s state for the outcome; if the update was held for review, read the proposal instead of applying it blind.\n\n### Now and then\n\nUseful once in a while.\n\n**1. Reimplement Ticket**\n- **What it does:** Gathers the context and attachments added since the last pass so a targeted follow-up change can be made.\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- **Flags:** None.\n\n**2. Get Reimplement Context**\n- **What it does:** Retrieves the follow-up context that was already gathered for a ticket.\n- **When it\'s useful:** (Implementation) When the follow-up context was already gathered and you want to read it without gathering it again.\n- **How to use it:** Ask your agent for the follow-up context already gathered for the ticket.\n- **Flags:** None.\n\n**3. Update Ticket**\n- **What it does:** Rewrites a ticket\'s description, fully replacing what is there today.\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 it fully overwrites the live description, which is hard to reverse).\n- **Flags:** None.\n\n**4. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description.\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- **Flags:** None.\n\n**5. Search Tickets**\n- **What it does:** Searches across the tickets in your project.\n- **When it\'s useful:** (Refinement) When you need to find tickets by project, status, or wording rather than by key.\n- **How to use it:** Ask your agent \u2014 "Search our project for open tickets mentioning rate limiting."\n- **Flags:** Narrow the search by project, status, issue type, or free text.\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a ticket.\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 is rotated."\n- **Flags:** A long comment can be attached as a file instead of inlined.\n\n**7. Read Comments**\n- **What it does:** Reads the comment thread on a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When the discussion on a ticket matters and you want the agent to read it before acting.\n- **How to use it:** Ask your agent \u2014 "Read the comments on BAPI-123 and summarize what was decided."\n- **Flags:** None.\n\n**8. Ticket Attachments**\n- **What it does:** Downloads files from a ticket to your disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files or 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," or "Attach build-log.txt to BAPI-123."\n- **Flags:** Choose the direction (download from the ticket, or upload to it) and, for a download, where the files should land.\n\n**9. Estimate Ticket**\n- **What it does:** Estimates the development effort for one ticket. Use Estimate Epic instead for a whole epic or a named group of tickets.\n- **When it\'s useful:** (Refinement) When you need a size for a single ticket before committing to it.\n- **How to use it:** Ask your agent \u2014 "Estimate BAPI-123."\n- **Flags:** Ask for a fresh estimate to regenerate rather than reuse a stored one.\n\n**10. Estimate Epic**\n- **What it does:** Estimates an epic, or an explicit group of tickets you name.\n- **When it\'s useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.\n- **How to use it:** `/estimate-epic BAPI-123`\n- **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.\n<!-- END GENERATED: mcp-tool-documentation -->\n\n### Workflow commands\n\nSlash commands that drive several tools at once. They are agent workflows rather than single MCP tools, so they are documented here by hand.\n\n**1. 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 \xB7 `--rounds=1|2` forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override.\n\n**2. 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**3. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket 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` 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\n**4. 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>` \u2014 or ask your agent, *"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**5. 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- **Flags:** None.\n\n**6. 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- **Flags:** None.\n\n**7. 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- **Flags:** None.\n\n**8. 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`\n- **Flags:** `--unit-only` skip the E2E suite \xB7 `--skip-e2e` same, phrased the other way.\n\n**9. 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- **Flags:** None.\n\n**10. 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- **Flags:** None.\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. An oversized response is saved in full to `BAPI_DOCS_DIR/sfcc/` and replaced by a parseable JSON descriptor \u2014 `truncated: true`, the `saved_path` it was written to, and the `page` metadata (`returned`, `total` when OCAPI supplied one, `has_more`) \u2014 so the collection metadata survives even though the data itself is on disk. If that save fails, the complete payload is returned inline instead, still as parseable JSON.\n\nAttribute-definition reads and writes can return an attribute\'s `default_value` at `projection: "full"`, and Bridge withholds it \u2014 every key is preserved except that one, whose value becomes `[REDACTED_BY_BRIDGE]` \u2014 from the inline response, the saved file, and a successful write echo alike. Attribute defaults are intentionally unavailable through this MCP surface; Business Manager is the supported path to read one.\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 system object type\'s definition.\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 (`default_value` withheld). OCAPI cannot enumerate custom object type *IDs* directly, so `object_type` must be known \u2014 but it is discoverable: call `system_object_list` at `projection: "full"` for each custom type\'s `display_name` and `attribute_definition_count`, derive a candidate id (e.g. strip spaces from `"Product Quality Result"` \u2192 `ProductQualityResult`), and confirm it by checking that this tool\'s returned attribute count matches that row\'s `attribute_definition_count`.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type (`default_value` withheld). Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability. Same discovery path as above applies to `object_type`.\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 authoritative tool catalog covers **92 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`, `get_ticket_state_tree` (live repo-wide lifecycle + dependency tree; read-only, no mutation parameter)\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';import{readdir,readFile}from"fs/promises";import path 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,base.instruction_file=step.instruction_file}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. Recipes are re-entrant: a recovery run may begin again at step 1, and a step whose tool reports a server-side reuse (e.g. reused: true) has completed successfully \u2014 treat that as the expected fast path, not a failure, and continue. A step can succeed (no on_error handling applies) while its own returned content is a JSON envelope shaped like error: "GATEWAY_TIMEOUT", status: 504, and a recovery_get field \u2014 recognize that shape and read it as "server-side processing may still be running", not as a failure or an invitation to retry. Poll the named retrieval tool (or the recovery_get URL) with the same artifact identifier until a terminal response is reached, and never reissue the original request tool. Only fall back to on_error handling if that retrieval itself terminally fails. 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 readFile(path.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 readFile(path.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:
4569
4569
  ${errors.join(`
4570
4570
  `)}`);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,mkdir,readFile as readFile2,stat}from"fs/promises";import path6 from"path";import os from"os";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:`
4571
4571
  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.
@@ -5419,7 +5419,7 @@ ${getExecutorUsage()}`),1;let options=parsed.options,baseUrlResult=resolveBaseUr
5419
5419
  `)),env=overrides.env??process.env,parsed=parseMcpInvokeArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return stderr(`Error: ${parsed.message}`),stderr(""),stderr(getMcpInvokeUsage()),1;let{target,projectRoot,baseUrl}=parsed,statFn=overrides.stat??(p=>stat6(p)),dirCheck=await validateProjectRootDirectory(projectRoot,statFn);if(!dirCheck.ok)return stderr(`Error: ${dirCheck.error}`),1;let credentialDeps={env,homedir:os12.homedir,platform:process.platform,readFile:p=>readFile8(p,"utf-8"),stat:p=>stat6(p),stderr};if(target==="bapi"){let resolveRepoName2=overrides.resolveRepoName??(pr=>resolveRepoNameForProjectRoot(pr,{readFile:p=>readFile8(p,"utf-8"),runCommand:defaultRunCommand2})),resolveCredentials=overrides.resolveCredentials??(rn=>resolveBapiCredentials(rn,credentialDeps)),resolved2=await resolveBapiInvocation(projectRoot,{env,execPath:process.execPath,scriptPath:process.argv[1],resolveRepoName:resolveRepoName2,resolveCredentials,baseUrl});return resolved2.ok?spawnInvocation(resolved2.invocation,overrides):(stderr(`Error: ${resolved2.error}`),1)}let readConfig=overrides.readBridgeConfig??(pr=>readBridgeConfig(pr,{readFile:p=>readFile8(p,"utf-8")})),getTargetDefinition=overrides.getTargetDefinition??getThirdPartyTargetDefinition,resolveTargetEnv=overrides.resolveThirdPartyEnv??((definition,secretBundle)=>resolveThirdPartyTargetEnv(definition,secretBundle,credentialDeps)),resolved=await resolveThirdPartyInvocation(target,projectRoot,{env,readBridgeConfig:readConfig,getTargetDefinition,resolveTargetEnv});return resolved.ok?spawnInvocation(resolved.invocation,overrides):(stderr(`Error: ${resolved.error}`),1)}function spawnInvocation(invocation,overrides){return overrides.spawnMcpCommand?overrides.spawnMcpCommand(invocation):overrides.spawnRealMcpServer?overrides.spawnRealMcpServer(invocation.env):spawnMcpCommand({spawn:spawn5,command:invocation.command,args:invocation.args,env:invocation.env,onSignal:(s,h)=>{process.on(s,h)},offSignal:(s,h)=>{process.off(s,h)}})}init_agent_registry();init_default_deps();var LABEL={pass:"PASS",fail:"FAIL",skip:"SKIP",hang:"HANG","not-applicable":"N/A "};function formatCapabilityReport(collection){let lines=["agent-capabilities report",""],agents=[];for(let rec of collection.records)agents.includes(rec.agent)||agents.push(rec.agent);let totals={pass:0,fail:0,skip:0,hang:0,"not-applicable":0};for(let agent of agents){lines.push(`Agent: ${agent}`);for(let rec of collection.records.filter(r=>r.agent===agent)){totals[rec.result.status]+=1;let ms=rec.result.elapsedMs!==void 0?` (${rec.result.elapsedMs}ms)`:"";lines.push(` [${LABEL[rec.result.status]}] ${rec.probeId.padEnd(24)} ${rec.result.detail}${ms}`),rec.result.evidence&&lines.push(` evidence: ${rec.result.evidence}`)}lines.push("")}let na=totals["not-applicable"]?`, ${totals["not-applicable"]} n/a`:"";return lines.push(`Summary: ${totals.pass} pass, ${totals.fail} fail, ${totals.skip} skip, ${totals.hang} hang${na}`),lines.join(`
5420
5420
  `)}function formatCapabilityJson(collection){return JSON.stringify({records:collection.records.map(r=>({agent:r.agent,probe:r.probeId,title:r.title,status:r.result.status,detail:r.result.detail,elapsedMs:r.result.elapsedMs??null,evidence:r.result.evidence??null,metadata:r.result.metadata??null}))},null,2)}init_agent_registry();init_probe_context();init_probes();init_types2();async function runOneProbe(deps,agentName,probe,ctx){if(probe.spawnsAgent){if(ctx.resolvedBinary===null)return{status:"skip",detail:`${ctx.agent.command} not on PATH \u2014 see binary-resolves`};let auth=AGENT_AUTH[agentName];if(auth.kind==="env"){let value=deps.env[auth.varName];if(!value||value.length===0)return{status:"skip",detail:`${auth.varName} not set \u2014 set it to run this probe`}}}try{return await probe.run(ctx)}catch(err){return{status:"fail",detail:err instanceof Error?err.message:String(err)}}}async function collectCapabilityResults(deps,options){let records=[];for(let agentName of options.agents){let agent=resolveAgentSpec(agentName);if(!agent)continue;let applicable=ALL_PROBES.filter(p=>p.appliesTo.includes(agentName)&&(!options.only||options.only.includes(p.id))),{ctx,cleanup}=await createProbeContext(deps,agent,options.timeoutMs);try{for(let probe of applicable){let result=await runOneProbe(deps,agentName,probe,ctx);records.push({agent:agentName,probeId:probe.id,title:probe.title,result})}}finally{await cleanup()}}return{records}}function hasFailureOrHang(collection){return collection.records.some(r=>r.result.status==="fail"||r.result.status==="hang")}init_probes();function getAgentCapabilitiesUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server agent-capabilities [--agent <name|all>] [--only <ids>] [--json]","","Empirically validates what an agent CLI can do (binary resolution, headless","print mode + exit, .claude/commands resolution, preamble drift-check, output","formats). Runs disposable probes in temp projects; never writes to your repo.","","Flags:",` --agent <name|all> Agent(s) to probe: ${formatValidAgentNames()}, or 'all' (default: ${DEFAULT_AGENT_NAME})`," --only <ids> Comma-separated probe ids to run (default: all). Valid ids:",` ${listProbeIds().join(", ")}`," --json Emit JSON instead of the human report"," -h, --help Show this help","","Auth: cursor-agent probes that spawn the agent require CURSOR_API_KEY (read from","the environment, never printed); they SKIP when it is unset. claude uses","keychain/OAuth, which cannot be verified from the environment.","","Exit code: 0 when every probe is pass/skip/n-a; nonzero when any probe FAILs or HANGs."].join(`
5421
5421
  `)}function parseListFlag(raw){return raw.split(",").map(s=>s.trim()).filter(s=>s.length>0)}function parseAgentCapabilitiesArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getAgentCapabilitiesUsage()};let agentSelector=DEFAULT_AGENT_NAME,only,json=!1,timeoutMs;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--json"){json=!0;continue}if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else{if(i+1>=argv.length)return{status:"error",message:"--agent requires a value (an agent name or 'all')."};value=argv[++i]}if(value!=="all"&&!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed: ${formatValidAgentNames()}, all).`};agentSelector=value;continue}if(arg==="--only"||arg.startsWith("--only=")){let value;if(arg.startsWith("--only="))value=arg.slice(7);else{if(i+1>=argv.length)return{status:"error",message:"--only requires a value (comma-separated probe ids)."};value=argv[++i]}let ids=parseListFlag(value),valid=new Set(listProbeIds()),unknown=ids.filter(id=>!valid.has(id));if(unknown.length>0)return{status:"error",message:`Unknown probe id(s): ${unknown.join(", ")} (valid: ${listProbeIds().join(", ")}).`};only=ids;continue}if(arg==="--timeout-ms"||arg.startsWith("--timeout-ms=")){let value;if(arg.startsWith("--timeout-ms="))value=arg.slice(13);else{if(i+1>=argv.length)return{status:"error",message:"--timeout-ms requires a numeric value."};value=argv[++i]}let parsed=Number(value);if(!Number.isFinite(parsed)||parsed<=0)return{status:"error",message:`Invalid --timeout-ms value: '${value}'.`};timeoutMs=parsed;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. agent-capabilities takes only flags.`}}return{status:"ok",options:{agentSelector,only,json,timeoutMs}}}async function runAgentCapabilitiesCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseAgentCapabilitiesArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getAgentCapabilitiesUsage()),1;let deps=overrides.deps??createDefaultAgentCapabilitiesDeps(),agents=parsed.options.agentSelector==="all"?listAgentNames():[parsed.options.agentSelector],collection=await collectCapabilityResults(deps,{agents,only:parsed.options.only,timeoutMs:parsed.options.timeoutMs});return log(parsed.options.json?formatCapabilityJson(collection):formatCapabilityReport(collection)),hasFailureOrHang(collection)?1:0}init_bridge_api_client();init_base_ref();init_plan();import{readFile as fsReadFile,stat as fsStat2}from"node:fs/promises";import os13 from"node:os";import readline2 from"node:readline";var SETUP_EPIC_REVIEW_POLICY_SOURCES=["verdict_protocol","native_review_decision","none"];function defaultPromptLine(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 createDefaultSetupEpicDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os13.homedir,readFile:p=>fsReadFile(p,"utf-8"),stat:p=>fsStat2(p),fetch:globalThis.fetch,log:m=>console.log(m),errorLog:m=>console.error(m),isTTY:!!process.stdin.isTTY,promptLine:defaultPromptLine}}function getSetupEpicUsage(){return["Usage: mcp-server setup-epic --epic-key <KEY> --plan-file <path> [options]","","Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,","and approves it. Idempotent \u2014 re-running reuses an existing live run.","","Required:"," --epic-key <KEY> Jira epic key (e.g. BAPI-405)"," --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)","","Options:"," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," --plan-version <n> Assert the sidecar's plan_version equals <n>"," --feature-branch <name> Run the epic on a dedicated feature branch."," The branch is created from the repository base branch"," on origin, and every child-ticket PR targets it."," Omit (the default) to continue on the repository base"," branch. Interactive runs are offered a proposal."," --review-policy <src> PER-RUN review policy source, one of:",` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`," Composed into policy_json.review_policy on create."," This setting is per-run: repository-level review-policy"," defaults are NOT persisted in supervisor project"," defaults yet \u2014 that is BAPI-694."," --json Emit a single JSON result object on stdout"," -h, --help Show this help","","Policy controls:"," --policy-file <path> JSON file holding the COMPLETE run policy. Applied in"," the same invocation that creates the run, so no"," post-create PATCH is needed. Contradicts nothing"," silently: if the file and --feature-branch or"," --review-policy disagree, setup-epic errors naming both."," --replace-policy Authorize replacing a LIVE run's stored policy with the"," policy file, as a complete replacement. Without it, a"," divergent policy on a reused run is refused with a"," redacted diff rather than applied. Requires"," --policy-file.","","Validation controls:"," --dry-run Run the SERVER's real plan validator (including"," file-overlap serialization) and stop. Creates no run,"," stores no plan, and incurs no automation-start charge."," --local-only Skip server validation and run only the local sidecar"," checks. Valid ONLY with --dry-run, and the result is"," labeled 'partial (local checks only)': the local checks"," do not cover everything the server enforces, so this is"," never a substitute for authoritative validation.","","After setup, the server-side reconciler picks the run up within ~30s.","To execute claimed jobs on this machine, run:"," npx -y @bridge_gpt/mcp-server executor --repo <name>"].join(`
5422
- `)}function takeValue2(argv,i,flag){let next=argv[i+1];return next===void 0||next.startsWith("-")?null:next}function parseFeatureBranchValue(raw){let trimmed=raw.trim();if(trimmed==="")return{ok:!0,value:void 0};let reason=validateBranchName(trimmed);return reason?{ok:!1,error:`Invalid --feature-branch value: ${reason}`}:{ok:!0,value:trimmed}}function parseReviewPolicyValue(raw){let trimmed=raw.trim();return trimmed===""?{ok:!0,value:void 0}:SETUP_EPIC_REVIEW_POLICY_SOURCES.includes(trimmed)?{ok:!0,value:trimmed}:{ok:!1,error:`Invalid --review-policy value '${trimmed}'. Expected one of: `+SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}}function parseSetupEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getSetupEpicUsage()};let epicKey,planFile,repo,planVersion,featureBranch,reviewPolicy,policyFile,replacePolicy=!1,localOnly=!1,dryRun=!1,json=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg.startsWith("--policy-file=")){let raw=arg.slice(14).trim();if(raw==="")return{status:"error",message:"--policy-file requires a value."};policyFile=raw;continue}if(arg.startsWith("--feature-branch=")){let parsedFb=parseFeatureBranchValue(arg.slice(17));if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value;continue}if(arg.startsWith("--review-policy=")){let parsedRp=parseReviewPolicyValue(arg.slice(16));if(!parsedRp.ok)return{status:"error",message:parsedRp.error};reviewPolicy=parsedRp.value;continue}switch(arg){case"--feature-branch":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--feature-branch requires a value."};let parsedFb=parseFeatureBranchValue(v);if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value,i++;break}case"--review-policy":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--review-policy requires a value."};let parsedRp=parseReviewPolicyValue(v);if(!parsedRp.ok)return{status:"error",message:parsedRp.error};reviewPolicy=parsedRp.value,i++;break}case"--epic-key":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--epic-key requires a value."};epicKey=v,i++;break}case"--plan-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-file requires a value."};planFile=v,i++;break}case"--repo":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--repo requires a value."};repo=v,i++;break}case"--plan-version":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-version requires a value."};if(!/^\d+$/.test(v))return{status:"error",message:`--plan-version must be a positive integer, got '${v}'.`};if(planVersion=Number(v),planVersion<1)return{status:"error",message:"--plan-version must be >= 1."};i++;break}case"--policy-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--policy-file requires a value."};if(policyFile=v.trim(),policyFile==="")return{status:"error",message:"--policy-file requires a value."};i++;break}case"--replace-policy":replacePolicy=!0;break;case"--local-only":localOnly=!0;break;case"--dry-run":dryRun=!0;break;case"--json":json=!0;break;default:return{status:"error",message:`Unknown argument '${arg}'. Run "setup-epic --help" for usage.`}}}return epicKey?planFile?localOnly&&!dryRun?{status:"error",message:"--local-only is only valid together with --dry-run. It downgrades the run to local checks only, which is never a substitute for the server's validation on a real setup."}:replacePolicy&&policyFile===void 0?{status:"error",message:"--replace-policy requires --policy-file. It authorizes replacing a live run's stored policy with the file's contents, so without a file there is nothing to replace it with."}:{status:"ok",options:{epicKey,planFile,repo,planVersion,featureBranch,reviewPolicy,policyFile,replacePolicy,localOnly,dryRun,json}}:{status:"error",message:"setup-epic requires --plan-file <path>."}:{status:"error",message:"setup-epic requires --epic-key <KEY>."}}async function readSetupEpicPolicyFile(filePath,readFile17){let raw;try{raw=await readFile17(filePath)}catch{return{ok:!1,error:`Could not read policy file '${filePath}'.`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{ok:!1,error:`Policy file '${filePath}' is not valid JSON.`}}return!parsed||typeof parsed!="object"||Array.isArray(parsed)?{ok:!1,error:`Policy file '${filePath}' must contain a JSON object (the run policy).`}:{ok:!0,policy:parsed}}function readReviewPolicySource(policy){if(!("review_policy"in policy))return{kind:"absent"};let block=policy.review_policy;if(block==null)return{kind:"absent"};if(typeof block!="object"||Array.isArray(block))return{kind:"ambiguous"};let source=block.source;return source==null?{kind:"absent"}:typeof source!="string"||source.trim()===""?{kind:"ambiguous"}:{kind:"source",value:source}}function composeSetupEpicPolicy(filePolicy,selections){let composed={...filePolicy},declaredBranches=[];for(let key of["base_branch","baseBranch"])key in composed&&composed[key]!==void 0&&composed[key]!==null&&declaredBranches.push({key,value:composed[key]});if(selections.featureBranch!==void 0){let conflicting=declaredBranches.filter(entry=>entry.value!==selections.featureBranch);if(conflicting.length>0){let names=conflicting.map(entry=>entry.key).join(" and ");return{ok:!1,error:`--feature-branch selected '${selections.featureBranch}', but --policy-file declares a different branch under ${names}. setup-epic will not pick a winner between them: drop the flag, or fix the policy file so both agree.`}}declaredBranches.length===0&&(composed.base_branch=selections.featureBranch)}let fileReview=readReviewPolicySource(composed);if(selections.reviewPolicy!==void 0){if(fileReview.kind==="ambiguous")return{ok:!1,error:`--review-policy selected '${selections.reviewPolicy}', but --policy-file's review_policy is not an object with a non-empty string 'source'. setup-epic will not guess which one you meant: drop the flag, or fix review_policy in the policy file.`};if(fileReview.kind==="source"&&fileReview.value!==selections.reviewPolicy)return{ok:!1,error:`--review-policy selected '${selections.reviewPolicy}', but --policy-file declares review_policy.source '${fileReview.value}'. setup-epic will not pick a winner between them: drop the flag, or fix the policy file so both agree.`};if(fileReview.kind==="absent"){let existing=composed.review_policy;composed.review_policy=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing,source:selections.reviewPolicy}:{source:selections.reviewPolicy}}}return{ok:!0,policy:composed}}function policySuppliesWebhookUrl(policy){let notify=policy.notify;return!notify||typeof notify!="object"||Array.isArray(notify)?!1:"webhook_url"in notify}function projectPolicyForComparison(policy){if(Array.isArray(policy))return policy.map(item=>projectPolicyForComparison(item));if(!policy||typeof policy!="object")return policy;let out={};for(let[key,value]of Object.entries(policy))if(key!=="feature_branch_provisioning"){if(key==="notify"&&value&&typeof value=="object"&&!Array.isArray(value)){let notify={};for(let[nKey,nValue]of Object.entries(value))nKey!=="webhook_url"&&(notify[nKey]=projectPolicyForComparison(nValue));out.notify=notify;continue}out[key]=projectPolicyForComparison(value)}return out}function renderDiffValue(value){let text2=JSON.stringify(value??null);return text2.length>120?`${text2.slice(0,120)}\u2026`:text2}function diffProjectedPolicies(stored,requested,basePath=""){let isPlainObject11=v=>!!v&&typeof v=="object"&&!Array.isArray(v);if(isPlainObject11(stored)&&isPlainObject11(requested)){let keys=[...new Set([...Object.keys(stored),...Object.keys(requested)])].sort(),entries=[];for(let key of keys){let path48=basePath?`${basePath}.${key}`:key,inStored=key in stored,inRequested=key in requested;inStored&&!inRequested?entries.push({path:path48,kind:"removed"}):!inStored&&inRequested?entries.push({path:path48,kind:"added"}):entries.push(...diffProjectedPolicies(stored[key],requested[key],path48))}return entries}return JSON.stringify(stored??null)===JSON.stringify(requested??null)?[]:[{path:basePath||"(policy)",kind:"changed"}]}function renderPolicyDiff(entries,stored,requested){let read=(source,path48)=>{let cursor=source;for(let part of path48.split(".")){if(!cursor||typeof cursor!="object")return;cursor=cursor[part]}return cursor};return entries.map(entry=>entry.kind==="added"?` + ${entry.path}: ${renderDiffValue(read(requested,entry.path))} (not in the stored policy)`:entry.kind==="removed"?` - ${entry.path}: ${renderDiffValue(read(stored,entry.path))} (absent from the policy file)`:` ~ ${entry.path}: stored ${renderDiffValue(read(stored,entry.path))} \u2192 file ${renderDiffValue(read(requested,entry.path))}`)}var WEBHOOK_SUPPLIED_DIFF_LINE=" ~ notify.webhook_url: (supplied \u2014 will replace stored value on apply)";function reconcileReusedRunPolicy(args){let storedProjected=projectPolicyForComparison(args.storedPolicy??{}),requestedProjected=projectPolicyForComparison(args.requestedPolicy),diffLines=renderPolicyDiff(diffProjectedPolicies(storedProjected,requestedProjected),storedProjected,requestedProjected);return policySuppliesWebhookUrl(args.requestedPolicy)&&diffLines.push(WEBHOOK_SUPPLIED_DIFF_LINE),diffLines.length===0?{kind:"noop"}:args.replacePolicy?{kind:"replace",diffLines}:{kind:"refused_divergent",diffLines}}function validateEpicPlanSidecar(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,error:"Plan sidecar must be a JSON object."};let plan=parsed,version=plan.plan_version;if(typeof version!="number"||!Number.isInteger(version)||version<1)return{ok:!1,error:`plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`};if(!Array.isArray(plan.nodes)||plan.nodes.length===0)return{ok:!1,error:"plan.nodes must be a non-empty array."};if(!Array.isArray(plan.edges))return{ok:!1,error:"plan.edges must be an array (use [] for none)."};let keys=new Set,warnings=[];for(let node of plan.nodes){if(!node||typeof node!="object")return{ok:!1,error:"Every plan node must be an object."};let key=typeof node.ticket_key=="string"?node.ticket_key.trim():"";if(!key)return{ok:!1,error:"Every plan node needs a non-empty ticket_key."};if(keys.has(key))return{ok:!1,error:`Duplicate ticket_key in plan: ${key}.`};keys.add(key),node.touched_files===void 0&&warnings.push(`Node ${key} has no touched_files, so it opts out of preemptive file-overlap serialization: the server accepts the plan but will not pre-serialize ${key} against overlapping siblings (a conflict would instead be caught reactively and rebased). Add touched_files to ${key} to get that scheduling protection (use [] when no files are predicted).`)}let adjacency=new Map,addEdge=(from,to)=>{let list=adjacency.get(from)??[];list.push(to),adjacency.set(from,list)};for(let node of plan.nodes){let deps=Array.isArray(node.depends_on)?node.depends_on:[];for(let dep of deps){if(!keys.has(dep))return{ok:!1,error:`Node ${node.ticket_key} depends_on unknown ticket '${dep}'.`};addEdge(dep,node.ticket_key)}}for(let edge of plan.edges){if(!edge||typeof edge!="object")return{ok:!1,error:"Every plan edge must be an object."};if(!keys.has(edge.from)||!keys.has(edge.to))return{ok:!1,error:`Edge ${JSON.stringify(edge.from)} -> ${JSON.stringify(edge.to)} references an unknown ticket.`};addEdge(edge.from,edge.to)}let cycle=findCycle(keys,adjacency);return cycle?{ok:!1,error:`Plan DAG has a cycle: ${cycle.join(" -> ")}.`}:{ok:!0,plan:parsed,warnings}}function findCycle(keys,adjacency){let color=new Map;for(let k of keys)color.set(k,0);for(let start of keys){if(color.get(start)!==0)continue;let stack=[{node:start,path:[start]}];for(;stack.length>0;){let{node,path:path48}=stack[stack.length-1];if(color.get(node)===0){color.set(node,1);for(let next of adjacency.get(node)??[]){if(color.get(next)===1)return[...path48,next];color.get(next)===0&&stack.push({node:next,path:[...path48,next]})}}else color.get(node)===1&&color.set(node,2),stack.pop()}}return null}function proposeFeatureBranchName(epicKey){return`epic/${epicKey}`}async function resolveFeatureBranchSelection(opts,repoName,deps,policyDeclaresBranch=!1){if(opts.featureBranch!==void 0)return opts.featureBranch;if(policyDeclaresBranch||!deps.isTTY||opts.json)return;let proposed=proposeFeatureBranchName(opts.epicKey);for(deps.errorLog(""),deps.errorLog(`Feature branch (optional) for epic ${opts.epicKey} on ${repoName}:`),deps.errorLog(` Proposed: ${proposed}`),deps.errorLog(" Strategy: create a new branch from the repository base branch"),deps.errorLog(" Effect: every child-ticket PR will target this branch");;){let answer=(await deps.promptLine(`Use feature branch? 'y' = ${proposed}, a name = custom, Enter = base branch: `)).trim();if(answer==="")return;let lowered=answer.toLowerCase();if(lowered==="n"||lowered==="no")return;if(lowered==="y"||lowered==="yes")return proposed;let reason=validateBranchName(answer);if(reason){deps.errorLog(` Invalid branch name: ${reason} Try again, or press Enter for the base branch.`);continue}return answer}}function errorDetail(err){if(err instanceof ConductorBridgeApiError){let status=err.status!==void 0?` (HTTP ${err.status})`:"",preview=err.bodyPreview?`: ${err.bodyPreview}`:"";return`${err.message}${status}${preview}`}return err instanceof Error?err.message:String(err)}async function applyReusedRunPolicy(args){let outcome2=reconcileReusedRunPolicy({storedPolicy:args.storedPolicy,requestedPolicy:args.requestedPolicy,replacePolicy:args.replacePolicy});if(outcome2.kind==="noop")return args.result.policy_applied="noop",args.say("Policy: policy unchanged"),!0;if(outcome2.kind==="refused_divergent")return args.result.policy_applied="refused_divergent",args.deps.errorLog(`Epic run ${args.epicRunId} is live and its stored policy differs from the policy file. setup-epic will not silently change a running epic's policy.
5422
+ `)}function takeValue2(argv,i,flag){let next=argv[i+1];return next===void 0||next.startsWith("-")?null:next}function parseFeatureBranchValue(raw){let trimmed=raw.trim();if(trimmed==="")return{ok:!0,value:void 0};let reason=validateBranchName(trimmed);return reason?{ok:!1,error:`Invalid --feature-branch value: ${reason}`}:{ok:!0,value:trimmed}}function parseReviewPolicyValue(raw){let trimmed=raw.trim();return trimmed===""?{ok:!0,value:void 0}:SETUP_EPIC_REVIEW_POLICY_SOURCES.includes(trimmed)?{ok:!0,value:trimmed}:{ok:!1,error:`Invalid --review-policy value '${trimmed}'. Expected one of: `+SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}}function parseSetupEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getSetupEpicUsage()};let epicKey,planFile,repo,planVersion,featureBranch,reviewPolicy,policyFile,replacePolicy=!1,localOnly=!1,dryRun=!1,json=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg.startsWith("--policy-file=")){let raw=arg.slice(14).trim();if(raw==="")return{status:"error",message:"--policy-file requires a value."};policyFile=raw;continue}if(arg.startsWith("--feature-branch=")){let parsedFb=parseFeatureBranchValue(arg.slice(17));if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value;continue}if(arg.startsWith("--review-policy=")){let parsedRp=parseReviewPolicyValue(arg.slice(16));if(!parsedRp.ok)return{status:"error",message:parsedRp.error};reviewPolicy=parsedRp.value;continue}switch(arg){case"--feature-branch":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--feature-branch requires a value."};let parsedFb=parseFeatureBranchValue(v);if(!parsedFb.ok)return{status:"error",message:parsedFb.error};featureBranch=parsedFb.value,i++;break}case"--review-policy":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--review-policy requires a value."};let parsedRp=parseReviewPolicyValue(v);if(!parsedRp.ok)return{status:"error",message:parsedRp.error};reviewPolicy=parsedRp.value,i++;break}case"--epic-key":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--epic-key requires a value."};epicKey=v,i++;break}case"--plan-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-file requires a value."};planFile=v,i++;break}case"--repo":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--repo requires a value."};repo=v,i++;break}case"--plan-version":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--plan-version requires a value."};if(!/^\d+$/.test(v))return{status:"error",message:`--plan-version must be a positive integer, got '${v}'.`};if(planVersion=Number(v),planVersion<1)return{status:"error",message:"--plan-version must be >= 1."};i++;break}case"--policy-file":{let v=takeValue2(argv,i,arg);if(v===null)return{status:"error",message:"--policy-file requires a value."};if(policyFile=v.trim(),policyFile==="")return{status:"error",message:"--policy-file requires a value."};i++;break}case"--replace-policy":replacePolicy=!0;break;case"--local-only":localOnly=!0;break;case"--dry-run":dryRun=!0;break;case"--json":json=!0;break;default:return{status:"error",message:`Unknown argument '${arg}'. Run "setup-epic --help" for usage.`}}}return epicKey?planFile?localOnly&&!dryRun?{status:"error",message:"--local-only is only valid together with --dry-run. It downgrades the run to local checks only, which is never a substitute for the server's validation on a real setup."}:replacePolicy&&policyFile===void 0?{status:"error",message:"--replace-policy requires --policy-file. It authorizes replacing a live run's stored policy with the file's contents, so without a file there is nothing to replace it with."}:{status:"ok",options:{epicKey,planFile,repo,planVersion,featureBranch,reviewPolicy,policyFile,replacePolicy,localOnly,dryRun,json}}:{status:"error",message:"setup-epic requires --plan-file <path>."}:{status:"error",message:"setup-epic requires --epic-key <KEY>."}}async function readSetupEpicPolicyFile(filePath,readFile17){let raw;try{raw=await readFile17(filePath)}catch{return{ok:!1,error:`Could not read policy file '${filePath}'.`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{ok:!1,error:`Policy file '${filePath}' is not valid JSON.`}}return!parsed||typeof parsed!="object"||Array.isArray(parsed)?{ok:!1,error:`Policy file '${filePath}' must contain a JSON object (the run policy).`}:{ok:!0,policy:parsed}}function readReviewPolicySource(policy){if(!("review_policy"in policy))return{kind:"absent"};let block=policy.review_policy;if(block==null)return{kind:"absent"};if(typeof block!="object"||Array.isArray(block))return{kind:"ambiguous"};let source=block.source;return source==null?{kind:"absent"}:typeof source!="string"||source.trim()===""?{kind:"ambiguous"}:{kind:"source",value:source}}function composeSetupEpicPolicy(filePolicy,selections){let composed={...filePolicy},declaredBranches=[];for(let key of["base_branch","baseBranch"])key in composed&&composed[key]!==void 0&&composed[key]!==null&&declaredBranches.push({key,value:composed[key]});if(selections.featureBranch!==void 0){let conflicting=declaredBranches.filter(entry=>entry.value!==selections.featureBranch);if(conflicting.length>0){let names=conflicting.map(entry=>entry.key).join(" and ");return{ok:!1,error:`--feature-branch selected '${selections.featureBranch}', but --policy-file declares a different branch under ${names}. setup-epic will not pick a winner between them: drop the flag, or fix the policy file so both agree.`}}declaredBranches.length===0&&(composed.base_branch=selections.featureBranch)}let fileReview=readReviewPolicySource(composed);if(selections.reviewPolicy!==void 0){if(fileReview.kind==="ambiguous")return{ok:!1,error:`--review-policy selected '${selections.reviewPolicy}', but --policy-file's review_policy is not an object with a non-empty string 'source'. setup-epic will not guess which one you meant: drop the flag, or fix review_policy in the policy file.`};if(fileReview.kind==="source"&&fileReview.value!==selections.reviewPolicy)return{ok:!1,error:`--review-policy selected '${selections.reviewPolicy}', but --policy-file declares review_policy.source '${fileReview.value}'. setup-epic will not pick a winner between them: drop the flag, or fix the policy file so both agree.`};if(fileReview.kind==="absent"){let existing=composed.review_policy;composed.review_policy=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing,source:selections.reviewPolicy}:{source:selections.reviewPolicy}}}return{ok:!0,policy:composed}}function policySuppliesWebhookUrl(policy){let notify=policy.notify;return!notify||typeof notify!="object"||Array.isArray(notify)?!1:"webhook_url"in notify}function projectPolicyForComparison(policy){if(Array.isArray(policy))return policy.map(item=>projectPolicyForComparison(item));if(!policy||typeof policy!="object")return policy;let out={};for(let[key,value]of Object.entries(policy))if(key!=="feature_branch_provisioning"){if(key==="notify"&&value&&typeof value=="object"&&!Array.isArray(value)){let notify={};for(let[nKey,nValue]of Object.entries(value))nKey!=="webhook_url"&&(notify[nKey]=projectPolicyForComparison(nValue));out.notify=notify;continue}out[key]=projectPolicyForComparison(value)}return out}function renderDiffValue(value){let text2=JSON.stringify(value??null);return text2.length>120?`${text2.slice(0,120)}\u2026`:text2}function diffProjectedPolicies(stored,requested,basePath=""){let isPlainObject12=v=>!!v&&typeof v=="object"&&!Array.isArray(v);if(isPlainObject12(stored)&&isPlainObject12(requested)){let keys=[...new Set([...Object.keys(stored),...Object.keys(requested)])].sort(),entries=[];for(let key of keys){let path48=basePath?`${basePath}.${key}`:key,inStored=key in stored,inRequested=key in requested;inStored&&!inRequested?entries.push({path:path48,kind:"removed"}):!inStored&&inRequested?entries.push({path:path48,kind:"added"}):entries.push(...diffProjectedPolicies(stored[key],requested[key],path48))}return entries}return JSON.stringify(stored??null)===JSON.stringify(requested??null)?[]:[{path:basePath||"(policy)",kind:"changed"}]}function renderPolicyDiff(entries,stored,requested){let read=(source,path48)=>{let cursor=source;for(let part of path48.split(".")){if(!cursor||typeof cursor!="object")return;cursor=cursor[part]}return cursor};return entries.map(entry=>entry.kind==="added"?` + ${entry.path}: ${renderDiffValue(read(requested,entry.path))} (not in the stored policy)`:entry.kind==="removed"?` - ${entry.path}: ${renderDiffValue(read(stored,entry.path))} (absent from the policy file)`:` ~ ${entry.path}: stored ${renderDiffValue(read(stored,entry.path))} \u2192 file ${renderDiffValue(read(requested,entry.path))}`)}var WEBHOOK_SUPPLIED_DIFF_LINE=" ~ notify.webhook_url: (supplied \u2014 will replace stored value on apply)";function reconcileReusedRunPolicy(args){let storedProjected=projectPolicyForComparison(args.storedPolicy??{}),requestedProjected=projectPolicyForComparison(args.requestedPolicy),diffLines=renderPolicyDiff(diffProjectedPolicies(storedProjected,requestedProjected),storedProjected,requestedProjected);return policySuppliesWebhookUrl(args.requestedPolicy)&&diffLines.push(WEBHOOK_SUPPLIED_DIFF_LINE),diffLines.length===0?{kind:"noop"}:args.replacePolicy?{kind:"replace",diffLines}:{kind:"refused_divergent",diffLines}}function validateEpicPlanSidecar(parsed){if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,error:"Plan sidecar must be a JSON object."};let plan=parsed,version=plan.plan_version;if(typeof version!="number"||!Number.isInteger(version)||version<1)return{ok:!1,error:`plan_version must be an integer >= 1, got ${JSON.stringify(version)}.`};if(!Array.isArray(plan.nodes)||plan.nodes.length===0)return{ok:!1,error:"plan.nodes must be a non-empty array."};if(!Array.isArray(plan.edges))return{ok:!1,error:"plan.edges must be an array (use [] for none)."};let keys=new Set,warnings=[];for(let node of plan.nodes){if(!node||typeof node!="object")return{ok:!1,error:"Every plan node must be an object."};let key=typeof node.ticket_key=="string"?node.ticket_key.trim():"";if(!key)return{ok:!1,error:"Every plan node needs a non-empty ticket_key."};if(keys.has(key))return{ok:!1,error:`Duplicate ticket_key in plan: ${key}.`};keys.add(key),node.touched_files===void 0&&warnings.push(`Node ${key} has no touched_files, so it opts out of preemptive file-overlap serialization: the server accepts the plan but will not pre-serialize ${key} against overlapping siblings (a conflict would instead be caught reactively and rebased). Add touched_files to ${key} to get that scheduling protection (use [] when no files are predicted).`)}let adjacency=new Map,addEdge=(from,to)=>{let list=adjacency.get(from)??[];list.push(to),adjacency.set(from,list)};for(let node of plan.nodes){let deps=Array.isArray(node.depends_on)?node.depends_on:[];for(let dep of deps){if(!keys.has(dep))return{ok:!1,error:`Node ${node.ticket_key} depends_on unknown ticket '${dep}'.`};addEdge(dep,node.ticket_key)}}for(let edge of plan.edges){if(!edge||typeof edge!="object")return{ok:!1,error:"Every plan edge must be an object."};if(!keys.has(edge.from)||!keys.has(edge.to))return{ok:!1,error:`Edge ${JSON.stringify(edge.from)} -> ${JSON.stringify(edge.to)} references an unknown ticket.`};addEdge(edge.from,edge.to)}let cycle=findCycle(keys,adjacency);return cycle?{ok:!1,error:`Plan DAG has a cycle: ${cycle.join(" -> ")}.`}:{ok:!0,plan:parsed,warnings}}function findCycle(keys,adjacency){let color=new Map;for(let k of keys)color.set(k,0);for(let start of keys){if(color.get(start)!==0)continue;let stack=[{node:start,path:[start]}];for(;stack.length>0;){let{node,path:path48}=stack[stack.length-1];if(color.get(node)===0){color.set(node,1);for(let next of adjacency.get(node)??[]){if(color.get(next)===1)return[...path48,next];color.get(next)===0&&stack.push({node:next,path:[...path48,next]})}}else color.get(node)===1&&color.set(node,2),stack.pop()}}return null}function proposeFeatureBranchName(epicKey){return`epic/${epicKey}`}async function resolveFeatureBranchSelection(opts,repoName,deps,policyDeclaresBranch=!1){if(opts.featureBranch!==void 0)return opts.featureBranch;if(policyDeclaresBranch||!deps.isTTY||opts.json)return;let proposed=proposeFeatureBranchName(opts.epicKey);for(deps.errorLog(""),deps.errorLog(`Feature branch (optional) for epic ${opts.epicKey} on ${repoName}:`),deps.errorLog(` Proposed: ${proposed}`),deps.errorLog(" Strategy: create a new branch from the repository base branch"),deps.errorLog(" Effect: every child-ticket PR will target this branch");;){let answer=(await deps.promptLine(`Use feature branch? 'y' = ${proposed}, a name = custom, Enter = base branch: `)).trim();if(answer==="")return;let lowered=answer.toLowerCase();if(lowered==="n"||lowered==="no")return;if(lowered==="y"||lowered==="yes")return proposed;let reason=validateBranchName(answer);if(reason){deps.errorLog(` Invalid branch name: ${reason} Try again, or press Enter for the base branch.`);continue}return answer}}function errorDetail(err){if(err instanceof ConductorBridgeApiError){let status=err.status!==void 0?` (HTTP ${err.status})`:"",preview=err.bodyPreview?`: ${err.bodyPreview}`:"";return`${err.message}${status}${preview}`}return err instanceof Error?err.message:String(err)}async function applyReusedRunPolicy(args){let outcome2=reconcileReusedRunPolicy({storedPolicy:args.storedPolicy,requestedPolicy:args.requestedPolicy,replacePolicy:args.replacePolicy});if(outcome2.kind==="noop")return args.result.policy_applied="noop",args.say("Policy: policy unchanged"),!0;if(outcome2.kind==="refused_divergent")return args.result.policy_applied="refused_divergent",args.deps.errorLog(`Epic run ${args.epicRunId} is live and its stored policy differs from the policy file. setup-epic will not silently change a running epic's policy.
5423
5423
  ${outcome2.diffLines.join(`
5424
5424
  `)}
5425
5425
  Re-run with --replace-policy to apply the file as the complete replacement, or drop --policy-file to reuse the run unchanged.`),!1;try{await replaceEpicRunPolicy(args.access,{epicRunId:args.epicRunId,policyJson:args.requestedPolicy},args.deps.fetch)}catch(err){return args.result.policy_applied="refused_divergent",args.deps.errorLog(`Failed to replace the run policy: ${errorDetail(err)}`),!1}args.result.policy_applied="replaced",args.say("Policy: policy applied: replaced");for(let line of outcome2.diffLines)args.say(line);return!0}function emitRefusal(deps,opts,result){return opts.json&&deps.log(JSON.stringify(result,null,2)),1}async function runSetupEpicCli(argv,overrides={}){let deps={...createDefaultSetupEpicDeps(),...overrides},parsed=parseSetupEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),deps.errorLog(""),deps.errorLog(getSetupEpicUsage()),1;let opts=parsed.options,say=opts.json?deps.errorLog:deps.log,raw;try{raw=await deps.readFile(opts.planFile)}catch(err){return deps.errorLog(`Could not read plan file '${opts.planFile}': ${errorDetail(err)}`),1}let parsedJson;try{parsedJson=JSON.parse(raw)}catch(err){return deps.errorLog(`Plan file '${opts.planFile}' is not valid JSON: ${errorDetail(err)}`),1}let validation=validateEpicPlanSidecar(parsedJson);if(!validation.ok)return deps.errorLog(`Invalid plan DAG: ${validation.error}`),1;let plan=validation.plan,warnings=[...validation.warnings];if(opts.planVersion!==void 0&&opts.planVersion!==plan.plan_version)return deps.errorLog(`--plan-version ${opts.planVersion} does not match the sidecar's plan_version ${plan.plan_version}. Fix the sidecar (or drop the flag) \u2014 setup-epic never rewrites the blob, because that would change its hash.`),1;let localHash=hashPlan(plan),filePolicy;if(opts.policyFile!==void 0){let policyRead=await readSetupEpicPolicyFile(opts.policyFile,deps.readFile);if(!policyRead.ok)return deps.errorLog(policyRead.error),1;filePolicy=policyRead.policy}let policyDeclaresBranch=filePolicy!==void 0&&["base_branch","baseBranch"].some(key=>key in filePolicy&&filePolicy[key]!==void 0&&filePolicy[key]!==null);if(opts.localOnly){if(filePolicy!==void 0){let composed=composeSetupEpicPolicy(filePolicy,{featureBranch:opts.featureBranch,reviewPolicy:opts.reviewPolicy});if(!composed.ok)return deps.errorLog(composed.error),1}say(`Epic: ${opts.epicKey}`),say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`),say(`Local hash: ${localHash}`);for(let w of warnings)say(` [warn] ${w}`);return say(""),say("Result: partial (local checks only)"),say(" The server's plan validator did NOT run, so this says nothing about whether the server would accept this plan. Re-run without --local-only for authoritative validation."),opts.json&&deps.log(JSON.stringify({dry_run:!0,local_only:!0,result:"partial (local checks only)",epic_key:opts.epicKey,plan_version:plan.plan_version,local_plan_hash:localHash,server_validated:!1,policy_applied:"absent",warnings},null,2)),0}let accessResult=await resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,repoName:opts.repo});if(!accessResult.ok)return deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`),1;let access2=accessResult.access;say(`Epic: ${opts.epicKey}`),say(`Repo: ${access2.repoName}`),say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`),say(`Local hash: ${localHash}`);for(let w of warnings)say(` [warn] ${w}`);let promptedInteractively=opts.featureBranch===void 0&&!policyDeclaresBranch&&deps.isTTY&&!opts.json,featureBranch=await resolveFeatureBranchSelection(opts,access2.repoName,deps,policyDeclaresBranch);featureBranch!==void 0?say(`Feature: ${featureBranch} (create from repository base branch on origin)`):promptedInteractively&&say("Feature: none \u2014 continue using the repository base branch");let requestedPolicy;if(filePolicy!==void 0){let composed=composeSetupEpicPolicy(filePolicy,{featureBranch,reviewPolicy:opts.reviewPolicy});if(!composed.ok)return deps.errorLog(composed.error),1;requestedPolicy=composed.policy,say(`Policy: ${opts.policyFile} (complete run policy, applied at creation)`)}let existingRunId=null,existingStatus=null,existingBaseBranch=null,existingReviewPolicy=null,existingPolicyJson=null;try{let state=await fetchEpicRunState(access2,opts.epicKey,deps.fetch);existingRunId=state.epic_run?.epic_run_id??null,existingStatus=state.epic_run?.status??null;let existingPolicy=state.epic_run?.policy_json;existingPolicyJson=existingPolicy??null;let existingBase=existingPolicy&&typeof existingPolicy=="object"?existingPolicy.base_branch:void 0;existingBaseBranch=typeof existingBase=="string"&&existingBase.trim()!==""?existingBase:null;let existingReview=existingPolicy&&typeof existingPolicy=="object"?existingPolicy.review_policy:void 0,existingReviewSource=existingReview&&typeof existingReview=="object"?existingReview.source:void 0;existingReviewPolicy=typeof existingReviewSource=="string"&&existingReviewSource.trim()!==""?existingReviewSource:null}catch(err){if(err instanceof ConductorBridgeApiError&&err.status===404)existingRunId=null;else return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 it is wedged, and every plan call will keep failing. Abandon the duplicate before retrying:
@@ -5623,7 +5623,7 @@ ${spawnCommand}
5623
5623
  `),candidates.forEach((c,i)=>{process.stderr.write(` [${i}] ${c.serverName} in ${c.filePath}
5624
5624
  `)}),rl.question("Enter the number to migrate (or blank to abort): ",answer=>{rl.close();let trimmed=answer.trim();if(trimmed.length===0){resolve2(null);return}let index=Number.parseInt(trimmed,10);if(Number.isInteger(index)&&index>=0&&index<candidates.length){resolve2(index);return}resolve2(null)})})}function createDefaultCredentialsDeps(writeCredentials){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os20.homedir,readFile:p=>readFile14(p,"utf-8"),mkdir:(p,o)=>mkdir9(p,o),writeFile:(p,d,o)=>writeFile9(p,d,o),rename:(a,b)=>rename3(a,b),chmod:(p,m)=>chmod3(p,m),unlink:p=>unlink3(p),writeCredentials,promptChoice:process.stdin.isTTY?promptChoiceViaReadline:void 0,log:m=>console.log(m),errorLog:m=>console.error(m)}}async function runCredentialsCli(argv,overrides){let parsed=(overrides?.parse??parseCredentialsArgs)(argv),log=overrides?.log??(m=>console.log(m)),errorLog=overrides?.errorLog??(m=>console.error(m));if(parsed.status==="help")return log(getCredentialsUsage()),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getCredentialsUsage()),1;let deps={...createDefaultCredentialsDeps(parsed.writeCredentials),...overrides};overrides?.writeCredentials===void 0&&(deps.writeCredentials=parsed.writeCredentials),overrides?.sources===void 0&&(deps.sources=parsed.sources);let result=await migrateAgentConfigCredentialToStore(deps);if(result.ok)return deps.log(`Stored routing credential for ${result.target} at ${result.path} (migrated from ${result.sourceServerName} in ${result.sourceFilePath}).`),0;if(result.kind==="consent-required"){if(deps.log(result.message),deps.log(""),deps.log("To migrate it, re-run with --write-credentials:"),deps.log(" npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials"),result.candidates&&result.candidates.length>0){deps.log(""),deps.log("Discovered source(s):");for(let candidate of result.candidates)deps.log(` - ${candidate.serverName} in ${candidate.filePath}`)}return 0}return deps.errorLog(`Error: ${result.message}`),1}init_taxonomy();init_errors();init_store();init_git_ci_types();init_pr_ci_producer();import{z as z2}from"zod";init_errors();init_producer_ledger();import*as nodeChildProcess from"node:child_process";import{isAbsolute as pathIsAbsolute}from"node:path";var CONDUCTOR_NODE_PATH_ENV="CONDUCTOR_NODE_PATH",BAPI_CONDUCTOR_CLI_FILE_ENV="BAPI_CONDUCTOR_CLI_FILE",WORKER_LEDGER_CLI_MAX_BUFFER=8*1024*1024,WORKER_LEDGER_CLI_TIMEOUT_MS=3e4,defaultExecFile=(file,args,options,callback)=>nodeChildProcess.execFile(file,args,options,callback);function nonEmpty2(value){return typeof value=="string"&&value.trim().length>0}function resolveWorkerLedgerCliRuntime(deps={}){let env=deps.env??process.env,isAbsolute2=deps.isAbsolute??pathIsAbsolute,nodePathRaw=env[CONDUCTOR_NODE_PATH_ENV];if(!nonEmpty2(nodePathRaw))throw new ConductorLedgerSubprocessRuntimeError("missing",CONDUCTOR_NODE_PATH_ENV);let nodePath=nodePathRaw.trim();if(!isAbsolute2(nodePath))throw new ConductorLedgerSubprocessRuntimeError("invalid",CONDUCTOR_NODE_PATH_ENV);let cliFileRaw=env[BAPI_CONDUCTOR_CLI_FILE_ENV];if(!nonEmpty2(cliFileRaw))throw new ConductorLedgerSubprocessRuntimeError("cli_missing",BAPI_CONDUCTOR_CLI_FILE_ENV);return{nodePath,cliFile:cliFileRaw.trim()}}function execConductorCli(subcommandArgs,stdin,deps={}){let runtime=resolveWorkerLedgerCliRuntime(deps),execFile7=deps.execFile??defaultExecFile,maxBuffer=deps.maxBuffer??WORKER_LEDGER_CLI_MAX_BUFFER,timeout=deps.timeout??WORKER_LEDGER_CLI_TIMEOUT_MS,argv=[runtime.cliFile,...subcommandArgs];return new Promise((resolve2,reject)=>{let child;try{child=execFile7(runtime.nodePath,argv,{maxBuffer,encoding:"utf8",timeout},(error,stdout)=>{if(error){reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",CONDUCTOR_NODE_PATH_ENV));return}resolve2(stdout)})}catch{reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",CONDUCTOR_NODE_PATH_ENV));return}if(stdin!==void 0){child.stdin?.on("error",()=>{});try{child.stdin?.write(stdin),child.stdin?.end()}catch{}}})}function parseCliJsonStdout(stdout){try{return JSON.parse(stdout)}catch{throw new ConductorLedgerSubprocessRuntimeError("malformed_stdout",CONDUCTOR_NODE_PATH_ENV)}}async function checkWorkerMessagesViaCli(input,deps={}){let args=["check-messages","--run-id",input.runId,"--worker-id",input.workerId];input.limit!==void 0&&args.push("--limit",String(input.limit)),args.push("--json");let stdout=await execConductorCli(args,void 0,deps);return parseCliJsonStdout(stdout)}function buildEmitEventArgs(event){let args=["emit-event","--type",event.type,"--source",event.source];return nonEmpty2(event.id??void 0)&&args.push("--id",event.id),nonEmpty2(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty2(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty2(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty2(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty2(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),event.schema_version!==void 0&&args.push("--schema-version",String(event.schema_version)),nonEmpty2(event.time??void 0)&&args.push("--time",event.time),typeof event.confidence=="number"&&args.push("--confidence",String(event.confidence)),args.push("--data-json-stdin","--json"),args}async function emitConductorEventViaCli(event,deps={}){let args=buildEmitEventArgs(event),stdinPayload=JSON.stringify(event.data??{}),stdout=await execConductorCli(args,stdinPayload,deps);return parseCliJsonStdout(stdout)}async function emitConductorEventIfNewViaCli(input,dimensions,deps={}){let dedupeKey=makeProducerDedupeKey(dimensions),eventId=makeStableProducerEventId(dedupeKey),existingData=input.data??{},existingDetails=existingData.details&&typeof existingData.details=="object"&&!Array.isArray(existingData.details)?existingData.details:{},data={...existingData,details:{...existingDetails,dedupe_key:dedupeKey}},result=await emitConductorEventViaCli({...input,id:eventId,data},deps);return result&&result.ok===!1&&result.reason==="duplicate"?{emitted:!1,reason:"duplicate"}:{emitted:!0,event_id:eventId}}init_bridge_api_client();function buildEventTypeZodEnum(){return z2.enum(SEMANTIC_EVENT_TYPES)}var EventFilterSchema=z2.object({type:buildEventTypeZodEnum().optional(),types:z2.array(buildEventTypeZodEnum()).optional(),source:z2.string().optional(),run_id:z2.string().optional(),worker_id:z2.string().optional(),subject:z2.string().optional(),producer:z2.string().optional()}).strict();function jsonResult(value){return{content:[{type:"text",text:JSON.stringify(value,null,2)}]}}function withConductorToolErrorHandling(handler){return async args=>{try{return await handler(args)}catch(error){return jsonResult(toConductorErrorEnvelope(error))}}}function registerEmitEventTool(registerTool2){registerTool2("emit_event",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},description:"Append a semantic coordination event to the LOCAL conductor ledger (~/.config/bridge/events.db). This is a local, append-only event store for multi-agent coordination \u2014 it does NOT call the Bridge API. Only the fixed semantic event taxonomy is accepted (e.g. run.started, agent.notification, ci.passed). Place tool-native fields (branch, commitSha, etc.) under data.raw \u2014 non-allowlisted top-level data keys are rejected. Secrets are redacted before storage and large payloads must be passed by reference (data.payload_ref / data.references).",inputSchema:{source:z2.string().describe("Logical producer of the event (e.g. 'claude-code', 'git-hook')."),type:buildEventTypeZodEnum().describe("Semantic event type from the fixed conductor taxonomy."),subject:z2.string().optional().describe("Optional subject the event is about (e.g. a ticket key)."),run_id:z2.string().optional().describe("Optional run/session identifier this event belongs to."),worker_id:z2.string().optional().describe("Optional worker/agent identifier."),producer:z2.string().optional().describe("Optional finer-grained producer identity."),schema_version:z2.number().int().positive().optional().describe("Event schema version (default 1)."),time:z2.string().optional().describe("Optional ISO-8601 event time (defaults to now)."),data:z2.record(z2.string(),z2.unknown()).optional().describe("Normalized event data. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw. Tool-native fields go under 'raw'."),confidence:z2.number().min(0).max(1).optional().describe("Optional confidence in [0,1]."),observed_via:z2.string().optional().describe("Optional channel the event was observed through.")}},withConductorToolErrorHandling(async args=>{let result=await emitConductorEvent({source:args.source,type:args.type,subject:args.subject,run_id:args.run_id,worker_id:args.worker_id,producer:args.producer,schema_version:args.schema_version,time:args.time,data:args.data??{},confidence:args.confidence,observed_via:args.observed_via});return jsonResult(result)}))}function registerPollEventsTool(registerTool2){registerTool2("poll_events",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Read ordered events from the LOCAL conductor ledger starting at an inclusive 'since_seq' cursor. Returns compact metadata-first summaries by default (data.raw omitted; raw_keys surfaced) and a 'next_seq' cursor to pass on the next call. Set data_mode='full' to retrieve complete (redacted) event data. Local read-only; does not call the Bridge API.",inputSchema:{since_seq:z2.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),filter:EventFilterSchema.optional().describe("Optional allowlisted filter."),data_mode:z2.enum(["summary","full"]).optional().describe("Projection mode (default 'summary')."),limit:z2.number().int().positive().optional().describe("Max events to return (default 100, max 1000).")}},withConductorToolErrorHandling(async args=>{let result=await pollConductorEvents({since_seq:args.since_seq??1,filter:args.filter,data_mode:args.data_mode??"summary",limit:args.limit});return jsonResult(result)}))}function registerWaitForEventTool(registerTool2){registerTool2("wait_for_event",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Long-poll the LOCAL conductor ledger: block up to 'timeout_ms' (bounded, max 120000) until events matching the filter appear at/after 'since_seq'. Returns the same shape as poll_events plus 'timed_out'. SQLite locks are never held between polls. Local read-only; does not call the Bridge API.",inputSchema:{since_seq:z2.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),filter:EventFilterSchema.optional().describe("Optional allowlisted filter."),data_mode:z2.enum(["summary","full"]).optional().describe("Projection mode (default 'summary')."),timeout_ms:z2.number().int().nonnegative().optional().describe("Max wait in ms (bounded, max 120000)."),limit:z2.number().int().positive().optional().describe("Max events to return (default 100, max 1000).")}},withConductorToolErrorHandling(async args=>{let result=await waitForConductorEvent({since_seq:args.since_seq??1,filter:args.filter,data_mode:args.data_mode??"summary",timeout_ms:args.timeout_ms,limit:args.limit});return jsonResult(result)}))}function registerGetSupervisorSnapshotTool(registerTool2){registerTool2("get_supervisor_snapshot",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Read the supervisor projection for a run_id from the LOCAL conductor ledger. The projection is maintained by the conductor supervisor runtime (`conductor supervise --run-id <id>`), which owns the deterministic worker watchdog state; this tool ONLY reads that projection and never derives state from raw events. Returns { run_id, status, projection } where projection is null and status is 'unknown' when no projection exists yet. Local read-only; does not call the Bridge API.",inputSchema:{run_id:z2.string().describe("The run/session identifier to read the supervisor projection for.")}},withConductorToolErrorHandling(async args=>{let result=await getSupervisorSnapshot(args.run_id);return jsonResult(result)}))}function registerGetEpicSnapshotTool(registerTool2){registerTool2("get_epic_snapshot",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the Epic Run snapshot for an epic key from the Bridge API. Returns the full EpicRunState: the epic run record (status, plan version, lease state, budget/consumed), per-ticket status rows, and dispatch rows. Returns { epic_key, status: 'unknown', state: null } if the Epic Run does not exist. Read-only; never triggers a tick, transitions Jira, or mutates Epic state.",inputSchema:{epic_key:z2.string().min(1).describe("The epic identifier to fetch the snapshot for (e.g. EPIC-123).")}},withConductorToolErrorHandling(async args=>{let epicKey=args.epic_key,accessResult=await resolveConductorBridgeApiAccess();if(!accessResult.ok)throw new ConductorValidationError(accessResult.error);try{let result=await fetchEpicRunState(accessResult.access,epicKey);return jsonResult(result)}catch(error){if(error instanceof ConductorBridgeApiError&&error.status===404)return jsonResult({epic_key:epicKey,status:"unknown",state:null});throw error}}))}var SHA_PATTERN=/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/;function registerWaitForDoneGateTool(registerTool2){registerTool2("wait_for_done_gate",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Bounded wait for the conductor done-gate on a pull request. Resolves the PR number + immutable head SHA once, polls CI for that SHA on a clamped interval, and emits conductor events (git.pr_opened, ci.passed/ci.failed, and gate.met when the configured required CI checks are green). This EMITS conductor coordination events only \u2014 it does NOT merge the PR, transition Jira, or mutate any repository state. Fails closed: an unset/disabled/malformed conductor_done_gate config never produces gate.met.",inputSchema:{repo_name:z2.string().optional().describe("Optional repo name override (defaults to BAPI_REPO_NAME/.bridge/config)."),pr_number:z2.number().int().positive().optional().describe("Optional explicit PR number (positive integer)."),head_sha:z2.string().regex(SHA_PATTERN).optional().describe("Optional explicit head SHA (40- or 64-character hex)."),timeout_ms:z2.number().int().nonnegative().optional().describe("Max wait in ms (clamped, max 120000)."),poll_interval_ms:z2.number().int().nonnegative().optional().describe("CI poll interval in ms (clamped)."),worktree_path:z2.string().optional().describe("Optional worktree path to resolve git/PR context from.")}},withConductorToolErrorHandling(async args=>{if(args.pr_number!==void 0&&normalizePrNumber(args.pr_number)===null)throw new ConductorValidationError("'pr_number' must be a positive integer.");if(args.head_sha!==void 0&&normalizeSha(args.head_sha)===null)throw new ConductorValidationError("'head_sha' must be a 40- or 64-character hex SHA.");let result=await waitForDoneGate({repoName:args.repo_name,prNumber:args.pr_number,headSha:args.head_sha,timeoutMs:args.timeout_ms,pollIntervalMs:args.poll_interval_ms,worktreePath:args.worktree_path},{resolveRunId:resolveDispatchRunIdForBinding,emitIfNew:(input,dimensions)=>emitConductorEventIfNewViaCli(input,dimensions)});return jsonResult({gate_met:result.gate_met,timed_out:result.timed_out,reason:result.reason,repo:result.repo,pr_number:result.pr_number,head_sha:result.head_sha,gate_event_summary:result.gate_event_summary})}))}function registerSendMessageTool(registerTool2){registerTool2("send_message",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Enqueue a typed, auditable message for ONE worker through the LOCAL cooperative conductor relay (~/.config/bridge/events.db). The supervisor sends; the worker reads/acknowledges later via check_messages. This is COOPERATIVE \u2014 it does NOT inject into, mutate, or prompt-inject a live worker session. Idempotent: a duplicate idempotency key (run_id+worker_id+type+cause_seq) does not enqueue a second message, and a same-type message inside the cooldown window is suppressed. Local only; does not call the Bridge API.",inputSchema:{run_id:z2.string().min(1).describe("Run/session identifier the message is scoped to."),worker_id:z2.string().min(1).describe("Target worker/agent identifier."),type:z2.string().min(1).describe("Typed message kind (e.g. 'supervisor.worker_stalled')."),cause_seq:z2.number().int().nonnegative().describe("Idempotency cause sequence (the supervisor's last_seq at decision time)."),payload:z2.record(z2.string(),z2.unknown()).optional().default({}).describe("Optional compact payload. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw."),available_at:z2.string().optional().describe("Optional ISO-8601 time the message becomes available (default now)."),cooldown_ms:z2.number().int().nonnegative().optional().describe("Optional per-call cooldown override in ms (falls back to the configured cooldown).")}},withConductorToolErrorHandling(async args=>{let result=await sendWorkerMessage({run_id:args.run_id,worker_id:args.worker_id,type:args.type,cause_seq:args.cause_seq,payload:args.payload??{},available_at:args.available_at,cooldown_ms:args.cooldown_ms});return jsonResult(result)}))}function registerCheckMessagesTool(registerTool2){registerTool2("check_messages",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},description:"Worker checkpoint poll for the LOCAL cooperative conductor relay. Call this at natural checkpoints to read any supervisor messages addressed to this worker. Returned messages are ACKNOWLEDGED by this call and are NOT redelivered on later polls. This is cooperative polling \u2014 it is NOT live prompt injection. run_id/worker_id default to BAPI_CONDUCTOR_RUN_ID / BAPI_CONDUCTOR_WORKER_ID from the environment when omitted. Local only; does not call the Bridge API.",inputSchema:{run_id:z2.string().optional().describe("Run identifier (defaults to BAPI_CONDUCTOR_RUN_ID)."),worker_id:z2.string().optional().describe("Worker identifier (defaults to BAPI_CONDUCTOR_WORKER_ID)."),limit:z2.number().int().positive().max(100).optional().describe("Max messages to deliver/ack (default 10, max 100).")}},withConductorToolErrorHandling(async args=>{let runId=args.run_id??process.env.BAPI_CONDUCTOR_RUN_ID??"",workerId=args.worker_id??process.env.BAPI_CONDUCTOR_WORKER_ID??"";if(runId.trim().length===0||workerId.trim().length===0)throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");let result=await checkWorkerMessagesViaCli({runId,workerId,limit:args.limit});return jsonResult(result)}))}function registerConductorTools(registerTool2){let reg=registerTool2;registerEmitEventTool(reg),registerPollEventsTool(reg),registerWaitForEventTool(reg),registerGetSupervisorSnapshotTool(reg),registerGetEpicSnapshotTool(reg),registerWaitForDoneGateTool(reg),registerSendMessageTool(reg),registerCheckMessagesTool(reg)}import{z as z16}from"zod";var SFCC_VERSIONS=["sfra","pwakit","sitegenesis","storefrontnext","hybrid"],DEFAULT_OCAPI_VERSION="v25_6",AM_HOST="account.demandware.com",AM_TOKEN_URL=`https://${AM_HOST}/dwsso/oauth2/access_token`;async function getSfccVersionConfig(buildGetUrl2,getGetHeaders2,repoName){try{let url=buildGetUrl2("/config-field/version",{repo_name:repoName}),resp=await fetch(url,{headers:await getGetHeaders2()});if(!resp.ok)return null;let value=(await resp.json()).value;return value==null||typeof value!="string"?null:value}catch{return null}}init_git_ignore_utils();import{readFile as readFile15,writeFile as writeFile10,mkdir as mkdir10}from"fs/promises";import path40 from"path";var ENV_HOSTNAME="SFCC_HOSTNAME",ENV_CLIENT_ID="SFCC_CLIENT_ID",ENV_CLIENT_SECRET="SFCC_CLIENT_SECRET",DW_JSON="dw.json";function safeHostLabel(hostname){return hostname.split(".")[0]??hostname}async function resolveSfccCredentials(explicitHostname,env=process.env,deps={}){if(explicitHostname){let clientId2=env[ENV_CLIENT_ID],clientSecret2=env[ENV_CLIENT_SECRET];return!clientId2||!clientSecret2?{ok:!1,error:`Explicit instance '${safeHostLabel(explicitHostname)}' provided but ${ENV_CLIENT_ID} and/or ${ENV_CLIENT_SECRET} are not set in environment. Set them and retry.`}:{ok:!0,credentials:{hostname:explicitHostname,clientId:clientId2,clientSecret:clientSecret2,source:`explicit arg + env (${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`}}}let envHostname=env[ENV_HOSTNAME],envClientId=env[ENV_CLIENT_ID],envClientSecret=env[ENV_CLIENT_SECRET];if(envHostname&&envClientId&&envClientSecret)return{ok:!0,credentials:{hostname:envHostname,clientId:envClientId,clientSecret:envClientSecret,source:`env (${ENV_HOSTNAME}/${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`}};let cwd=deps.cwd??process.cwd(),rf=deps.readFile??(p=>readFile15(p,"utf-8")),wf=deps.writeFile??((p,data)=>writeFile10(p,data,"utf-8")),mk=deps.mkdir??((p,opts)=>mkdir10(p,opts));try{await ensureGitInfoExcluded(cwd,DW_JSON,{readFile:rf,writeFile:wf,mkdir:mk})}catch{}let dwJsonPath=path40.join(cwd,DW_JSON),dwJson;try{let raw=await rf(dwJsonPath);dwJson=JSON.parse(raw)}catch{return{ok:!1,error:"Could not read dw.json. Create a dw.json file in your project root with your SFCC sandbox credentials (hostname, client-id, client-secret)."}}let configs=Array.isArray(dwJson.configs)?dwJson.configs:null;if(configs&&configs.length>1){let instances=configs.map(c=>safeHostLabel(String(c.hostname??c.host??"unknown"))).join(", ");return{ok:!1,error:`dw.json contains multiple sandboxes (${instances}). Pass an explicit 'instance' argument to select one: ${instances}.`}}let cfg=configs&&configs.length===1?configs[0]:dwJson,hostname=String(cfg.hostname??cfg.host??""),clientId=String(cfg["client-id"]??cfg.clientId??cfg.client_id??""),clientSecret=String(cfg["client-secret"]??cfg.clientSecret??cfg.client_secret??"");return!hostname||!clientId||!clientSecret?{ok:!1,error:"dw.json is present but missing required fields (hostname, client-id, client-secret). Ensure all three fields are set."}:{ok:!0,credentials:{hostname,clientId,clientSecret,source:`dw.json (instance: ${safeHostLabel(hostname)})`}}}var KNOWN_WRITE_FAULTS={400:"MalformedKeyParameterException",404:"AttributeDefinitionNotFoundException",409:"IfMatchRequiredException",412:"InvalidIfMatchException"};function extractOcapiFaultType(body){if(body===null||typeof body!="object")return;let record=body,fault=record.fault;if(typeof fault=="string"&&fault.length>0)return fault;if(fault!==null&&typeof fault=="object"){let faultType=fault.type;if(typeof faultType=="string"&&faultType.length>0)return faultType}let type=record.type;if(typeof type=="string"&&type.length>0)return type}function mapOcapiWriteFault(status,body){let faultType=extractOcapiFaultType(body),expected=KNOWN_WRITE_FAULTS[status],known=expected!==void 0&&faultType===expected;return{status,faultType,known,errorCode:known?expected:"OCAPI_WRITE_FAULT"}}function buildSyntheticIfMatchRequiredBody(path48){return{fault:{type:"IfMatchRequiredException",message:`PATCH ${path48} requires an ETag (If-Match) captured from the GET round trip, but the GET response returned no ETag header. Cannot safely issue a conditional PATCH.`}}}var tokenMutex=new Map,tokenCache=new Map;async function getAmToken(credentials){let instanceKey=credentials.hostname,cached=tokenCache.get(instanceKey);if(cached)return cached;let existing=tokenMutex.get(instanceKey);if(existing)return existing;let promise=(async()=>{let body=new URLSearchParams({grant_type:"client_credentials",client_id:credentials.clientId,client_secret:credentials.clientSecret}),resp=await fetch(AM_TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:body.toString()});if(!resp.ok)throw new Error(`AM token acquisition failed: HTTP ${resp.status} for instance ${instanceKey.split(".")[0]}`);let data=await resp.json(),token=typeof data.access_token=="string"?data.access_token:"";if(!token)throw new Error(`AM token acquisition returned no access_token for instance ${instanceKey.split(".")[0]}`);return tokenCache.set(instanceKey,token),token})();tokenMutex.set(instanceKey,promise);try{return await promise}finally{tokenMutex.delete(instanceKey)}}function invalidateAmToken(credentials){tokenCache.delete(credentials.hostname)}function buildOcapiUrl(hostname,ocapiVersion,path48){return`${`https://${hostname}/s/-/dw/data/${ocapiVersion}`}${path48.startsWith("/")?path48:"/"+path48}`}async function parseOcapiResponse(resp){try{return await resp.json()}catch{return null}}function captureOcapiHeaders(resp){let headers={},respHeaders=resp.headers;return respHeaders&&typeof respHeaders.forEach=="function"?(respHeaders.forEach((value,key)=>{headers[key.toLowerCase()]=value}),{headers,etag:respHeaders.get("etag")}):{headers,etag:null}}function sleep2(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}var MAX_RETRY_AFTER_MS=5e3;function parseRetryAfterMs2(headerValue){if(!headerValue)return;let trimmed=headerValue.trim();if(trimmed==="")return;if(/^\d+$/.test(trimmed))return Number(trimmed)*1e3;let dateMs=Date.parse(trimmed);if(!Number.isNaN(dateMs)){let delta=dateMs-Date.now();return delta>0?delta:0}}var BACKOFF_SCHEDULE_MS=[250,500,1e3];async function fetchWith429Backoff(url,init){let resp=await fetch(url,init);for(let attempt=0;attempt<BACKOFF_SCHEDULE_MS.length;attempt++){if(resp.status!==429)return resp;let retryAfterHeader=resp.headers&&typeof resp.headers.get=="function"?resp.headers.get("retry-after"):null,retryAfter=parseRetryAfterMs2(retryAfterHeader),backoff=retryAfter!==void 0?Math.min(retryAfter,MAX_RETRY_AFTER_MS):BACKOFF_SCHEDULE_MS[attempt];await sleep2(backoff),resp=await fetch(url,init)}return resp}async function ocapiRequest(method,path48,body,credentials,ocapiVersion,extraHeaders){let doRequest=async()=>{let token=await getAmToken(credentials),url=buildOcapiUrl(credentials.hostname,ocapiVersion,path48),init={method,headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/json",...extraHeaders??{}}};body!==void 0&&(init.body=JSON.stringify(body));let resp=await fetchWith429Backoff(url,init),status=resp.status,respBody=await parseOcapiResponse(resp),{headers,etag}=captureOcapiHeaders(resp),result={ok:resp.ok,status,body:respBody,headers,etag};return(method==="PUT"||method==="PATCH")&&resp.ok&&(status===201?result.outcome="created":status===200&&(result.outcome="updated")),resp.ok||(result.fault=mapOcapiWriteFault(status,respBody)),result},first=await doRequest();return first.status===401?(invalidateAmToken(credentials),doRequest()):first}async function ocapiGet(path48,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("GET",path48,void 0,credentials,ocapiVersion)}async function ocapiPost(path48,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("POST",path48,body,credentials,ocapiVersion)}async function ocapiPut(path48,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("PUT",path48,body,credentials,ocapiVersion)}async function ocapiPatch(path48,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){let getResult=await ocapiGet(path48,credentials,ocapiVersion);if(!getResult.ok)return getResult;let etag=getResult.etag;if(etag==null||etag.trim()===""){let syntheticBody=buildSyntheticIfMatchRequiredBody(path48);return{ok:!1,status:409,body:syntheticBody,fault:mapOcapiWriteFault(409,syntheticBody)}}return ocapiRequest("PATCH",path48,body,credentials,ocapiVersion,{"If-Match":etag})}async function ocapiPatchDirect(path48,body,credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){return ocapiRequest("PATCH",path48,body,credentials,ocapiVersion)}async function sfccSetupStatusTool(deps){let lines=[`## SFCC Setup Status
5625
5625
  `],apiKeyOk=!!deps.apiKey;lines.push(`1. Bridge API Key: ${apiKeyOk?"\u2713 Resolved":"\u2717 Missing (set BAPI_API_KEY)"}`);let repoOk=!!deps.repoName;lines.push(`2. Repo Name: ${repoOk?`\u2713 Set (${deps.repoName})`:"\u2717 Not set (set BAPI_REPO_NAME)"}`);let versionStatus="\u2717 Not set",resolvedVersion=null;if(apiKeyOk&&repoOk)try{resolvedVersion=await getSfccVersionConfig(deps.buildGetUrl,deps.getGetHeaders,deps.repoName),resolvedVersion===null?versionStatus="\u2717 Not set (configure the 'version' field in Bridge API project settings)":SFCC_VERSIONS.includes(resolvedVersion)?versionStatus=`\u2713 '${resolvedVersion}'`:versionStatus=`\u2717 '${resolvedVersion}' is not an SFCC version (expected: ${SFCC_VERSIONS.join(", ")})`}catch{versionStatus="\u2717 Could not read (Bridge API error)"}else versionStatus="\u2014 Skipped (Bridge API not configured)";lines.push(`3. SFCC Version: ${versionStatus}`);let credStatus="\u2717 Missing",resolvedCredentials=null;try{let result=await resolveSfccCredentials();result.ok?(resolvedCredentials={hostname:result.credentials.hostname.split(".")[0]??result.credentials.hostname,source:result.credentials.source},credStatus=`\u2713 Found (${resolvedCredentials.source})`):credStatus=`\u2717 ${result.error}`}catch(err){credStatus=`\u2717 Resolution error: ${err instanceof Error?err.message:String(err)}`}lines.push(`4. dw.json / Credentials: ${credStatus}`);let tokenStatus="\u2014 Skipped (credentials not available)";if(resolvedCredentials)try{let credResult=await resolveSfccCredentials();credResult.ok?(await getAmToken(credResult.credentials),tokenStatus=`\u2713 Token acquired for instance ${resolvedCredentials.hostname}`):tokenStatus="\u2717 Credentials not resolved"}catch(err){tokenStatus=`\u2717 ${err instanceof Error?err.message:String(err)}`}lines.push(`5. AM Token (OCAPI): ${tokenStatus}`);let logQueryStatus="\u2014 Skipped (Bridge API not configured)";if(apiKeyOk&&repoOk)try{let url=deps.buildGetUrl("/sfcc/logs/capability",{repo_name:deps.repoName}),resp=await fetch(url,{headers:await deps.getGetHeaders()});if(!resp.ok)logQueryStatus=`\u2717 Could not read (Bridge API ${resp.status})`;else{let body=await resp.json();body?.configured===!0?logQueryStatus="\u2713 Configured (WebDAV log access ready)":logQueryStatus=`\u2717 ${typeof body?.message=="string"?body.message:"Not configured"}`}}catch(err){logQueryStatus=`\u2717 Resolution error: ${err instanceof Error?err.message:String(err)}`}return lines.push(`6. SFCC Log Query (WebDAV): ${logQueryStatus}`),lines.push("\nRun `check_permissions` to probe OCAPI access once steps 1\u20135 are all green. Step 6 (log/WebDAV access) is independent and gates `sfcc_log_query`."),{content:[{type:"text",text:lines.join(`
5626
- `)}]}}function buildSfccSetupStatusHandler(buildGetUrl2,getGetHeaders2,repoName,getApiKey){return async _args=>{let apiKey=await getApiKey();return sfccSetupStatusTool({buildGetUrl:buildGetUrl2,getGetHeaders:getGetHeaders2,repoName,apiKey})}}function isPlainObject5(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeDetails(details){return details===void 0?{}:isPlainObject5(details)?{...details}:{value:details}}function formatSfccFailure(input){let details=normalizeDetails(input.details);input.upstreamHttpStatus!==void 0&&input.upstreamHttpStatus!==input.status&&(details.upstream_http_status=input.upstreamHttpStatus);let envelope={error:{code:input.code,status:input.status,message:input.message,source:input.source,details}};return{isError:!0,content:[{type:"text",text:JSON.stringify(envelope,null,2)}]}}function formatSfccReadFailure(input){return formatSfccFailure(input)}function formatSfccWriteFailure(input){return formatSfccFailure(input)}var NO_RETRY_INSTRUCTION="This repository is not configured for SFCC; do not retry SFCC tools.";function notConfigured(failureClass,message){return formatSfccFailure({code:"NOT_CONFIGURED",status:503,message:`${message} ${NO_RETRY_INSTRUCTION}`,source:"gate",details:{failure_class:failureClass}})}function withSfccGate(deps,handler){return async args=>{let version=await getSfccVersionConfig(deps.buildGetUrl,deps.getGetHeaders,deps.repoName);if(version===null)return notConfigured("bridge-auth","Could not read the SFCC version from Bridge API (/config-field/version). Ensure your Bridge API key is set and the repo is configured. Run sfcc_setup_status for a full diagnostic.");if(!SFCC_VERSIONS.includes(version))return notConfigured("version-not-sfcc",`Repo version '${version}' is not an SFCC version. Expected one of: ${SFCC_VERSIONS.join(", ")}. Update the version field in your Bridge API project settings.`);let rawInstance=typeof args.instance=="string"?args.instance:void 0,explicitHostname=rawInstance&&rawInstance.includes(".")?rawInstance:void 0,credResult=await resolveSfccCredentials(explicitHostname);return credResult.ok?handler(args,credResult.credentials):notConfigured("missing-dw-json",`SFCC credential resolution failed: ${credResult.error} Ensure a dw.json file exists in your project root with hostname, client-id, and client-secret fields.`)}}function buildPageMeta(items,rawTotal,rawNext){let page={returned:items.length,has_more:rawNext!=null};return typeof rawTotal=="number"&&(page.total=rawTotal),page}function normalizeOcapiPage(body){if(body===null||typeof body!="object"||!Array.isArray(body.data)||typeof body.count!="number")return null;let envelope=body;return{items:envelope.data,page:buildPageMeta(envelope.data,envelope.total,envelope.next)}}function normalizeOcapiBody(body){if(Array.isArray(body))return{items:body,page:buildPageMeta(body,void 0,null)};if(body!==null&&typeof body=="object"){let obj=body;if(Array.isArray(obj.data)){let items2=obj.data;return{items:items2,page:buildPageMeta(items2,obj.total,obj.next)}}let items=[body];return{items,page:buildPageMeta(items,obj.total,obj.next)}}return{items:[],page:buildPageMeta([],void 0,null)}}var OCAPI_WRITE_RESOURCE_IDS=["/system_object_definitions","/system_object_definitions/**","/custom_object_definitions/**","/site_preferences/**"];function buildOcapiWriteGrantSettings(ocapiVersion,clientIdPlaceholder="<YOUR_CLIENT_ID>"){return{_v:ocapiVersion,clients:[{client_id:clientIdPlaceholder,resources:OCAPI_WRITE_RESOURCE_IDS.map(resource_id=>({resource_id,methods:["get","put","patch","delete"],read_attributes:"(**)",write_attributes:"(**)"}))}]}}function formatOcapiWriteGrantJson(ocapiVersion,clientIdPlaceholder="<YOUR_CLIENT_ID>"){return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion,clientIdPlaceholder),null,2)}var OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH="Administration > Site Development > Open Commerce API Settings \u2192 Data API tab",OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION="Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.";function ocapiWriteForbiddenMessage(operation,path48){return`OCAPI write access denied for ${operation} ${path48}. The client is not granted write access to this resource. See error.details.remediation for the exact write-grant settings to paste in Business Manager.`}function writeGrantForbiddenResult(params){let{operation,path:path48,ocapiVersion,body}=params;return formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:403,message:ocapiWriteForbiddenMessage(operation,path48),source:"ocapi",details:{operation,path:path48,remediation:{business_manager_path:OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH,instruction:OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION,write_grant:buildOcapiWriteGrantSettings(ocapiVersion)},...body===void 0?{}:{body}}})}var OCAPI_SETTINGS_READ_ONLY=ocapiVersion=>JSON.stringify({_v:ocapiVersion,clients:[{client_id:"<YOUR_CLIENT_ID>",resources:[{resource_id:"/system_object_definitions",methods:["get"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/system_object_definitions/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/site_preferences/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/custom_object_definitions/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"}]}]},null,2),OCAPI_SETTINGS_WRITE_IMPORT=ocapiVersion=>formatOcapiWriteGrantJson(ocapiVersion);async function checkPermissionsTool(credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){let result;try{result=await ocapiGet("/system_object_definitions",credentials,ocapiVersion)}catch(err){return{content:[{type:"text",text:`OCAPI probe failed: ${err instanceof Error?err.message:String(err)}
5626
+ `)}]}}function buildSfccSetupStatusHandler(buildGetUrl2,getGetHeaders2,repoName,getApiKey){return async _args=>{let apiKey=await getApiKey();return sfccSetupStatusTool({buildGetUrl:buildGetUrl2,getGetHeaders:getGetHeaders2,repoName,apiKey})}}function isPlainObject5(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeDetails(details){return details===void 0?{}:isPlainObject5(details)?{...details}:{value:details}}function formatSfccFailure(input){let details=normalizeDetails(input.details);input.upstreamHttpStatus!==void 0&&input.upstreamHttpStatus!==input.status&&(details.upstream_http_status=input.upstreamHttpStatus);let envelope={error:{code:input.code,status:input.status,message:input.message,source:input.source,details}};return{isError:!0,content:[{type:"text",text:JSON.stringify(envelope,null,2)}]}}function formatSfccReadFailure(input){return formatSfccFailure(input)}function formatSfccWriteFailure(input){return formatSfccFailure(input)}var NO_RETRY_INSTRUCTION="This repository is not configured for SFCC; do not retry SFCC tools.";function notConfigured(failureClass,message){return formatSfccFailure({code:"NOT_CONFIGURED",status:503,message:`${message} ${NO_RETRY_INSTRUCTION}`,source:"gate",details:{failure_class:failureClass}})}function withSfccGate(deps,handler){return async args=>{let version=await getSfccVersionConfig(deps.buildGetUrl,deps.getGetHeaders,deps.repoName);if(version===null)return notConfigured("bridge-auth","Could not read the SFCC version from Bridge API (/config-field/version). Ensure your Bridge API key is set and the repo is configured. Run sfcc_setup_status for a full diagnostic.");if(!SFCC_VERSIONS.includes(version))return notConfigured("version-not-sfcc",`Repo version '${version}' is not an SFCC version. Expected one of: ${SFCC_VERSIONS.join(", ")}. Update the version field in your Bridge API project settings.`);let rawInstance=typeof args.instance=="string"?args.instance:void 0,explicitHostname=rawInstance&&rawInstance.includes(".")?rawInstance:void 0,credResult=await resolveSfccCredentials(explicitHostname);return credResult.ok?handler(args,credResult.credentials):notConfigured("missing-dw-json",`SFCC credential resolution failed: ${credResult.error} Ensure a dw.json file exists in your project root with hostname, client-id, and client-secret fields.`)}}function buildPageMeta(items,rawTotal,rawNext){let page={returned:items.length,has_more:rawNext!=null};return typeof rawTotal=="number"&&(page.total=rawTotal),page}function normalizeOcapiPage(body){if(body===null||typeof body!="object"||!Array.isArray(body.data)||typeof body.count!="number")return null;let envelope=body;return{items:envelope.data,page:buildPageMeta(envelope.data,envelope.total,envelope.next)}}function normalizeOcapiBody(body){if(Array.isArray(body))return{items:body,page:buildPageMeta(body,void 0,null)};if(body!==null&&typeof body=="object"){let obj=body;if(Array.isArray(obj.data)){let items2=obj.data;return{items:items2,page:buildPageMeta(items2,obj.total,obj.next)}}let items=[body];return{items,page:buildPageMeta(items,void 0,obj.next)}}return{items:[],page:buildPageMeta([],void 0,null)}}var OCAPI_WRITE_RESOURCE_IDS=["/system_object_definitions","/system_object_definitions/**","/custom_object_definitions/**","/site_preferences/**"];function buildOcapiWriteGrantSettings(ocapiVersion,clientIdPlaceholder="<YOUR_CLIENT_ID>"){return{_v:ocapiVersion,clients:[{client_id:clientIdPlaceholder,resources:OCAPI_WRITE_RESOURCE_IDS.map(resource_id=>({resource_id,methods:["get","put","patch","delete"],read_attributes:"(**)",write_attributes:"(**)"}))}]}}function formatOcapiWriteGrantJson(ocapiVersion,clientIdPlaceholder="<YOUR_CLIENT_ID>"){return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion,clientIdPlaceholder),null,2)}var OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH="Administration > Site Development > Open Commerce API Settings \u2192 Data API tab",OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION="Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.";function ocapiWriteForbiddenMessage(operation,path48){return`OCAPI write access denied for ${operation} ${path48}. The client is not granted write access to this resource. See error.details.remediation for the exact write-grant settings to paste in Business Manager.`}function writeGrantForbiddenResult(params){let{operation,path:path48,ocapiVersion,body}=params;return formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:403,message:ocapiWriteForbiddenMessage(operation,path48),source:"ocapi",details:{operation,path:path48,remediation:{business_manager_path:OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH,instruction:OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION,write_grant:buildOcapiWriteGrantSettings(ocapiVersion)},...body===void 0?{}:{body}}})}var OCAPI_SETTINGS_READ_ONLY=ocapiVersion=>JSON.stringify({_v:ocapiVersion,clients:[{client_id:"<YOUR_CLIENT_ID>",resources:[{resource_id:"/system_object_definitions",methods:["get"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/system_object_definitions/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/site_preferences/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"},{resource_id:"/custom_object_definitions/**",methods:["get","post"],read_attributes:"(**)",write_attributes:"(**)"}]}]},null,2),OCAPI_SETTINGS_WRITE_IMPORT=ocapiVersion=>formatOcapiWriteGrantJson(ocapiVersion);async function checkPermissionsTool(credentials,ocapiVersion=DEFAULT_OCAPI_VERSION){let result;try{result=await ocapiGet("/system_object_definitions",credentials,ocapiVersion)}catch(err){return{content:[{type:"text",text:`OCAPI probe failed: ${err instanceof Error?err.message:String(err)}
5627
5627
 
5628
5628
  `+ocapiSettingsInstructions(ocapiVersion)}]}}if(result.ok){let normalized=normalizeOcapiPage(result.body);return{content:[{type:"text",text:`\u2713 OCAPI access confirmed.
5629
5629
  Instance: ${credentials.hostname.split(".")[0]}
@@ -5640,8 +5640,8 @@ ${OCAPI_SETTINGS_READ_ONLY(ocapiVersion)}
5640
5640
  --- WRITE/IMPORT GRANTS (v2 \u2014 forward-looking, paste once) ---
5641
5641
  ${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}
5642
5642
 
5643
- Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path42 from"path";import{z as z5}from"zod";import{z as z3}from"zod";var SFCC_INTERNAL_ERROR_MESSAGE="An unexpected internal error occurred while handling this SFCC read. The underlying failure detail is withheld from this response to avoid leaking credentials or internal state.",SFCC_VALIDATION_ERROR_MESSAGE="Arguments failed schema validation before any OCAPI call was made. See error.details.issues for the offending fields.";function ocapiFallbackMessage(status){return`The SFCC OCAPI Data API returned HTTP ${status} for this read and supplied no fault message. The upstream response body is preserved under error.details.`}function isPlainObject6(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function formatOcapiReadFailure(result,upstreamHttpStatus){return formatSfccFailure({code:"OCAPI_FAULT",status:result.status,message:ocapiFaultMessage(result.body,result.status),source:"ocapi",details:result.body,upstreamHttpStatus})}function ocapiFaultMessage(body,status){if(isPlainObject6(body)&&isPlainObject6(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiFallbackMessage(status)}function isSchemaFailure(err){return err instanceof z3.ZodError?!0:typeof err=="object"&&err!==null&&err.name==="ZodError"&&Array.isArray(err.issues)}function safeZodIssues(err){return(Array.isArray(err.issues)?err.issues:[]).map(issue=>({path:Array.isArray(issue.path)?issue.path.map(String).join("."):"",code:typeof issue.code=="string"?issue.code:"invalid",message:typeof issue.message=="string"?issue.message:"Invalid input."}))}function withSfccReadErrorBoundary(handler){return async(...args)=>{try{return await handler(...args)}catch(err){return isSchemaFailure(err)?formatSfccFailure({code:"VALIDATION_ERROR",status:400,message:SFCC_VALIDATION_ERROR_MESSAGE,source:"tool",details:{issues:safeZodIssues(err)}}):formatSfccFailure({code:"INTERNAL_ERROR",status:500,message:SFCC_INTERNAL_ERROR_MESSAGE,source:"tool",details:{}})}}}import{z as z4}from"zod";var FULL_SELECT_EXPRESSION="(**)",PROJECTION_POLICY={attribute_search:{defaultProjection:"full",placement:"body",inputDescription:"Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only."},attribute_collection:{defaultProjection:"full",placement:"query",inputDescription:"Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only."},broad_list:{defaultProjection:"lean",placement:"query",inputDescription:"Detail level. Default: lean (ids only); full returns all fields."}};function resolveReadProjection(category,requested){return requested??PROJECTION_POLICY[category].defaultProjection}function projectionPlacementFor(category){return PROJECTION_POLICY[category].placement}function selectExpressionFor(projection){return projection==="full"?FULL_SELECT_EXPRESSION:void 0}function applyProjectionToBody(body,category,requested){if(projectionPlacementFor(category)!=="body")throw new Error(`SFCC read category "${category}" carries its projection in the query string, not the POST body.`);let expression=selectExpressionFor(resolveReadProjection(category,requested));return expression!==void 0&&(body.select=expression),body}function projectionQueryEntries(category,requested){if(projectionPlacementFor(category)!=="query")throw new Error(`SFCC read category "${category}" carries its projection in the POST body, not the query string.`);let expression=selectExpressionFor(resolveReadProjection(category,requested));return expression===void 0?{}:{select:expression}}function projectionInputFor(category){return z4.enum(["full","lean"]).optional().describe(PROJECTION_POLICY[category].inputDescription)}import path41 from"path";import{mkdir as mkdir11,writeFile as writeFile11}from"fs/promises";var SFCC_MAX_INLINE=5e4;async function truncateAndSaveIfNeeded(text2,dir,filename,page,deps={}){if(text2.length<=SFCC_MAX_INLINE)return text2;let mk=deps.mkdir??mkdir11,wf=deps.writeFile??writeFile11,filePath=path41.join(dir,filename);try{await mk(dir,{recursive:!0}),await wf(filePath,text2,"utf-8")}catch(err){let descriptor2={truncated:!1,save_failed:!0,oversized:!0,warning:`Response was NOT truncated because the local save failed: ${err instanceof Error?err.message:String(err)}. The complete payload is returned inline.`,page};try{descriptor2.data=JSON.parse(text2)}catch{descriptor2.data_text=text2}return JSON.stringify(descriptor2)}return JSON.stringify({truncated:!0,saved_path:filePath,page})}var READ_ANNOTATIONS={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},systemObjectListInput=z5.object({count:z5.number().optional().describe("Maximum number of system object types to return."),start:z5.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")}),systemObjectGetInput=z5.object({object_type:z5.string().describe('System object type identifier, e.g. "Product" or "Order".')}),systemObjectAttributeSearchInput=z5.object({object_type:z5.string().describe('System object type to search within, e.g. "Order".'),query:z5.union([z5.string(),z5.record(z5.string(),z5.any())]).describe("Search query. A plain string is a case-insensitive substring match across id and display_name; a structured OCAPI query object passes through unchanged."),start:z5.number().optional().describe("Zero-based offset for paging."),count:z5.number().optional().describe("Maximum number of results to return."),sorts:z5.array(z5.any()).optional().describe("Array of OCAPI sort descriptors."),projection:projectionInputFor("attribute_search")});function safeTimestamp(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult(output)}function buildSystemObjectListHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{count,start,projection}=systemObjectListInput.parse(args),queryParams={...projectionQueryEntries("broad_list",projection)};count!==void 0&&(queryParams.count=String(count)),start!==void 0&&(queryParams.start=String(start));let queryStr=Object.keys(queryParams).length>0?"?"+new URLSearchParams(queryParams).toString():"",result=await ocapiGet(`/system_object_definitions${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-list-${safeTimestamp()}.json`,normalized.page)}))}function buildSystemObjectGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type}=systemObjectGetInput.parse(args),encodedType=encodeURIComponent(object_type),result=await ocapiGet(`/system_object_definitions/${encodedType}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`,normalized.page)}))}function buildSystemObjectAttributeSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,query,start,count,sorts,projection}=systemObjectAttributeSearchInput.parse(args),encodedType=encodeURIComponent(object_type),postBody={query:typeof query=="string"?{text_query:{fields:["id","display_name"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts),applyProjectionToBody(postBody,"attribute_search",projection);let result=await ocapiPost(`/system_object_definitions/${encodedType}/attribute_definition_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`,normalized.page)}))}function registerSystemObjectReadTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("system_object_list",{description:"List all system object types from the developer sandbox via GET /system_object_definitions. Read-only. Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). Returns ids only by default; pass projection=full for all fields. Oversized outputs are auto-saved locally.",inputSchema:systemObjectListInput,annotations:READ_ANNOTATIONS},buildSystemObjectListHandler(gateDeps,getDocsDir2)),registerTool2("system_object_get",{description:"Retrieve a system object type definition from the developer sandbox. Read-only. GET /system_object_definitions/{type}. Prefer system_object_attribute_search for targeted attribute lookups. Oversized payloads are auto-saved locally.",inputSchema:systemObjectGetInput,annotations:READ_ANNOTATIONS},buildSystemObjectGetHandler(gateDeps,getDocsDir2)),registerTool2("system_object_attribute_search",{description:"Search attribute definitions for a system object type. Read-only. POST /system_object_definitions/{type}/attribute_definition_search. Use for targeted c_ attribute lookups; returns each attribute's full schema by default. Pass a plain string for text search or a structured OCAPI query. Oversized results auto-saved locally.",inputSchema:systemObjectAttributeSearchInput,annotations:READ_ANNOTATIONS},buildSystemObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path43 from"path";import{z as z6}from"zod";var READ_ANNOTATIONS2={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},customObjectAttributesGetInput=z6.object({object_type:z6.string().describe('Known custom object type identifier, e.g. a custom type id starting with "c_". OCAPI cannot enumerate custom object types \u2014 the type must already be known.'),projection:projectionInputFor("attribute_collection")}),customObjectAttributeSearchInput=z6.object({object_type:z6.string().describe('Known custom object type to search within, e.g. a custom type id starting with "c_". OCAPI cannot enumerate custom object types \u2014 the type must already be known.'),query:z6.union([z6.string(),z6.record(z6.string(),z6.any())]).describe("Search query. A plain string is a case-insensitive substring match across id and display_name; a structured OCAPI query object passes through unchanged."),start:z6.number().optional().describe("Zero-based offset for paging."),count:z6.number().optional().describe("Maximum number of results to return."),sorts:z6.array(z6.any()).optional().describe("Array of OCAPI sort descriptors."),projection:projectionInputFor("attribute_search")});function safeTimestamp2(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType2(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult2(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn2(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult2(output)}function buildCustomObjectAttributesGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,projection}=customObjectAttributesGetInput.parse(args),encodedType=encodeURIComponent(object_type),queryEntries=projectionQueryEntries("attribute_collection",projection),queryStr=Object.keys(queryEntries).length>0?"?"+new URLSearchParams(queryEntries).toString():"",result=await ocapiGet(`/custom_object_definitions/${encodedType}/attribute_definitions${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path43.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text2,dir,`custom-object-def-attributes-${safeType2(object_type)}-${safeTimestamp2()}.json`,normalized.page)}))}function buildCustomObjectAttributeSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,query,start,count,sorts,projection}=customObjectAttributeSearchInput.parse(args),encodedType=encodeURIComponent(object_type),postBody={query:typeof query=="string"?{text_query:{fields:["id","display_name"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts),applyProjectionToBody(postBody,"attribute_search",projection);let result=await ocapiPost(`/custom_object_definitions/${encodedType}/attribute_definition_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path43.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text2,dir,`custom-object-def-search-${safeType2(object_type)}-${safeTimestamp2()}.json`,normalized.page)}))}function registerSfccCustomObjectDefReadTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("custom_object_definition_attributes_get",{description:"Retrieve attribute definitions for a KNOWN custom object type via GET /custom_object_definitions/{type}/attribute_definitions. Returns each attribute's full schema by default. OCAPI cannot enumerate types \u2014 object_type is required. Type creation is v2 metadata-import only. Oversized payloads auto-saved locally.",inputSchema:customObjectAttributesGetInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributesGetHandler(gateDeps,getDocsDir2)),registerTool2("custom_object_definition_attribute_search",{description:"Search attribute definitions within a KNOWN custom object type via POST /custom_object_definitions/{type}/attribute_definition_search. Returns each attribute's full schema by default. OCAPI cannot enumerate types; object_type is required. Pass a string or OCAPI query. Type creation is v2 metadata-import only. Oversized results auto-saved locally.",inputSchema:customObjectAttributeSearchInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path44 from"path";import{z as z7}from"zod";var READ_ANNOTATIONS3={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},INSTANCE_ENUM=z7.enum(["staging","development","sandbox","production"]),INSTANCE_DESCRIBE="OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected with a validation error. Defaults to 'sandbox'.",sitePreferenceGetInput=z7.object({group:z7.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),start:z7.number().optional().describe("Zero-based offset for paging."),count:z7.number().optional().describe("Maximum number of preferences to return.")}),sitePreferenceSearchInput=z7.object({group:z7.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),query:z7.union([z7.string(),z7.record(z7.string(),z7.any())]).describe("Search query. Pass a plain string for text search across preference ids, or a structured OCAPI query object (term_query, filtered_query, etc.)."),start:z7.number().optional().describe("Zero-based offset for paging."),count:z7.number().optional().describe("Maximum number of results to return."),sorts:z7.array(z7.any()).optional().describe("Array of OCAPI sort descriptors.")}),sitePreferenceGroupListInput=z7.object({count:z7.number().optional().describe("Maximum number of site preference groups to return."),start:z7.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")});function safeTimestamp3(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeGroup(group){return encodeURIComponent(group).replace(/%/g,"_")}function textResult3(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn3(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult3(output)}function rejectIfNotSandbox(instance){return instance!=="sandbox"?formatSfccReadFailure({code:"VALIDATION_ERROR",status:400,message:`The Bridge site-preference tool only supports the 'sandbox' instance context. Received: '${instance}'.`,source:"tool",details:{guard:"sandbox_only",received_instance:instance,supported_instances:["sandbox"]}}):null}function buildSitePreferenceGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{group,instance,start,count}=sitePreferenceGetInput.parse(args),guard=rejectIfNotSandbox(instance);if(guard)return guard;let encodedGroup=encodeURIComponent(group),postBody={query:{match_all_query:{}}};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count);let result=await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-get-${safeGroup(group)}-${safeTimestamp3()}.json`,normalized.page)}))}function buildSitePreferenceSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{group,instance,query,start,count,sorts}=sitePreferenceSearchInput.parse(args),guard=rejectIfNotSandbox(instance);if(guard)return guard;let encodedGroup=encodeURIComponent(group),postBody={query:typeof query=="string"?{text_query:{fields:["id"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts);let result=await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-search-${safeGroup(group)}-${safeTimestamp3()}.json`,normalized.page)}))}function buildSitePreferenceGroupListHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{count,start,projection}=sitePreferenceGroupListInput.parse(args),queryParams={...projectionQueryEntries("broad_list",projection)};count!==void 0&&(queryParams.count=String(count)),start!==void 0&&(queryParams.start=String(start));let queryStr=Object.keys(queryParams).length>0?"?"+new URLSearchParams(queryParams).toString():"",result=await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=normalizeOcapiBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-group-list-${safeTimestamp3()}.json`,normalized.page)}))}function registerSitePreferenceTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("site_preference_group_list",{description:"List custom site preference group ids from the developer sandbox via GET /system_object_definitions/SitePreferences/attribute_groups. Read-only; use this to discover group ids before site_preference_get or site_preference_search. Accepts optional `count` and `start` for paging. Oversized outputs are auto-saved locally.",inputSchema:sitePreferenceGroupListInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceGroupListHandler(gateDeps,getDocsDir2)),registerTool2("site_preference_get",{description:"List preference IDENTIFIERS for a site preference group from the developer sandbox via a match-all POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Returns ids only, NOT values \u2014 'unset' and 'empty string' are indistinguishable. Read-only, sandbox-only. Optional start/count paging. Oversized payloads auto-saved locally.",inputSchema:sitePreferenceGetInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceGetHandler(gateDeps,getDocsDir2)),registerTool2("site_preference_search",{description:"Search preference IDENTIFIERS within a site preference group via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Returns ids only \u2014 this endpoint does not expose values. Read-only; v1 sandbox only. Pass a plain string for id-only text search or a structured OCAPI query. Oversized results auto-saved locally.",inputSchema:sitePreferenceSearchInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceSearchHandler(gateDeps,getDocsDir2))}import{z as z10}from"zod";function rejectIfNotSandboxForWrite(instance){let effective=instance===void 0?"sandbox":instance;return effective==="sandbox"?null:formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message:`SFCC write tools are sandbox-only. Refusing to write against instance '${effective}'. Re-run the write against a developer sandbox instance.`,source:"tool",details:{guard:"sandbox_only",received_instance:effective,supported_instances:["sandbox"]}})}function textResult4(text2){return{content:[{type:"text",text:text2}]}}function isPlainObject7(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function ocapiWriteFallbackMessage(status){return`The SFCC OCAPI Data API returned HTTP ${status} for this write and supplied no fault message. The upstream response body is preserved under error.details.`}function ocapiWriteFaultMessage(body,status){if(isPlainObject7(body)&&isPlainObject7(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiWriteFallbackMessage(status)}function formatOcapiWriteToolResult(result,operation,path48,ocapiVersion=DEFAULT_OCAPI_VERSION){return result.status===403?writeGrantForbiddenResult({operation,path:path48,ocapiVersion,body:result.body}):result.ok?textResult4(JSON.stringify({status:result.status,outcome:result.outcome,body:result.body})):formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:result.status,message:ocapiWriteFaultMessage(result.body,result.status),source:"ocapi",details:{operation,path:path48,fault:result.fault,body:result.body}})}import{z as z8}from"zod";var localizedStringSchema=z8.record(z8.string(),z8.string()),objectAttributeValueTypeSchema=z8.enum(["string","int","double","text","html","date","image","boolean","money","quantity","datetime","email","password","set_of_string","set_of_int","set_of_double","enum_of_string","enum_of_int"]),OUTPUT_ONLY_BODY_PROPERTIES=["html","image"];function rejectOutputOnlyProperties(body,ctx){for(let key of OUTPUT_ONLY_BODY_PROPERTIES)Object.prototype.hasOwnProperty.call(body,key)&&ctx.addIssue({code:z8.ZodIssueCode.custom,path:[key],message:`'${key}' is an output-only OCAPI property and cannot be set on a write body. (Use value_type: "${key}" to declare an ${key} attribute instead.)`})}var objectAttributeDefinitionCommonShape={id:z8.string().optional(),system:z8.boolean().optional(),display_name:localizedStringSchema.optional(),description:localizedStringSchema.optional(),mandatory:z8.boolean().optional(),localizable:z8.boolean().optional(),site_specific:z8.boolean().optional(),default_value:z8.any().optional()},objectAttributeDefinitionCreateBodySchema=z8.object({value_type:objectAttributeValueTypeSchema.describe("Required OCAPI attribute value type."),...objectAttributeDefinitionCommonShape}).passthrough().superRefine(rejectOutputOnlyProperties),objectAttributeDefinitionPatchBodySchema=z8.object({value_type:objectAttributeValueTypeSchema.optional(),...objectAttributeDefinitionCommonShape}).passthrough().superRefine((body,ctx)=>{rejectOutputOnlyProperties(body,ctx),Object.keys(body).length===0&&ctx.addIssue({code:z8.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),attributeGroupPutBodySchema=z8.object({display_name:localizedStringSchema.describe("Localized group display name."),internal:z8.boolean().describe("Whether the group is internal (BM-only).")}),attributeGroupPatchBodySchema=z8.object({display_name:localizedStringSchema.optional(),internal:z8.boolean().optional()}).superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z8.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),SfccWritePayloadFault=class extends Error{status;faultType;constructor(faultType,message,status=400){super(message),this.name="SfccWritePayloadFault",this.faultType=faultType,this.status=status}};function buildObjectAttributeDefinitionCreatePayload(urlId,body){if(body.id!==void 0&&body.id!==urlId)throw new SfccWritePayloadFault("IdConflictException",`Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`);if(body.system===!0)throw new SfccWritePayloadFault("AttributeDefinitionKeyReadOnlyException","Cannot create a system attribute definition (system: true) over OCAPI writes.");return{...body,id:urlId,system:!1}}function buildObjectAttributeDefinitionPatchPayload(urlId,body){if(body.id!==void 0&&body.id!==urlId)throw new SfccWritePayloadFault("IdConflictException",`Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`);if(body.system===!0)throw new SfccWritePayloadFault("AttributeDefinitionKeyReadOnlyException","Cannot patch an attribute definition to system: true over OCAPI writes.");return{...body}}function buildAttributeGroupPutPayload(body){return{display_name:body.display_name,internal:body.internal}}function buildAttributeGroupPatchPayload(body){return{...body}}function buildEmptyRelationPayload(){return{}}import{z as z9}from"zod";var WRITE_ANNOTATIONS={readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0};var SFCC_WRITE_VALIDATION_ERROR_MESSAGE="Input failed schema validation before any OCAPI call. See error.details.issues for the offending fields.",SFCC_WRITE_INTERNAL_ERROR_MESSAGE="An unexpected internal error occurred while handling this SFCC write. The underlying failure detail is withheld from this response to avoid leaking credentials or internal state.";function safeZodIssues2(err){return(Array.isArray(err.issues)?err.issues:[]).map(issue=>({path:Array.isArray(issue.path)?issue.path.map(String).join("."):"",message:typeof issue.message=="string"?issue.message:"Invalid input."}))}function zodValidationEnvelope(err){return formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message:SFCC_WRITE_VALIDATION_ERROR_MESSAGE,source:"tool",details:{issues:safeZodIssues2(err)}})}function payloadFaultEnvelope(fault){return formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:fault.status,message:fault.message,source:"tool",details:{fault:{type:fault.faultType,message:fault.message}}})}function validationEnvelope(message){return formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message,source:"tool",details:{}})}function unexpectedEnvelope(){return formatSfccWriteFailure({code:"INTERNAL_ERROR",status:500,message:SFCC_WRITE_INTERNAL_ERROR_MESSAGE,source:"tool",details:{}})}function isSchemaFailure2(err){return err instanceof z9.ZodError?!0:typeof err=="object"&&err!==null&&err.name==="ZodError"&&Array.isArray(err.issues)}function isPayloadFault(err){return err instanceof SfccWritePayloadFault?!0:typeof err=="object"&&err!==null&&err.name==="SfccWritePayloadFault"&&typeof err.faultType=="string"&&typeof err.status=="number"}function preTransportErrorEnvelope(err){return isSchemaFailure2(err)?zodValidationEnvelope(err):isPayloadFault(err)?payloadFaultEnvelope(err):unexpectedEnvelope()}function encodedSegment(segment){return encodeURIComponent(segment)}function attributeDefinitionPath(objectType,attributeId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_definitions/${encodedSegment(attributeId)}`}function attributeGroupPath(objectType,groupId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}`}function attributeGroupAssignmentPath(objectType,groupId,attributeId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}/attribute_definitions/${encodedSegment(attributeId)}`}function preferenceObjectTypeForScope(scope){return scope==="site"?"SitePreferences":"OrganizationPreferences"}var instanceSchema=z10.string().optional().describe('Sandbox-only: omit (defaults to sandbox) or pass "sandbox".'),createAttributeDefinitionInput=z10.object({object_type:z10.string().describe('System object type, e.g. "Product".'),attribute_id:z10.string().describe("Attribute id (URL id); the body id must match it."),definition:objectAttributeDefinitionCreateBodySchema.describe("OCAPI ObjectAttributeDefinition create body (requires value_type)."),instance:instanceSchema}),updateAttributeDefinitionInput=z10.object({object_type:z10.string().describe("System object type."),attribute_id:z10.string().describe("Attribute id (URL id)."),patch:objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body (>=1 field)."),instance:instanceSchema}),createAttributeGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id (URL id)."),display_name:localizedStringSchema.describe("Localized group display name."),internal:z10.boolean().describe("Whether the group is internal (BM-only)."),instance:instanceSchema}),updateAttributeGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id (URL id)."),patch:attributeGroupPatchBodySchema.describe("Partial group body (display_name and/or internal; >=1 field)."),instance:instanceSchema}),assignAttributeToGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id."),attribute_id:z10.string().describe("Attribute definition id to assign."),instance:instanceSchema}),createCustomPreferenceDefinitionInput=z10.object({preference_scope:z10.enum(["site","organization"]).describe("site \u2192 SitePreferences; organization \u2192 OrganizationPreferences."),preference_id:z10.string().describe("Preference (attribute) id."),definition:objectAttributeDefinitionCreateBodySchema.describe("OCAPI ObjectAttributeDefinition create body (requires value_type). Set default_value for booleans (else they resolve to null)."),instance:instanceSchema});function buildCreateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=updateAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id,parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}function buildCreateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPutPayload({display_name:parsed.display_name,internal:parsed.internal})}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=updateAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPatchPayload(parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}function buildAssignAttributeToGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48;try{let parsed=assignAttributeToGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupAssignmentPath(parsed.object_type,parsed.group_id,parsed.attribute_id)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,buildEmptyRelationPayload(),credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildCreateCustomPreferenceDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createCustomPreferenceDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let objectType=preferenceObjectTypeForScope(parsed.preference_scope);path48=attributeDefinitionPath(objectType,parsed.preference_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.preference_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}var SYSTEM_OBJECT_WRITE_TOOL_NAMES=["system_object_attribute_definition_create","system_object_attribute_definition_update","system_object_attribute_group_create","system_object_attribute_group_update","system_object_attribute_assign_to_group","custom_preference_definition_create"];function registerSystemObjectWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("system_object_attribute_definition_create",{description:"Create a system-object attribute definition (sandbox-only, destructive). PUT .../attribute_definitions/{id}; body id must equal URL id and system must be false. Returns 201. HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:createAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateAttributeDefinitionHandler(gateDeps)),registerTool2("system_object_attribute_definition_update",{description:"Update a system-object attribute definition (sandbox-only, destructive). PATCH .../attribute_definitions/{id} via the ETag GET-then-If-Match round trip. Returns 200; surfaces 409/412 on ETag conflicts. HTTP 403 echoes the write-grant JSON.",inputSchema:updateAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildUpdateAttributeDefinitionHandler(gateDeps)),registerTool2("system_object_attribute_group_create",{description:"Create a system-object attribute group (sandbox-only, destructive). PUT .../attribute_groups/{id} with a minimal body (display_name + internal). HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:createAttributeGroupInput,annotations:WRITE_ANNOTATIONS},buildCreateAttributeGroupHandler(gateDeps)),registerTool2("system_object_attribute_group_update",{description:"Update a system-object attribute group (sandbox-only, destructive). PATCH .../attribute_groups/{id} via the ETag round trip (display_name and/or internal). HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:updateAttributeGroupInput,annotations:WRITE_ANNOTATIONS},buildUpdateAttributeGroupHandler(gateDeps)),registerTool2("system_object_attribute_assign_to_group",{description:"Assign an attribute definition to an attribute group (sandbox-only, destructive). PUT .../attribute_groups/{group}/attribute_definitions/{def} with an empty body. HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:assignAttributeToGroupInput,annotations:WRITE_ANNOTATIONS},buildAssignAttributeToGroupHandler(gateDeps)),registerTool2("custom_preference_definition_create",{description:"Create a custom preference definition (sandbox-only, destructive). PUT .../{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}. Booleans resolve to null unless default_value is set. HTTP 403 echoes the write-grant JSON.",inputSchema:createCustomPreferenceDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateCustomPreferenceDefinitionHandler(gateDeps))}import{z as z12}from"zod";import{z as z11}from"zod";var objectAttributeValueTypeSchema2=z11.enum(["string","int","double","boolean","date","datetime","email","enum_of_int","enum_of_string","html","image","money","password","quantity","set_of_int","set_of_string","set_of_double","text"]),objectAttributeDefinitionCreateBodySchema2=z11.object({id:z11.string().min(1).optional(),value_type:objectAttributeValueTypeSchema2}).passthrough(),objectAttributeDefinitionPatchBodySchema2=z11.object({value_type:objectAttributeValueTypeSchema2.optional()}).passthrough().superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z11.ZodIssueCode.custom,message:"Patch body must contain at least one attribute-definition field to update."})});function buildObjectAttributeDefinitionCreatePayload2(attributeId,body){return{...body,id:attributeId}}function buildObjectAttributeDefinitionPatchPayload2(body){return{...body}}var INSTANCE_DESCRIBE2="OCAPI instance context. SFCC writes are sandbox-only; omit for sandbox. Any other value is rejected before OCAPI is called.",createCustomObjectAttributeDefinitionInput=z12.object({object_type:z12.string().describe("Known custom object type identifier (must already exist). OCAPI cannot enumerate or create custom object types \u2014 only attribute definitions on a known type."),attribute_id:z12.string().describe("URL attribute-definition id. If the body also carries `id`, it must match."),definition:objectAttributeDefinitionCreateBodySchema2.describe("ObjectAttributeDefinition body; `value_type` is required for a create."),instance:z12.string().optional().describe(INSTANCE_DESCRIBE2)}),updateCustomObjectAttributeDefinitionInput=z12.object({object_type:z12.string().describe("Known custom object type identifier (must already exist). OCAPI cannot create types."),attribute_id:z12.string().describe("URL attribute-definition id to update."),patch:objectAttributeDefinitionPatchBodySchema2.describe("Partial ObjectAttributeDefinition body; must change at least one field."),instance:z12.string().optional().describe(INSTANCE_DESCRIBE2)});function customObjectAttributeDefinitionPath(objectType,attributeId){return`/custom_object_definitions/${encodedSegment(objectType)}/attribute_definitions/${encodedSegment(attributeId)}`}function buildCreateCustomObjectAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=createCustomObjectAttributeDefinitionInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;if(parsed.definition.id!==void 0&&parsed.definition.id!==parsed.attribute_id)return validationEnvelope(`Body id '${parsed.definition.id}' does not match URL attribute_id '${parsed.attribute_id}'.`);let path48=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id,parsed.definition);try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=updateCustomObjectAttributeDefinitionInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let path48=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload2(parsed.patch);try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}var CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES=["custom_object_definition_attribute_create","custom_object_definition_attribute_update"];function registerSfccCustomObjectDefWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("custom_object_definition_attribute_create",{description:"Create an attribute definition on a KNOWN custom object type via PUT /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; the type must pre-exist (OCAPI cannot create types). Echoes paste-ready grant JSON on 403.",inputSchema:createCustomObjectAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateCustomObjectAttributeDefinitionHandler(gateDeps)),registerTool2("custom_object_definition_attribute_update",{description:"Update an attribute definition on a KNOWN custom object type via an ETag-conditional PATCH /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; surfaces 409/412 conflicts and echoes grant JSON on 403.",inputSchema:updateCustomObjectAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps))}import{z as z14}from"zod";import{z as z13}from"zod";var sitePreferenceValueSchema=z13.union([z13.string(),z13.number().finite(),z13.boolean(),z13.array(z13.string())]),sitePreferenceValuesPatchBodySchema=z13.record(z13.string(),sitePreferenceValueSchema).superRefine((values,ctx)=>{let keys=Object.keys(values);keys.length===0&&ctx.addIssue({code:z13.ZodIssueCode.custom,message:"At least one preference value is required."});for(let key of keys)key.startsWith("c_")||ctx.addIssue({code:z13.ZodIssueCode.custom,path:[key],message:`Preference id '${key}' must be a custom preference starting with 'c_'.`})});function buildSitePreferenceValuesPatchPayload(values){return{...values}}var INSTANCE_ENUM2=z14.enum(["staging","development","sandbox","production"]),INSTANCE_DESCRIBE3="OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected before OCAPI is called. Defaults to 'sandbox'.",sitePreferenceValuesSetInput=z14.object({group:z14.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM2.optional().default("sandbox").describe(INSTANCE_DESCRIBE3),values:sitePreferenceValuesPatchBodySchema.describe("Flat map of c_-prefixed preference ids to values (string, number, boolean, or string[]).")});function sitePreferenceGroupPath(group){return`/site_preferences/preference_groups/${encodedSegment(group)}/sandbox`}function buildSitePreferenceValuesSetHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=sitePreferenceValuesSetInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let path48=sitePreferenceGroupPath(parsed.group),body=buildSitePreferenceValuesPatchPayload(parsed.values);try{let result=await ocapiPatchDirect(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}var SITE_PREFERENCE_WRITE_TOOL_NAMES=["site_preference_values_set"];function registerSitePreferenceWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("site_preference_values_set",{description:"Set custom site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. Sandbox-only, destructive; body is a flat map of c_-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 CustomPreferenceGroupNotFoundException; echoes grant JSON on 403.",inputSchema:sitePreferenceValuesSetInput,annotations:WRITE_ANNOTATIONS},buildSitePreferenceValuesSetHandler(gateDeps))}var SFCC_WRITE_TOOL_NAMES=[...SYSTEM_OBJECT_WRITE_TOOL_NAMES,...CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES,...SITE_PREFERENCE_WRITE_TOOL_NAMES];function registerSfccWriteTools(registerTool2,deps){registerSystemObjectWriteTools(registerTool2,{gateDeps:deps.gateDeps}),registerSfccCustomObjectDefWriteTools(registerTool2,{gateDeps:deps.gateDeps}),registerSitePreferenceWriteTools(registerTool2,{gateDeps:deps.gateDeps})}import{z as z15}from"zod";function notConfigured2(failureClass,message,limits){return formatSfccFailure({code:"NOT_CONFIGURED",status:503,message,source:"gate",details:{failure_class:failureClass,...limits!==void 0?{limits}:{}}})}function withSfccLogGate(deps,handler){return async args=>{let body;try{let url=deps.buildGetUrl("/sfcc/logs/capability",{repo_name:deps.repoName}),resp=await fetch(url,{headers:await deps.getGetHeaders()});if(!resp.ok)return formatSfccFailure({code:resp.status===401||resp.status===403?"UNAUTHORIZED":"SERVICE_UNAVAILABLE",status:resp.status,message:"Could not read the SFCC log capability from Bridge API (/jira/sfcc/logs/capability). Ensure your Bridge API key is set and the repo is authorized. Run sfcc_setup_status for a full diagnostic.",source:"webdav",details:{},upstreamHttpStatus:resp.status});body=await resp.json()}catch{return formatSfccFailure({code:"BAD_GATEWAY",status:502,message:"Could not reach Bridge API to resolve the SFCC log capability. Check that BAPI_BASE_URL points to a running Bridge API instance.",source:"webdav",details:{}})}if(body?.configured!==!0){let failureClass=typeof body?.failure_class=="string"?body.failure_class:"not_configured",message=typeof body?.message=="string"?body.message:"SFCC on-demand log queries are not configured for this repository. Run sfcc_setup_status for a diagnostic.";return notConfigured2(failureClass,message,body?.limits)}return handler(args)}}var MAX_QUERY_RANGE_HOURS=24,HIGH_VOLUME_MAX_RANGE_HOURS=6,HIGH_VOLUME_PREFIXES=new Set(["info","jobs","debug","customdebug"]),MAX_SELECTED_PREFIXES=5,MAX_MAX_ENTRIES=2e3,SUPPORTED_ENVIRONMENTS=["production","staging","development"],PREFIX_RE=/^[a-z][a-z0-9]*$/;function backendFailureMessage(status){return`The Bridge API SFCC log-query endpoint returned HTTP ${status}. No log content is included in this response.`}function safeBackendDetail(text2){let parsed;try{parsed=JSON.parse(text2)}catch{return{}}if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{};let detail=parsed.detail;return typeof detail=="string"?{detail}:{}}function validationError(message){return formatSfccFailure({code:"VALIDATION_ERROR",status:400,message,source:"tool",details:{}})}var inputSchema=z15.object({environment:z15.enum(SUPPORTED_ENVIRONMENTS).describe("REQUIRED environment to scope the query to. Deliberately required so a query can never fan across every environment at once."),time_range:z15.object({start:z15.string().datetime({offset:!0}).describe("Inclusive ISO-8601 UTC start."),end:z15.string().datetime({offset:!0}).describe("Exclusive ISO-8601 UTC end (after start).")}).strict().describe("REQUIRED bounded window. No open-ended or inferred period is ever assumed."),prefixes:z15.array(z15.string().regex(PREFIX_RE,"prefix must be a letter followed by letters/digits")).max(MAX_SELECTED_PREFIXES).optional().describe(`Optional log-file prefix selection (max ${MAX_SELECTED_PREFIXES}). Empty = the shipped error-class defaults. High-volume prefixes (info/jobs/debug/customdebug) impose a stricter ${HIGH_VOLUME_MAX_RANGE_HOURS}h max time range.`),max_entries:z15.number().int().min(1).max(MAX_MAX_ENTRIES).optional().describe(`Optional per-query entry-scan cap (1..${MAX_MAX_ENTRIES}).`)});function semanticCheck(args){let start=Date.parse(args.time_range.start),end=Date.parse(args.time_range.end);if(Number.isNaN(start)||Number.isNaN(end))return"time_range.start and time_range.end must be valid ISO-8601 timestamps.";if(!(start<end))return"time_range.start must be strictly before time_range.end.";let prefixes=args.prefixes??[],maxHours=prefixes.some(p=>HIGH_VOLUME_PREFIXES.has(p))?HIGH_VOLUME_MAX_RANGE_HOURS:MAX_QUERY_RANGE_HOURS,spanHours=(end-start)/36e5;return spanHours>maxHours?`time_range spans ${spanHours.toFixed(1)}h but the maximum for this prefix selection is ${maxHours}h. environment and time_range are required, and the range is capped, to prevent broad cross-environment or open-ended log scans.`:new Set(prefixes).size!==prefixes.length?"prefixes must be unique.":null}function buildHandler(deps){return async rawArgs=>{let parsed=inputSchema.safeParse(rawArgs);if(!parsed.success)return validationError(parsed.error.issues.map(i=>i.message).join("; "));let args=parsed.data,semanticFailure=semanticCheck(args);if(semanticFailure)return validationError(semanticFailure);let body={repo_name:deps.repoName,environment:args.environment,time_range:{start:args.time_range.start,end:args.time_range.end},prefixes:args.prefixes??[],...args.max_entries!==void 0?{max_entries:args.max_entries}:{}},resp;try{let url=deps.buildGetUrl("/sfcc/logs/query",{repo_name:deps.repoName});resp=await fetch(url,{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)})}catch{return formatSfccFailure({code:"BAD_GATEWAY",status:502,message:"Could not reach Bridge API to run the SFCC log query. Check that BAPI_BASE_URL points to a running Bridge API instance.",source:"webdav",details:{}})}let text2=await resp.text();return resp.ok?{content:[{type:"text",text:text2}]}:formatSfccFailure({code:resp.status>=500?"SERVICE_UNAVAILABLE":"REQUEST_FAILED",status:resp.status,message:backendFailureMessage(resp.status),source:"webdav",details:safeBackendDetail(text2),upstreamHttpStatus:resp.status})}}function registerSfccLogQueryTool(registerTool2,deps){let gated=withSfccLogGate({buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,repoName:deps.repoName},buildHandler(deps));registerTool2("sfcc_log_query",{description:"Query redacted, filtered SFCC logs on demand, scoped to a REQUIRED environment and time_range. Runs pull\u2192redact\u2192filter on the Bridge backend; entries, time range, and log-file prefixes are capped (high-volume prefixes get a stricter range). Returns a NOT_CONFIGURED 503 when the log capability isn't set up \u2014 run sfcc_setup_status.",inputSchema,annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},gated)}function registerSfccTools(registerTool2,deps){let gateDeps={buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,repoName:deps.repoName};registerTool2("sfcc_setup_status",{description:"Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Always-registered; returns status without requiring full SFCC configuration to be complete.",inputSchema:z16.object({}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},buildSfccSetupStatusHandler(deps.buildGetUrl,deps.getGetHeaders,deps.repoName,deps.getResolvedApiKey));let gatedCheckPermissions=withSfccGate(gateDeps,async(_args,credentials)=>checkPermissionsTool(credentials));registerTool2("check_permissions",{description:"Probe SFCC OCAPI access via GET /system_object_definitions. On 200: reports OK and the detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).",inputSchema:z16.object({instance:z16.string().optional().describe("Explicit sandbox hostname to use instead of dw.json auto-detection.")}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},gatedCheckPermissions),deps.includeReadTools&&(registerSystemObjectReadTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccCustomObjectDefReadTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSitePreferenceTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccWriteTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccLogQueryTool(registerTool2,{buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,getPostHeaders:deps.getPostHeaders,repoName:deps.repoName}))}init_mcp_profile();init_errors();init_store();init_producer_ledger();init_taxonomy();import{readFileSync as readFileSync3,unlinkSync}from"node:fs";init_file_scope_guard();init_git_ci_types();init_git_inspection();init_producer_ledger();var COMMITTED_REF_PHASE="committed";function buildCommitCreatedEventInput(context,metadata){let details={repo:context.repo,branch:context.branch,worktree_path:context.worktree_path,commit_sha:metadata.sha,parent_shas:metadata.parents,author:{name:metadata.author_name,email:metadata.author_email},committer:{name:metadata.committer_name,email:metadata.committer_email},co_authors:metadata.co_authors,authored_at:metadata.authored_at,committed_at:metadata.committed_at,subject:metadata.subject,attribution_source:metadata.attribution_source};return{source:"git",type:"git.commit_created",subject:context.repo,producer:GIT_HOOK_PRODUCER,observed_via:"git-hook:post-commit",data:{summary:`Commit ${metadata.sha??"(unknown)"} on ${context.branch??"(detached)"}`,status:"created",details}}}function buildWorktreeChangedEventInput(context,updates,phase){let updatesHash=stableJsonHash({phase,updates}),details={repo:context.repo,branch:context.branch,worktree_path:context.worktree_path,transaction_phase:phase,ref_updates:updates,updates_hash:updatesHash};return{source:"git",type:"worktree.changed",subject:context.repo,producer:GIT_HOOK_PRODUCER,observed_via:"git-hook:reference-transaction",data:{summary:`${updates.length} ref update(s) on ${context.repo}`,status:"changed",details}}}async function runPostCommitHookProducer(deps={}){let getContext=deps.getContext??getGitWorktreeContext,readMetadata=deps.readMetadata??readHeadCommitMetadata,emitIfNew=deps.emitIfNew??emitConductorEventIfNew;try{let context=getContext({cwd:deps.cwd,env:deps.env}),metadata=readMetadata({cwd:deps.cwd});if(!metadata||metadata.sha===null)return{ok:!0,emitted:!1,reason:"no-head-commit"};let input=buildCommitCreatedEventInput(context,metadata),decision=await emitIfNew(input,{event_type:"git.commit_created",repo:context.repo,commit_sha:metadata.sha});return{ok:!0,emitted:decision.emitted,reason:decision.reason}}catch{return{ok:!0,emitted:!1,reason:"skipped"}}}async function runReferenceTransactionHookProducer(args,deps={}){let getContext=deps.getContext??getGitWorktreeContext,parseUpdates=deps.parseUpdates??parseReferenceTransactionUpdates,emitIfNew=deps.emitIfNew??emitConductorEventIfNew;try{if(args.phase!==COMMITTED_REF_PHASE)return{ok:!0,emitted:!1,reason:"non-committed-phase"};let updates=parseUpdates(args.stdin);if(updates.length===0)return{ok:!0,emitted:!1,reason:"no-updates"};let context=getContext({cwd:deps.cwd,env:deps.env}),input=buildWorktreeChangedEventInput(context,updates,args.phase),refUpdatesHash=stableJsonHash({phase:args.phase,updates}),decision=await emitIfNew(input,{event_type:"worktree.changed",repo:context.repo,ref_updates_hash:refUpdatesHash});return{ok:!0,emitted:decision.emitted,reason:decision.reason}}catch{return{ok:!0,emitted:!1,reason:"skipped"}}}init_supervisor_config();function formatConductorErrorLine(envelope){if(envelope.error==="LEDGER_NATIVE_MODULE_LOAD_FAILED"&&envelope.details){let d=envelope.details;return`${envelope.message} details: module=${d.module} node_version=${d.node_version} node_modules_abi=${d.node_modules_abi}`}return envelope.message}function getConductorUsage(){return["Usage: conductor <command> [options]","","Local append-only event ledger for multi-agent coordination.","Talks ONLY to the local SQLite store (~/.config/bridge/events.db); no Bridge API calls.","","Commands:"," emit-event Append one semantic event to the ledger"," supervise --run-id <id> Run the foreground, run-scoped supervisor loop"," epic-tick Run one stateless reconciliation pass for an Epic"," epic-status Print read-only health summary of an Epic Run"," send-message Enqueue ONE typed supervisor->worker relay message (idempotent)"," check-messages Read + ACK pending relay messages for a worker (no redelivery)"," doctor Read-only health/diagnostics report (ledger + git hooks)"," purge Delete ALL ledger rows (events, messages, supervisor_projection)"," install-git-hooks Install local, opportunistic, non-blocking git hooks"," git-hook post-commit Run the post-commit producer (invoked by the installed hook)"," git-hook reference-transaction --phase <p> --stdin-file <f>"," Run the reference-transaction producer (invoked by the hook)"," file-scope-guard Warn-only: compare the branch diff against the declared"," touched-file set (always exits 0; never blocks a PR)","","supervise options:"," --run-id <id> Run/session identifier to supervise (required)"," --wake-interval-ms <n> Deterministic event-poll cadence (clamped 30000..60000)"," --global-timeout-ms <n> Total wall-clock ceiling for the run"," --escalation-cooldown-ms <n> Min gap between escalations for the same worker+reason","","install-git-hooks notes:"," Hooks are LOCAL, unversioned, opportunistic, and bypassable. Missing hooks are a"," degraded optional capability and never prevent PR/CI gate evaluation.","","emit-event options:"," --type <t> Semantic event type (required). One of:",` ${SEMANTIC_EVENT_TYPES.join(", ")}`," --source <s> Logical producer (required)"," --subject <s> Subject the event is about (e.g. ticket key)"," --run-id <s> Run/session identifier"," --worker-id <s> Worker/agent identifier"," --producer <s> Finer-grained producer identity"," --schema-version <n> Event schema version (default 1)"," --time <iso> ISO-8601 event time (default now)"," --confidence <0..1> Confidence score"," --observed-via <s> Channel the event was observed through"," --data-json <json> Normalized data object (allowlisted top-level keys)"," --data-json-stdin Read the complete normalized data object as JSON from stdin"," (mutually exclusive with --data-json; keeps raw payloads and"," secrets out of the process argument list)"," --raw-json <json> Tool-native object; nested under data.raw"," --payload-ref <ref> Reference for large external payloads (-> data.payload_ref)"," --json Print compact JSON result","","send-message options:"," --run-id <s> Run/session identifier (required)"," --worker-id <s> Target worker identifier (required)"," --type <s> Typed message kind, e.g. supervisor.worker_stalled (required)"," --cause-seq <n> Idempotency cause sequence, non-negative integer (required)"," --payload-json <json> Compact payload object (allowlisted top-level keys)"," --payload-json-stdin Read the payload object as JSON from stdin"," (mutually exclusive with --payload-json)"," --available-at <iso> ISO-8601 time the message becomes available (default now)"," --cooldown-ms <n> Per-call cooldown override in ms"," --json Print compact JSON result"," Note: a duplicate idempotency key or a same-type message inside the cooldown"," window does NOT enqueue a second message.","","check-messages options:"," --run-id <s> Run/session identifier (required)"," --worker-id <s> Worker identifier (required)"," --limit <n> Max messages to deliver/ack (default 10, max 100)"," --json Print compact JSON result"," Note: returned messages are ACKNOWLEDGED by this call and are not redelivered.","","doctor / purge options:"," --json Print machine-readable JSON"," --no-deny-probe (doctor only) Skip the deny-enforcement preflight \u2014 no headless"," agent is spawned; the deny_enforcement section reports an"," explicit skipped state (enforcement UNVERIFIED, never enforced)","","Examples:"," conductor emit-event --type run.started --source git-hook --run-id BAPI-393 \\",` --data-json '{"summary":"run started"}'`," conductor emit-event --type git.commit_created --source git-hook \\",` --raw-json '{"branch":"feature/x","sha":"abc123"}'`," conductor emit-event --type ci.failed --source ci \\"," --payload-ref 'file:///tmp/ci-log.txt'"," conductor emit-event --type merge.succeeded --source conductor-merge \\",` --worker-id w1 --data-json '{"summary":"auto-merged","status":"succeeded"}'`," conductor doctor --json"," conductor purge","","epic-tick options:"," --epic-key <KEY> Epic key to supervise (required, non-empty)"," --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)"," --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)","","approve-plan options:"," approve-plan <epic_key> --plan-version N [--json]"," <epic_key> Epic key (e.g. EPIC-405) (required positional)"," --plan-version <n> Strictly positive plan version to approve (required)"," --json Print compact JSON result"," --help Print this usage message","","epic-status options:"," --epic-key <KEY> Epic key to fetch the snapshot for (required)"," --json Print compact JSON result"," --help Print this usage message","","Examples:"," conductor approve-plan EPIC-405 --plan-version 2"," conductor approve-plan EPIC-405 --plan-version 2 --json"," conductor epic-status --epic-key EPIC-405"," conductor epic-status --epic-key EPIC-405 --json"].join(`
5644
- `)}var VALID_COMMANDS=new Set(["emit-event","supervise","epic-tick","approve-plan","epic-status","send-message","check-messages","doctor","purge","install-git-hooks","git-hook","file-scope-guard"]);function parseConductorArgs(argv){if(argv.length===0)return{kind:"help"};let first=argv[0];return first==="-h"||first==="--help"?{kind:"help"}:first.startsWith("-")?{kind:"error",message:`Unknown option "${first}". Run "conductor --help" for usage.`}:VALID_COMMANDS.has(first)?{kind:"command",command:first,argv:argv.slice(1)}:{kind:"error",message:`Unknown command "${first}". Run "conductor --help" for usage.`}}function tokenizeFlags(argv,valueFlags,boolFlags){let values=new Map,bools=new Set;for(let i=0;i<argv.length;i+=1){let token=argv[i];if(token==="-h"){bools.add("--help");continue}if(!token.startsWith("--"))throw new ConductorValidationError(`Unexpected argument "${token}".`);let eq=token.indexOf("="),name=eq>=0?token.slice(0,eq):token;if(boolFlags.has(name)){bools.add(name);continue}if(!valueFlags.has(name))throw new ConductorValidationError(`Unknown flag "${name}".`);let value;if(eq>=0)value=token.slice(eq+1);else{let next=argv[i+1];if(next===void 0)throw new ConductorValidationError(`Flag "${name}" requires a value.`);value=next,i+=1}values.set(name,value)}return{values,bools}}var EMIT_VALUE_FLAGS=new Set(["--type","--source","--id","--subject","--run-id","--worker-id","--producer","--schema-version","--time","--confidence","--observed-via","--data-json","--raw-json","--payload-ref"]),EMIT_BOOL_FLAGS=new Set(["--json","--help","--data-json-stdin"]);function defaultReadStdin(){return readFileSync3(0,"utf-8")}function parseJsonFlag(raw,flag){try{return JSON.parse(raw)}catch{throw new ConductorValidationError(`Flag "${flag}" must be valid JSON.`)}}function isPlainObject9(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function parseEmitEventArgs(argv,deps={}){let{values,bools}=tokenizeFlags(argv,EMIT_VALUE_FLAGS,EMIT_BOOL_FLAGS);if(bools.has("--help"))return{input:{source:"",type:"run.started"},json:bools.has("--json"),help:!0};let type=values.get("--type"),source=values.get("--source");if(!type)throw new ConductorValidationError('Flag "--type" is required for emit-event.');if(!source)throw new ConductorValidationError('Flag "--source" is required for emit-event.');let data={},dataJsonRaw=values.get("--data-json"),dataJsonStdin=bools.has("--data-json-stdin");if(dataJsonRaw!==void 0&&dataJsonStdin)throw new ConductorValidationError('Flags "--data-json" and "--data-json-stdin" are mutually exclusive; pass the normalized data object exactly one way.');if(dataJsonStdin){let stdinRaw=(deps.readStdin??defaultReadStdin)(),parsed=parseJsonFlag(stdinRaw,"--data-json-stdin");if(!isPlainObject9(parsed))throw new ConductorValidationError('Flag "--data-json-stdin" must be a JSON object.');data={...parsed}}else if(dataJsonRaw!==void 0){let parsed=parseJsonFlag(dataJsonRaw,"--data-json");if(!isPlainObject9(parsed))throw new ConductorValidationError('Flag "--data-json" must be a JSON object.');data={...parsed}}let rawJsonRaw=values.get("--raw-json");if(rawJsonRaw!==void 0){let parsedRaw=parseJsonFlag(rawJsonRaw,"--raw-json");if(!isPlainObject9(parsedRaw))throw new ConductorValidationError('Flag "--raw-json" must be a JSON object.');let existingRaw=isPlainObject9(data.raw)?data.raw:{};data.raw={...existingRaw,...parsedRaw}}let payloadRef=values.get("--payload-ref");payloadRef!==void 0&&(data.payload_ref=payloadRef);let schemaVersionRaw=values.get("--schema-version"),confidenceRaw=values.get("--confidence"),input={source,type,id:values.get("--id"),subject:values.get("--subject"),run_id:values.get("--run-id"),worker_id:values.get("--worker-id"),producer:values.get("--producer"),time:values.get("--time"),observed_via:values.get("--observed-via"),data};if(schemaVersionRaw!==void 0){let n=Number.parseInt(schemaVersionRaw,10);if(!Number.isFinite(n))throw new ConductorValidationError('Flag "--schema-version" must be an integer.');input.schema_version=n}if(confidenceRaw!==void 0){let n=Number.parseFloat(confidenceRaw);if(!Number.isFinite(n))throw new ConductorValidationError('Flag "--confidence" must be a number.');input.confidence=n}return{input,json:bools.has("--json"),help:!1}}async function runEmitEventCommand(argv,deps={}){let parsed=parseEmitEventArgs(argv,deps);if(parsed.help)return console.log(getConductorUsage()),0;let result;try{result=await emitConductorEvent(parsed.input)}catch(error){if(isDuplicateConstraintError2(error)){let dup={ok:!1,reason:"duplicate"};return console.log(parsed.json?JSON.stringify(dup):JSON.stringify(dup,null,2)),0}throw error}return parsed.json?console.log(JSON.stringify(result)):console.log(JSON.stringify(result,null,2)),0}var SEND_MESSAGE_VALUE_FLAGS=new Set(["--run-id","--worker-id","--type","--cause-seq","--payload-json","--available-at","--cooldown-ms"]),SEND_MESSAGE_BOOL_FLAGS=new Set(["--payload-json-stdin","--json","--help"]);function parseSendMessageArgs(argv,deps={}){let{values,bools}=tokenizeFlags(argv,SEND_MESSAGE_VALUE_FLAGS,SEND_MESSAGE_BOOL_FLAGS);if(bools.has("--help"))return{input:{run_id:"",worker_id:"",type:"",cause_seq:0},json:bools.has("--json"),help:!0};let runId=values.get("--run-id"),workerId=values.get("--worker-id"),type=values.get("--type"),causeSeqRaw=values.get("--cause-seq");if(!runId)throw new ConductorValidationError('Flag "--run-id" is required for send-message.');if(!workerId)throw new ConductorValidationError('Flag "--worker-id" is required for send-message.');if(!type)throw new ConductorValidationError('Flag "--type" is required for send-message.');if(causeSeqRaw===void 0)throw new ConductorValidationError('Flag "--cause-seq" is required for send-message.');if(!/^\d+$/.test(causeSeqRaw.trim()))throw new ConductorValidationError('Flag "--cause-seq" must be a non-negative integer.');let causeSeq=Number.parseInt(causeSeqRaw.trim(),10),payload={},payloadInline=values.get("--payload-json"),payloadStdin=bools.has("--payload-json-stdin");if(payloadInline!==void 0&&payloadStdin)throw new ConductorValidationError('Flags "--payload-json" and "--payload-json-stdin" are mutually exclusive; pass the payload object exactly one way.');if(payloadStdin){let readStdin=deps.readStdin??defaultReadStdin,parsed=parseJsonFlag(readStdin(),"--payload-json-stdin");if(!isPlainObject9(parsed))throw new ConductorValidationError('Flag "--payload-json-stdin" must be a JSON object.');payload={...parsed}}else if(payloadInline!==void 0){let parsed=parseJsonFlag(payloadInline,"--payload-json");if(!isPlainObject9(parsed))throw new ConductorValidationError('Flag "--payload-json" must be a JSON object.');payload={...parsed}}let input={run_id:runId,worker_id:workerId,type,cause_seq:causeSeq,payload},availableAt=values.get("--available-at");availableAt!==void 0&&(input.available_at=availableAt);let cooldownRaw=values.get("--cooldown-ms");if(cooldownRaw!==void 0){if(!/^\d+$/.test(cooldownRaw.trim()))throw new ConductorValidationError('Flag "--cooldown-ms" must be a non-negative integer.');input.cooldown_ms=Number.parseInt(cooldownRaw.trim(),10)}return{input,json:bools.has("--json"),help:!1}}async function runSendMessageCommand(argv,deps={}){let parsed=parseSendMessageArgs(argv,deps);if(parsed.help)return console.log(getConductorUsage()),0;let result=await sendWorkerMessage(parsed.input);return parsed.json?console.log(JSON.stringify(result)):console.log([`Message ${result.status}.`,` id: ${result.message.id}`,` type: ${result.message.type}`,` state: ${result.message.state}`].join(`
5643
+ Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path42 from"path";import{z as z5}from"zod";import{z as z3}from"zod";var SFCC_INTERNAL_ERROR_MESSAGE="An unexpected internal error occurred while handling this SFCC read. The underlying failure detail is withheld from this response to avoid leaking credentials or internal state.",SFCC_VALIDATION_ERROR_MESSAGE="Arguments failed schema validation before any OCAPI call was made. See error.details.issues for the offending fields.";function ocapiFallbackMessage(status){return`The SFCC OCAPI Data API returned HTTP ${status} for this read and supplied no fault message. The upstream response body is preserved under error.details.`}function isPlainObject6(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function formatOcapiReadFailure(result,upstreamHttpStatus){return formatSfccFailure({code:"OCAPI_FAULT",status:result.status,message:ocapiFaultMessage(result.body,result.status),source:"ocapi",details:result.body,upstreamHttpStatus})}function ocapiFaultMessage(body,status){if(isPlainObject6(body)&&isPlainObject6(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiFallbackMessage(status)}function isSchemaFailure(err){return err instanceof z3.ZodError?!0:typeof err=="object"&&err!==null&&err.name==="ZodError"&&Array.isArray(err.issues)}function safeZodIssues(err){return(Array.isArray(err.issues)?err.issues:[]).map(issue=>({path:Array.isArray(issue.path)?issue.path.map(String).join("."):"",code:typeof issue.code=="string"?issue.code:"invalid",message:typeof issue.message=="string"?issue.message:"Invalid input."}))}function withSfccReadErrorBoundary(handler){return async(...args)=>{try{return await handler(...args)}catch(err){return isSchemaFailure(err)?formatSfccFailure({code:"VALIDATION_ERROR",status:400,message:SFCC_VALIDATION_ERROR_MESSAGE,source:"tool",details:{issues:safeZodIssues(err)}}):formatSfccFailure({code:"INTERNAL_ERROR",status:500,message:SFCC_INTERNAL_ERROR_MESSAGE,source:"tool",details:{}})}}}var BRIDGE_REDACTED_MARKER="[REDACTED_BY_BRIDGE]";function isPlainObject7(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function redactSfccBody(body){if(Array.isArray(body))return body.map(item=>redactSfccBody(item));if(isPlainObject7(body)){let result={};for(let[key,value]of Object.entries(body)){if(key==="default_value"){result[key]=BRIDGE_REDACTED_MARKER;continue}result[key]=redactSfccBody(value)}return result}return body}function readSfccBody(body){return normalizeOcapiBody(redactSfccBody(body))}import{z as z4}from"zod";var FULL_SELECT_EXPRESSION="(**)",PROJECTION_POLICY={attribute_search:{defaultProjection:"full",placement:"body",inputDescription:"Detail level. Default: full \u2014 OCAPI's complete metadata projection (value_type, mandatory, localizable, searchable, site_specific); default_value is withheld. lean returns ids only."},attribute_collection:{defaultProjection:"full",placement:"query",inputDescription:"Detail level. Default: full \u2014 OCAPI's complete metadata projection (value_type, mandatory, localizable, searchable, site_specific); default_value is withheld. lean returns ids only."},broad_list:{defaultProjection:"lean",placement:"query",inputDescription:"Detail level. Default: lean (ids only); full returns all fields."}};function resolveReadProjection(category,requested){return requested??PROJECTION_POLICY[category].defaultProjection}function projectionPlacementFor(category){return PROJECTION_POLICY[category].placement}function selectExpressionFor(projection){return projection==="full"?FULL_SELECT_EXPRESSION:void 0}function applyProjectionToBody(body,category,requested){if(projectionPlacementFor(category)!=="body")throw new Error(`SFCC read category "${category}" carries its projection in the query string, not the POST body.`);let expression=selectExpressionFor(resolveReadProjection(category,requested));return expression!==void 0&&(body.select=expression),body}function projectionQueryEntries(category,requested){if(projectionPlacementFor(category)!=="query")throw new Error(`SFCC read category "${category}" carries its projection in the POST body, not the query string.`);let expression=selectExpressionFor(resolveReadProjection(category,requested));return expression===void 0?{}:{select:expression}}function projectionInputFor(category){return z4.enum(["full","lean"]).optional().describe(PROJECTION_POLICY[category].inputDescription)}import path41 from"path";import{mkdir as mkdir11,writeFile as writeFile11}from"fs/promises";var SFCC_MAX_INLINE=5e4;async function truncateAndSaveIfNeeded(text2,dir,filename,page,deps={}){if(text2.length<=SFCC_MAX_INLINE)return text2;let mk=deps.mkdir??mkdir11,wf=deps.writeFile??writeFile11,filePath=path41.join(dir,filename);try{await mk(dir,{recursive:!0}),await wf(filePath,text2,"utf-8")}catch(err){let descriptor2={truncated:!1,save_failed:!0,oversized:!0,warning:`Response was NOT truncated because the local save failed: ${err instanceof Error?err.message:String(err)}. The complete payload is returned inline.`,page};try{descriptor2.data=JSON.parse(text2)}catch{descriptor2.data_text=text2}return JSON.stringify(descriptor2)}return JSON.stringify({truncated:!0,saved_path:filePath,page})}var READ_ANNOTATIONS={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},systemObjectListInput=z5.object({count:z5.number().optional().describe("Maximum number of system object types to return."),start:z5.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")}),systemObjectGetInput=z5.object({object_type:z5.string().describe('System object type identifier, e.g. "Product" or "Order".')}),systemObjectAttributeSearchInput=z5.object({object_type:z5.string().describe('System object type to search within, e.g. "Order".'),query:z5.union([z5.string(),z5.record(z5.string(),z5.any())]).describe("Search query. A plain string is a case-insensitive substring match across id and display_name; a structured OCAPI query object passes through unchanged."),start:z5.number().optional().describe("Zero-based offset for paging."),count:z5.number().optional().describe("Maximum number of results to return."),sorts:z5.array(z5.any()).optional().describe("Array of OCAPI sort descriptors."),projection:projectionInputFor("attribute_search")});function safeTimestamp(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult(output)}function buildSystemObjectListHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{count,start,projection}=systemObjectListInput.parse(args),queryParams={...projectionQueryEntries("broad_list",projection)};count!==void 0&&(queryParams.count=String(count)),start!==void 0&&(queryParams.start=String(start));let queryStr=Object.keys(queryParams).length>0?"?"+new URLSearchParams(queryParams).toString():"",result=await ocapiGet(`/system_object_definitions${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-list-${safeTimestamp()}.json`,normalized.page)}))}function buildSystemObjectGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type}=systemObjectGetInput.parse(args),encodedType=encodeURIComponent(object_type),result=await ocapiGet(`/system_object_definitions/${encodedType}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`,normalized.page)}))}function buildSystemObjectAttributeSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,query,start,count,sorts,projection}=systemObjectAttributeSearchInput.parse(args),encodedType=encodeURIComponent(object_type),postBody={query:typeof query=="string"?{text_query:{fields:["id","display_name"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts),applyProjectionToBody(postBody,"attribute_search",projection);let result=await ocapiPost(`/system_object_definitions/${encodedType}/attribute_definition_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path42.join(await getDocsDir2(),"sfcc");return saveAndReturn(text2,dir,`system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`,normalized.page)}))}function registerSystemObjectReadTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("system_object_list",{description:"List all system object types from the developer sandbox via GET /system_object_definitions. Read-only. Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). Returns ids only by default; pass projection=full for all fields. Oversized outputs are auto-saved locally.",inputSchema:systemObjectListInput,annotations:READ_ANNOTATIONS},buildSystemObjectListHandler(gateDeps,getDocsDir2)),registerTool2("system_object_get",{description:"Retrieve a system object type definition from the developer sandbox. Read-only. GET /system_object_definitions/{type}. Prefer system_object_attribute_search for targeted attribute lookups. Oversized payloads are auto-saved locally.",inputSchema:systemObjectGetInput,annotations:READ_ANNOTATIONS},buildSystemObjectGetHandler(gateDeps,getDocsDir2)),registerTool2("system_object_attribute_search",{description:"Search attribute definitions for a system object type. Read-only. POST /system_object_definitions/{type}/attribute_definition_search. Use for targeted c_ attribute lookups; returns each attribute's schema by default (default_value withheld). Pass a plain string for text search or a structured OCAPI query. Oversized results auto-saved locally.",inputSchema:systemObjectAttributeSearchInput,annotations:READ_ANNOTATIONS},buildSystemObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path43 from"path";import{z as z6}from"zod";var READ_ANNOTATIONS2={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},OBJECT_TYPE_DISCOVERY_HINT=`Known custom object type id (e.g. starting with "c_"). OCAPI cannot enumerate type IDs directly, but you can discover a candidate via system_object_list at projection="full" (distinct display_name + attribute_definition_count per custom type) and confirm it by checking that this tool's returned attribute count matches that row's attribute_definition_count.`,customObjectAttributesGetInput=z6.object({object_type:z6.string().describe(OBJECT_TYPE_DISCOVERY_HINT),projection:projectionInputFor("attribute_collection")}),customObjectAttributeSearchInput=z6.object({object_type:z6.string().describe(OBJECT_TYPE_DISCOVERY_HINT),query:z6.union([z6.string(),z6.record(z6.string(),z6.any())]).describe("Search query. A plain string is a case-insensitive substring match across id and display_name; a structured OCAPI query object passes through unchanged."),start:z6.number().optional().describe("Zero-based offset for paging."),count:z6.number().optional().describe("Maximum number of results to return."),sorts:z6.array(z6.any()).optional().describe("Array of OCAPI sort descriptors."),projection:projectionInputFor("attribute_search")});function safeTimestamp2(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType2(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult2(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn2(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult2(output)}function buildCustomObjectAttributesGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,projection}=customObjectAttributesGetInput.parse(args),encodedType=encodeURIComponent(object_type),queryEntries=projectionQueryEntries("attribute_collection",projection),queryStr=Object.keys(queryEntries).length>0?"?"+new URLSearchParams(queryEntries).toString():"",result=await ocapiGet(`/custom_object_definitions/${encodedType}/attribute_definitions${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path43.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text2,dir,`custom-object-def-attributes-${safeType2(object_type)}-${safeTimestamp2()}.json`,normalized.page)}))}function buildCustomObjectAttributeSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{object_type,query,start,count,sorts,projection}=customObjectAttributeSearchInput.parse(args),encodedType=encodeURIComponent(object_type),postBody={query:typeof query=="string"?{text_query:{fields:["id","display_name"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts),applyProjectionToBody(postBody,"attribute_search",projection);let result=await ocapiPost(`/custom_object_definitions/${encodedType}/attribute_definition_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path43.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text2,dir,`custom-object-def-search-${safeType2(object_type)}-${safeTimestamp2()}.json`,normalized.page)}))}function registerSfccCustomObjectDefReadTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("custom_object_definition_attributes_get",{description:"Retrieve attribute definitions for a KNOWN custom object type via GET /custom_object_definitions/{type}/attribute_definitions (default_value withheld). OCAPI cannot enumerate type IDs directly; derive one via system_object_list (projection=full, attribute_definition_count), confirm here. Type creation is v2 metadata-import only.",inputSchema:customObjectAttributesGetInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributesGetHandler(gateDeps,getDocsDir2)),registerTool2("custom_object_definition_attribute_search",{description:"Search attribute definitions within a KNOWN custom object type via POST /custom_object_definitions/{type}/attribute_definition_search (default_value withheld). OCAPI cannot enumerate type IDs directly; derive one via system_object_list (projection=full, attribute_definition_count), confirm here. Type creation is v2 metadata-import only.",inputSchema:customObjectAttributeSearchInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path44 from"path";import{z as z7}from"zod";var READ_ANNOTATIONS3={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},INSTANCE_ENUM=z7.enum(["staging","development","sandbox","production"]),INSTANCE_DESCRIBE="OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected with a validation error. Defaults to 'sandbox'.",sitePreferenceGetInput=z7.object({group:z7.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),start:z7.number().optional().describe("Zero-based offset for paging."),count:z7.number().optional().describe("Maximum number of preferences to return.")}),sitePreferenceSearchInput=z7.object({group:z7.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),query:z7.union([z7.string(),z7.record(z7.string(),z7.any())]).describe("Search query. Pass a plain string for text search across preference ids, or a structured OCAPI query object (term_query, filtered_query, etc.)."),start:z7.number().optional().describe("Zero-based offset for paging."),count:z7.number().optional().describe("Maximum number of results to return."),sorts:z7.array(z7.any()).optional().describe("Array of OCAPI sort descriptors.")}),sitePreferenceGroupListInput=z7.object({count:z7.number().optional().describe("Maximum number of site preference groups to return."),start:z7.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")});function safeTimestamp3(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeGroup(group){return encodeURIComponent(group).replace(/%/g,"_")}function textResult3(text2){return{content:[{type:"text",text:text2}]}}async function saveAndReturn3(text2,dir,filename,page){let output=await truncateAndSaveIfNeeded(text2,dir,filename,page);return textResult3(output)}function rejectIfNotSandbox(instance){return instance!=="sandbox"?formatSfccReadFailure({code:"VALIDATION_ERROR",status:400,message:`The Bridge site-preference tool only supports the 'sandbox' instance context. Received: '${instance}'.`,source:"tool",details:{guard:"sandbox_only",received_instance:instance,supported_instances:["sandbox"]}}):null}function buildSitePreferenceGetHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{group,instance,start,count}=sitePreferenceGetInput.parse(args),guard=rejectIfNotSandbox(instance);if(guard)return guard;let encodedGroup=encodeURIComponent(group),postBody={query:{match_all_query:{}}};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count);let result=await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-get-${safeGroup(group)}-${safeTimestamp3()}.json`,normalized.page)}))}function buildSitePreferenceSearchHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{group,instance,query,start,count,sorts}=sitePreferenceSearchInput.parse(args),guard=rejectIfNotSandbox(instance);if(guard)return guard;let encodedGroup=encodeURIComponent(group),postBody={query:typeof query=="string"?{text_query:{fields:["id"],search_phrase:query}}:query};start!==void 0&&(postBody.start=start),count!==void 0&&(postBody.count=count),sorts!==void 0&&(postBody.sorts=sorts);let result=await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`,postBody,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-search-${safeGroup(group)}-${safeTimestamp3()}.json`,normalized.page)}))}function buildSitePreferenceGroupListHandler(gateDeps,getDocsDir2){return withSfccReadErrorBoundary(withSfccGate(gateDeps,async(args,credentials)=>{let{count,start,projection}=sitePreferenceGroupListInput.parse(args),queryParams={...projectionQueryEntries("broad_list",projection)};count!==void 0&&(queryParams.count=String(count)),start!==void 0&&(queryParams.start=String(start));let queryStr=Object.keys(queryParams).length>0?"?"+new URLSearchParams(queryParams).toString():"",result=await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`,credentials);if(!result.ok)return formatOcapiReadFailure(result);let normalized=readSfccBody(result.body),text2=JSON.stringify(normalized,null,2),dir=path44.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text2,dir,`site-preference-group-list-${safeTimestamp3()}.json`,normalized.page)}))}function registerSitePreferenceTools(registerTool2,deps){let{gateDeps,getDocsDir:getDocsDir2}=deps;registerTool2("site_preference_group_list",{description:"List custom site preference group ids from the developer sandbox via GET /system_object_definitions/SitePreferences/attribute_groups. Read-only; use this to discover group ids before site_preference_get or site_preference_search. Accepts optional `count` and `start` for paging. Oversized outputs are auto-saved locally.",inputSchema:sitePreferenceGroupListInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceGroupListHandler(gateDeps,getDocsDir2)),registerTool2("site_preference_get",{description:"List preference IDENTIFIERS for a site preference group from the developer sandbox via a match-all POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Returns ids only, NOT values \u2014 'unset' and 'empty string' are indistinguishable. Read-only, sandbox-only. Optional start/count paging. Oversized payloads auto-saved locally.",inputSchema:sitePreferenceGetInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceGetHandler(gateDeps,getDocsDir2)),registerTool2("site_preference_search",{description:"Search preference IDENTIFIERS within a site preference group via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Returns ids only \u2014 this endpoint does not expose values. Read-only; v1 sandbox only. Pass a plain string for id-only text search or a structured OCAPI query. Oversized results auto-saved locally.",inputSchema:sitePreferenceSearchInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceSearchHandler(gateDeps,getDocsDir2))}import{z as z10}from"zod";function rejectIfNotSandboxForWrite(instance){let effective=instance===void 0?"sandbox":instance;return effective==="sandbox"?null:formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message:`SFCC write tools are sandbox-only. Refusing to write against instance '${effective}'. Re-run the write against a developer sandbox instance.`,source:"tool",details:{guard:"sandbox_only",received_instance:effective,supported_instances:["sandbox"]}})}function textResult4(text2){return{content:[{type:"text",text:text2}]}}function isPlainObject8(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function ocapiWriteFallbackMessage(status){return`The SFCC OCAPI Data API returned HTTP ${status} for this write and supplied no fault message. The upstream response body is preserved under error.details.`}function ocapiWriteFaultMessage(body,status){if(isPlainObject8(body)&&isPlainObject8(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiWriteFallbackMessage(status)}function formatOcapiWriteToolResult(result,operation,path48,ocapiVersion=DEFAULT_OCAPI_VERSION){return result.status===403?writeGrantForbiddenResult({operation,path:path48,ocapiVersion,body:result.body}):result.ok?textResult4(JSON.stringify({status:result.status,outcome:result.outcome,body:redactSfccBody(result.body)})):formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:result.status,message:ocapiWriteFaultMessage(result.body,result.status),source:"ocapi",details:{operation,path:path48,fault:result.fault,body:result.body}})}import{z as z8}from"zod";var localizedStringSchema=z8.record(z8.string(),z8.string()),objectAttributeValueTypeSchema=z8.enum(["string","int","double","text","html","date","image","boolean","money","quantity","datetime","email","password","set_of_string","set_of_int","set_of_double","enum_of_string","enum_of_int"]),OUTPUT_ONLY_BODY_PROPERTIES=["html","image"];function rejectOutputOnlyProperties(body,ctx){for(let key of OUTPUT_ONLY_BODY_PROPERTIES)Object.prototype.hasOwnProperty.call(body,key)&&ctx.addIssue({code:z8.ZodIssueCode.custom,path:[key],message:`'${key}' is an output-only OCAPI property and cannot be set on a write body. (Use value_type: "${key}" to declare an ${key} attribute instead.)`})}var objectAttributeDefinitionCommonShape={id:z8.string().optional(),system:z8.boolean().optional(),display_name:localizedStringSchema.optional(),description:localizedStringSchema.optional(),mandatory:z8.boolean().optional(),localizable:z8.boolean().optional(),site_specific:z8.boolean().optional(),default_value:z8.any().optional()},objectAttributeDefinitionCreateBodySchema=z8.object({value_type:objectAttributeValueTypeSchema.describe("Required OCAPI attribute value type."),...objectAttributeDefinitionCommonShape}).passthrough().superRefine(rejectOutputOnlyProperties),objectAttributeDefinitionPatchBodySchema=z8.object({value_type:objectAttributeValueTypeSchema.optional(),...objectAttributeDefinitionCommonShape}).passthrough().superRefine((body,ctx)=>{rejectOutputOnlyProperties(body,ctx),Object.keys(body).length===0&&ctx.addIssue({code:z8.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),attributeGroupPutBodySchema=z8.object({display_name:localizedStringSchema.describe("Localized group display name."),internal:z8.boolean().describe("Whether the group is internal (BM-only).")}),attributeGroupPatchBodySchema=z8.object({display_name:localizedStringSchema.optional(),internal:z8.boolean().optional()}).superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z8.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),SfccWritePayloadFault=class extends Error{status;faultType;constructor(faultType,message,status=400){super(message),this.name="SfccWritePayloadFault",this.faultType=faultType,this.status=status}};function buildObjectAttributeDefinitionCreatePayload(urlId,body){if(body.id!==void 0&&body.id!==urlId)throw new SfccWritePayloadFault("IdConflictException",`Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`);if(body.system===!0)throw new SfccWritePayloadFault("AttributeDefinitionKeyReadOnlyException","Cannot create a system attribute definition (system: true) over OCAPI writes.");return{...body,id:urlId,system:!1}}function buildObjectAttributeDefinitionPatchPayload(urlId,body){if(body.id!==void 0&&body.id!==urlId)throw new SfccWritePayloadFault("IdConflictException",`Attribute definition id '${body.id}' does not match the URL id '${urlId}'. Omit 'id' or set it equal to the URL id.`);if(body.system===!0)throw new SfccWritePayloadFault("AttributeDefinitionKeyReadOnlyException","Cannot patch an attribute definition to system: true over OCAPI writes.");return{...body}}function buildAttributeGroupPutPayload(body){return{display_name:body.display_name,internal:body.internal}}function buildAttributeGroupPatchPayload(body){return{...body}}function buildEmptyRelationPayload(){return{}}import{z as z9}from"zod";var WRITE_ANNOTATIONS={readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0};var SFCC_WRITE_VALIDATION_ERROR_MESSAGE="Input failed schema validation before any OCAPI call. See error.details.issues for the offending fields.",SFCC_WRITE_INTERNAL_ERROR_MESSAGE="An unexpected internal error occurred while handling this SFCC write. The underlying failure detail is withheld from this response to avoid leaking credentials or internal state.";function safeZodIssues2(err){return(Array.isArray(err.issues)?err.issues:[]).map(issue=>({path:Array.isArray(issue.path)?issue.path.map(String).join("."):"",message:typeof issue.message=="string"?issue.message:"Invalid input."}))}function zodValidationEnvelope(err){return formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message:SFCC_WRITE_VALIDATION_ERROR_MESSAGE,source:"tool",details:{issues:safeZodIssues2(err)}})}function payloadFaultEnvelope(fault){return formatSfccWriteFailure({code:"OCAPI_WRITE_ERROR",status:fault.status,message:fault.message,source:"tool",details:{fault:{type:fault.faultType,message:fault.message}}})}function validationEnvelope(message){return formatSfccWriteFailure({code:"VALIDATION_ERROR",status:400,message,source:"tool",details:{}})}function unexpectedEnvelope(){return formatSfccWriteFailure({code:"INTERNAL_ERROR",status:500,message:SFCC_WRITE_INTERNAL_ERROR_MESSAGE,source:"tool",details:{}})}function isSchemaFailure2(err){return err instanceof z9.ZodError?!0:typeof err=="object"&&err!==null&&err.name==="ZodError"&&Array.isArray(err.issues)}function isPayloadFault(err){return err instanceof SfccWritePayloadFault?!0:typeof err=="object"&&err!==null&&err.name==="SfccWritePayloadFault"&&typeof err.faultType=="string"&&typeof err.status=="number"}function preTransportErrorEnvelope(err){return isSchemaFailure2(err)?zodValidationEnvelope(err):isPayloadFault(err)?payloadFaultEnvelope(err):unexpectedEnvelope()}function encodedSegment(segment){return encodeURIComponent(segment)}function attributeDefinitionPath(objectType,attributeId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_definitions/${encodedSegment(attributeId)}`}function attributeGroupPath(objectType,groupId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}`}function attributeGroupAssignmentPath(objectType,groupId,attributeId){return`/system_object_definitions/${encodedSegment(objectType)}/attribute_groups/${encodedSegment(groupId)}/attribute_definitions/${encodedSegment(attributeId)}`}function preferenceObjectTypeForScope(scope){return scope==="site"?"SitePreferences":"OrganizationPreferences"}var instanceSchema=z10.string().optional().describe('Sandbox-only: omit (defaults to sandbox) or pass "sandbox".'),createAttributeDefinitionInput=z10.object({object_type:z10.string().describe('System object type, e.g. "Product".'),attribute_id:z10.string().describe("Attribute id (URL id); the body id must match it."),definition:objectAttributeDefinitionCreateBodySchema.describe("OCAPI ObjectAttributeDefinition create body (requires value_type)."),instance:instanceSchema}),updateAttributeDefinitionInput=z10.object({object_type:z10.string().describe("System object type."),attribute_id:z10.string().describe("Attribute id (URL id)."),patch:objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body (>=1 field)."),instance:instanceSchema}),createAttributeGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id (URL id)."),display_name:localizedStringSchema.describe("Localized group display name."),internal:z10.boolean().describe("Whether the group is internal (BM-only)."),instance:instanceSchema}),updateAttributeGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id (URL id)."),patch:attributeGroupPatchBodySchema.describe("Partial group body (display_name and/or internal; >=1 field)."),instance:instanceSchema}),assignAttributeToGroupInput=z10.object({object_type:z10.string().describe("System object type."),group_id:z10.string().describe("Attribute group id."),attribute_id:z10.string().describe("Attribute definition id to assign."),instance:instanceSchema}),createCustomPreferenceDefinitionInput=z10.object({preference_scope:z10.enum(["site","organization"]).describe("site \u2192 SitePreferences; organization \u2192 OrganizationPreferences."),preference_id:z10.string().describe("Preference (attribute) id."),definition:objectAttributeDefinitionCreateBodySchema.describe("OCAPI ObjectAttributeDefinition create body (requires value_type). Set default_value for booleans (else they resolve to null)."),instance:instanceSchema});function buildCreateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=updateAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id,parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}function buildCreateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPutPayload({display_name:parsed.display_name,internal:parsed.internal})}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=updateAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPatchPayload(parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}function buildAssignAttributeToGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48;try{let parsed=assignAttributeToGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path48=attributeGroupAssignmentPath(parsed.object_type,parsed.group_id,parsed.attribute_id)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,buildEmptyRelationPayload(),credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildCreateCustomPreferenceDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path48,body;try{let parsed=createCustomPreferenceDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let objectType=preferenceObjectTypeForScope(parsed.preference_scope);path48=attributeDefinitionPath(objectType,parsed.preference_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.preference_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}var SYSTEM_OBJECT_WRITE_TOOL_NAMES=["system_object_attribute_definition_create","system_object_attribute_definition_update","system_object_attribute_group_create","system_object_attribute_group_update","system_object_attribute_assign_to_group","custom_preference_definition_create"];function registerSystemObjectWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("system_object_attribute_definition_create",{description:"Create a system-object attribute definition (sandbox-only, destructive). PUT .../attribute_definitions/{id}; body id must equal URL id and system must be false. Returns 201. HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:createAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateAttributeDefinitionHandler(gateDeps)),registerTool2("system_object_attribute_definition_update",{description:"Update a system-object attribute definition (sandbox-only, destructive). PATCH .../attribute_definitions/{id} via the ETag GET-then-If-Match round trip. Returns 200; surfaces 409/412 on ETag conflicts. HTTP 403 echoes the write-grant JSON.",inputSchema:updateAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildUpdateAttributeDefinitionHandler(gateDeps)),registerTool2("system_object_attribute_group_create",{description:"Create a system-object attribute group (sandbox-only, destructive). PUT .../attribute_groups/{id} with a minimal body (display_name + internal). HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:createAttributeGroupInput,annotations:WRITE_ANNOTATIONS},buildCreateAttributeGroupHandler(gateDeps)),registerTool2("system_object_attribute_group_update",{description:"Update a system-object attribute group (sandbox-only, destructive). PATCH .../attribute_groups/{id} via the ETag round trip (display_name and/or internal). HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:updateAttributeGroupInput,annotations:WRITE_ANNOTATIONS},buildUpdateAttributeGroupHandler(gateDeps)),registerTool2("system_object_attribute_assign_to_group",{description:"Assign an attribute definition to an attribute group (sandbox-only, destructive). PUT .../attribute_groups/{group}/attribute_definitions/{def} with an empty body. HTTP 403 echoes the OCAPI write-grant JSON.",inputSchema:assignAttributeToGroupInput,annotations:WRITE_ANNOTATIONS},buildAssignAttributeToGroupHandler(gateDeps)),registerTool2("custom_preference_definition_create",{description:"Create a custom preference definition (sandbox-only, destructive). PUT .../{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}. Booleans resolve to null unless default_value is set. HTTP 403 echoes the write-grant JSON.",inputSchema:createCustomPreferenceDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateCustomPreferenceDefinitionHandler(gateDeps))}import{z as z12}from"zod";import{z as z11}from"zod";var objectAttributeValueTypeSchema2=z11.enum(["string","int","double","boolean","date","datetime","email","enum_of_int","enum_of_string","html","image","money","password","quantity","set_of_int","set_of_string","set_of_double","text"]),objectAttributeDefinitionCreateBodySchema2=z11.object({id:z11.string().min(1).optional(),value_type:objectAttributeValueTypeSchema2}).passthrough(),objectAttributeDefinitionPatchBodySchema2=z11.object({value_type:objectAttributeValueTypeSchema2.optional()}).passthrough().superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z11.ZodIssueCode.custom,message:"Patch body must contain at least one attribute-definition field to update."})});function buildObjectAttributeDefinitionCreatePayload2(attributeId,body){return{...body,id:attributeId}}function buildObjectAttributeDefinitionPatchPayload2(body){return{...body}}var INSTANCE_DESCRIBE2="OCAPI instance context. SFCC writes are sandbox-only; omit for sandbox. Any other value is rejected before OCAPI is called.",createCustomObjectAttributeDefinitionInput=z12.object({object_type:z12.string().describe("Known custom object type identifier (must already exist). OCAPI cannot create types, and cannot enumerate type IDs directly \u2014 only attribute definitions on a known type."),attribute_id:z12.string().describe("URL attribute-definition id. If the body also carries `id`, it must match."),definition:objectAttributeDefinitionCreateBodySchema2.describe("ObjectAttributeDefinition body; `value_type` is required for a create."),instance:z12.string().optional().describe(INSTANCE_DESCRIBE2)}),updateCustomObjectAttributeDefinitionInput=z12.object({object_type:z12.string().describe("Known custom object type identifier (must already exist). OCAPI cannot create types."),attribute_id:z12.string().describe("URL attribute-definition id to update."),patch:objectAttributeDefinitionPatchBodySchema2.describe("Partial ObjectAttributeDefinition body; must change at least one field."),instance:z12.string().optional().describe(INSTANCE_DESCRIBE2)});function customObjectAttributeDefinitionPath(objectType,attributeId){return`/custom_object_definitions/${encodedSegment(objectType)}/attribute_definitions/${encodedSegment(attributeId)}`}function buildCreateCustomObjectAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=createCustomObjectAttributeDefinitionInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;if(parsed.definition.id!==void 0&&parsed.definition.id!==parsed.attribute_id)return validationEnvelope(`Body id '${parsed.definition.id}' does not match URL attribute_id '${parsed.attribute_id}'.`);let path48=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id,parsed.definition);try{let result=await ocapiPut(path48,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path48)}catch{return unexpectedEnvelope()}})}function buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=updateCustomObjectAttributeDefinitionInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let path48=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload2(parsed.patch);try{let result=await ocapiPatch(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}var CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES=["custom_object_definition_attribute_create","custom_object_definition_attribute_update"];function registerSfccCustomObjectDefWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("custom_object_definition_attribute_create",{description:"Create an attribute definition on a KNOWN custom object type via PUT /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; the type must pre-exist (OCAPI cannot create types). Echoes paste-ready grant JSON on 403.",inputSchema:createCustomObjectAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateCustomObjectAttributeDefinitionHandler(gateDeps)),registerTool2("custom_object_definition_attribute_update",{description:"Update an attribute definition on a KNOWN custom object type via an ETag-conditional PATCH /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, destructive; surfaces 409/412 conflicts and echoes grant JSON on 403.",inputSchema:updateCustomObjectAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps))}import{z as z14}from"zod";import{z as z13}from"zod";var sitePreferenceValueSchema=z13.union([z13.string(),z13.number().finite(),z13.boolean(),z13.array(z13.string())]),sitePreferenceValuesPatchBodySchema=z13.record(z13.string(),sitePreferenceValueSchema).superRefine((values,ctx)=>{let keys=Object.keys(values);keys.length===0&&ctx.addIssue({code:z13.ZodIssueCode.custom,message:"At least one preference value is required."});for(let key of keys)key.startsWith("c_")||ctx.addIssue({code:z13.ZodIssueCode.custom,path:[key],message:`Preference id '${key}' must be a custom preference starting with 'c_'.`})});function buildSitePreferenceValuesPatchPayload(values){return{...values}}var INSTANCE_ENUM2=z14.enum(["staging","development","sandbox","production"]),INSTANCE_DESCRIBE3="OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected before OCAPI is called. Defaults to 'sandbox'.",sitePreferenceValuesSetInput=z14.object({group:z14.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM2.optional().default("sandbox").describe(INSTANCE_DESCRIBE3),values:sitePreferenceValuesPatchBodySchema.describe("Flat map of c_-prefixed preference ids to values (string, number, boolean, or string[]).")});function sitePreferenceGroupPath(group){return`/site_preferences/preference_groups/${encodedSegment(group)}/sandbox`}function buildSitePreferenceValuesSetHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let parsed;try{parsed=sitePreferenceValuesSetInput.parse(args)}catch(err){return preTransportErrorEnvelope(err)}let guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let path48=sitePreferenceGroupPath(parsed.group),body=buildSitePreferenceValuesPatchPayload(parsed.values);try{let result=await ocapiPatchDirect(path48,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path48)}catch{return unexpectedEnvelope()}})}var SITE_PREFERENCE_WRITE_TOOL_NAMES=["site_preference_values_set"];function registerSitePreferenceWriteTools(registerTool2,deps){let{gateDeps}=deps;registerTool2("site_preference_values_set",{description:"Set custom site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. Sandbox-only, destructive; body is a flat map of c_-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 CustomPreferenceGroupNotFoundException; echoes grant JSON on 403.",inputSchema:sitePreferenceValuesSetInput,annotations:WRITE_ANNOTATIONS},buildSitePreferenceValuesSetHandler(gateDeps))}var SFCC_WRITE_TOOL_NAMES=[...SYSTEM_OBJECT_WRITE_TOOL_NAMES,...CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES,...SITE_PREFERENCE_WRITE_TOOL_NAMES];function registerSfccWriteTools(registerTool2,deps){registerSystemObjectWriteTools(registerTool2,{gateDeps:deps.gateDeps}),registerSfccCustomObjectDefWriteTools(registerTool2,{gateDeps:deps.gateDeps}),registerSitePreferenceWriteTools(registerTool2,{gateDeps:deps.gateDeps})}import{z as z15}from"zod";function notConfigured2(failureClass,message,limits){return formatSfccFailure({code:"NOT_CONFIGURED",status:503,message,source:"gate",details:{failure_class:failureClass,...limits!==void 0?{limits}:{}}})}function withSfccLogGate(deps,handler){return async args=>{let body;try{let url=deps.buildGetUrl("/sfcc/logs/capability",{repo_name:deps.repoName}),resp=await fetch(url,{headers:await deps.getGetHeaders()});if(!resp.ok)return formatSfccFailure({code:resp.status===401||resp.status===403?"UNAUTHORIZED":"SERVICE_UNAVAILABLE",status:resp.status,message:"Could not read the SFCC log capability from Bridge API (/jira/sfcc/logs/capability). Ensure your Bridge API key is set and the repo is authorized. Run sfcc_setup_status for a full diagnostic.",source:"webdav",details:{},upstreamHttpStatus:resp.status});body=await resp.json()}catch{return formatSfccFailure({code:"BAD_GATEWAY",status:502,message:"Could not reach Bridge API to resolve the SFCC log capability. Check that BAPI_BASE_URL points to a running Bridge API instance.",source:"webdav",details:{}})}if(body?.configured!==!0){let failureClass=typeof body?.failure_class=="string"?body.failure_class:"not_configured",message=typeof body?.message=="string"?body.message:"SFCC on-demand log queries are not configured for this repository. Run sfcc_setup_status for a diagnostic.";return notConfigured2(failureClass,message,body?.limits)}return handler(args)}}var MAX_QUERY_RANGE_HOURS=24,HIGH_VOLUME_MAX_RANGE_HOURS=6,HIGH_VOLUME_PREFIXES=new Set(["info","jobs","debug","customdebug"]),MAX_SELECTED_PREFIXES=5,MAX_MAX_ENTRIES=2e3,SUPPORTED_ENVIRONMENTS=["production","staging","development"],PREFIX_RE=/^[a-z][a-z0-9]*$/;function backendFailureMessage(status){return`The Bridge API SFCC log-query endpoint returned HTTP ${status}. No log content is included in this response.`}function safeBackendDetail(text2){let parsed;try{parsed=JSON.parse(text2)}catch{return{}}if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{};let detail=parsed.detail;return typeof detail=="string"?{detail}:{}}function validationError(message){return formatSfccFailure({code:"VALIDATION_ERROR",status:400,message,source:"tool",details:{}})}var inputSchema=z15.object({environment:z15.enum(SUPPORTED_ENVIRONMENTS).describe("REQUIRED environment to scope the query to. Deliberately required so a query can never fan across every environment at once."),time_range:z15.object({start:z15.string().datetime({offset:!0}).describe("Inclusive ISO-8601 UTC start."),end:z15.string().datetime({offset:!0}).describe("Exclusive ISO-8601 UTC end (after start).")}).strict().describe("REQUIRED bounded window. No open-ended or inferred period is ever assumed."),prefixes:z15.array(z15.string().regex(PREFIX_RE,"prefix must be a letter followed by letters/digits")).max(MAX_SELECTED_PREFIXES).optional().describe(`Optional log-file prefix selection (max ${MAX_SELECTED_PREFIXES}). Empty = the shipped error-class defaults. High-volume prefixes (info/jobs/debug/customdebug) impose a stricter ${HIGH_VOLUME_MAX_RANGE_HOURS}h max time range.`),max_entries:z15.number().int().min(1).max(MAX_MAX_ENTRIES).optional().describe(`Optional per-query entry-scan cap (1..${MAX_MAX_ENTRIES}).`)});function semanticCheck(args){let start=Date.parse(args.time_range.start),end=Date.parse(args.time_range.end);if(Number.isNaN(start)||Number.isNaN(end))return"time_range.start and time_range.end must be valid ISO-8601 timestamps.";if(!(start<end))return"time_range.start must be strictly before time_range.end.";let prefixes=args.prefixes??[],maxHours=prefixes.some(p=>HIGH_VOLUME_PREFIXES.has(p))?HIGH_VOLUME_MAX_RANGE_HOURS:MAX_QUERY_RANGE_HOURS,spanHours=(end-start)/36e5;return spanHours>maxHours?`time_range spans ${spanHours.toFixed(1)}h but the maximum for this prefix selection is ${maxHours}h. environment and time_range are required, and the range is capped, to prevent broad cross-environment or open-ended log scans.`:new Set(prefixes).size!==prefixes.length?"prefixes must be unique.":null}function buildHandler(deps){return async rawArgs=>{let parsed=inputSchema.safeParse(rawArgs);if(!parsed.success)return validationError(parsed.error.issues.map(i=>i.message).join("; "));let args=parsed.data,semanticFailure=semanticCheck(args);if(semanticFailure)return validationError(semanticFailure);let body={repo_name:deps.repoName,environment:args.environment,time_range:{start:args.time_range.start,end:args.time_range.end},prefixes:args.prefixes??[],...args.max_entries!==void 0?{max_entries:args.max_entries}:{}},resp;try{let url=deps.buildGetUrl("/sfcc/logs/query",{repo_name:deps.repoName});resp=await fetch(url,{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)})}catch{return formatSfccFailure({code:"BAD_GATEWAY",status:502,message:"Could not reach Bridge API to run the SFCC log query. Check that BAPI_BASE_URL points to a running Bridge API instance.",source:"webdav",details:{}})}let text2=await resp.text();return resp.ok?{content:[{type:"text",text:text2}]}:formatSfccFailure({code:resp.status>=500?"SERVICE_UNAVAILABLE":"REQUEST_FAILED",status:resp.status,message:backendFailureMessage(resp.status),source:"webdav",details:safeBackendDetail(text2),upstreamHttpStatus:resp.status})}}function registerSfccLogQueryTool(registerTool2,deps){let gated=withSfccLogGate({buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,repoName:deps.repoName},buildHandler(deps));registerTool2("sfcc_log_query",{description:"Query redacted, filtered SFCC logs on demand, scoped to a REQUIRED environment and time_range. Runs pull\u2192redact\u2192filter on the Bridge backend; entries, time range, and log-file prefixes are capped (high-volume prefixes get a stricter range). Returns a NOT_CONFIGURED 503 when the log capability isn't set up \u2014 run sfcc_setup_status.",inputSchema,annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},gated)}function registerSfccTools(registerTool2,deps){let gateDeps={buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,repoName:deps.repoName};registerTool2("sfcc_setup_status",{description:"Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Always-registered; returns status without requiring full SFCC configuration to be complete.",inputSchema:z16.object({}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},buildSfccSetupStatusHandler(deps.buildGetUrl,deps.getGetHeaders,deps.repoName,deps.getResolvedApiKey));let gatedCheckPermissions=withSfccGate(gateDeps,async(_args,credentials)=>checkPermissionsTool(credentials));registerTool2("check_permissions",{description:"Probe SFCC OCAPI access via GET /system_object_definitions. On 200: reports OK and the detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).",inputSchema:z16.object({instance:z16.string().optional().describe("Explicit sandbox hostname to use instead of dw.json auto-detection.")}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},gatedCheckPermissions),deps.includeReadTools&&(registerSystemObjectReadTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccCustomObjectDefReadTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSitePreferenceTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccWriteTools(registerTool2,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccLogQueryTool(registerTool2,{buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,getPostHeaders:deps.getPostHeaders,repoName:deps.repoName}))}init_mcp_profile();init_errors();init_store();init_producer_ledger();init_taxonomy();import{readFileSync as readFileSync3,unlinkSync}from"node:fs";init_file_scope_guard();init_git_ci_types();init_git_inspection();init_producer_ledger();var COMMITTED_REF_PHASE="committed";function buildCommitCreatedEventInput(context,metadata){let details={repo:context.repo,branch:context.branch,worktree_path:context.worktree_path,commit_sha:metadata.sha,parent_shas:metadata.parents,author:{name:metadata.author_name,email:metadata.author_email},committer:{name:metadata.committer_name,email:metadata.committer_email},co_authors:metadata.co_authors,authored_at:metadata.authored_at,committed_at:metadata.committed_at,subject:metadata.subject,attribution_source:metadata.attribution_source};return{source:"git",type:"git.commit_created",subject:context.repo,producer:GIT_HOOK_PRODUCER,observed_via:"git-hook:post-commit",data:{summary:`Commit ${metadata.sha??"(unknown)"} on ${context.branch??"(detached)"}`,status:"created",details}}}function buildWorktreeChangedEventInput(context,updates,phase){let updatesHash=stableJsonHash({phase,updates}),details={repo:context.repo,branch:context.branch,worktree_path:context.worktree_path,transaction_phase:phase,ref_updates:updates,updates_hash:updatesHash};return{source:"git",type:"worktree.changed",subject:context.repo,producer:GIT_HOOK_PRODUCER,observed_via:"git-hook:reference-transaction",data:{summary:`${updates.length} ref update(s) on ${context.repo}`,status:"changed",details}}}async function runPostCommitHookProducer(deps={}){let getContext=deps.getContext??getGitWorktreeContext,readMetadata=deps.readMetadata??readHeadCommitMetadata,emitIfNew=deps.emitIfNew??emitConductorEventIfNew;try{let context=getContext({cwd:deps.cwd,env:deps.env}),metadata=readMetadata({cwd:deps.cwd});if(!metadata||metadata.sha===null)return{ok:!0,emitted:!1,reason:"no-head-commit"};let input=buildCommitCreatedEventInput(context,metadata),decision=await emitIfNew(input,{event_type:"git.commit_created",repo:context.repo,commit_sha:metadata.sha});return{ok:!0,emitted:decision.emitted,reason:decision.reason}}catch{return{ok:!0,emitted:!1,reason:"skipped"}}}async function runReferenceTransactionHookProducer(args,deps={}){let getContext=deps.getContext??getGitWorktreeContext,parseUpdates=deps.parseUpdates??parseReferenceTransactionUpdates,emitIfNew=deps.emitIfNew??emitConductorEventIfNew;try{if(args.phase!==COMMITTED_REF_PHASE)return{ok:!0,emitted:!1,reason:"non-committed-phase"};let updates=parseUpdates(args.stdin);if(updates.length===0)return{ok:!0,emitted:!1,reason:"no-updates"};let context=getContext({cwd:deps.cwd,env:deps.env}),input=buildWorktreeChangedEventInput(context,updates,args.phase),refUpdatesHash=stableJsonHash({phase:args.phase,updates}),decision=await emitIfNew(input,{event_type:"worktree.changed",repo:context.repo,ref_updates_hash:refUpdatesHash});return{ok:!0,emitted:decision.emitted,reason:decision.reason}}catch{return{ok:!0,emitted:!1,reason:"skipped"}}}init_supervisor_config();function formatConductorErrorLine(envelope){if(envelope.error==="LEDGER_NATIVE_MODULE_LOAD_FAILED"&&envelope.details){let d=envelope.details;return`${envelope.message} details: module=${d.module} node_version=${d.node_version} node_modules_abi=${d.node_modules_abi}`}return envelope.message}function getConductorUsage(){return["Usage: conductor <command> [options]","","Local append-only event ledger for multi-agent coordination.","Talks ONLY to the local SQLite store (~/.config/bridge/events.db); no Bridge API calls.","","Commands:"," emit-event Append one semantic event to the ledger"," supervise --run-id <id> Run the foreground, run-scoped supervisor loop"," epic-tick Run one stateless reconciliation pass for an Epic"," epic-status Print read-only health summary of an Epic Run"," send-message Enqueue ONE typed supervisor->worker relay message (idempotent)"," check-messages Read + ACK pending relay messages for a worker (no redelivery)"," doctor Read-only health/diagnostics report (ledger + git hooks)"," purge Delete ALL ledger rows (events, messages, supervisor_projection)"," install-git-hooks Install local, opportunistic, non-blocking git hooks"," git-hook post-commit Run the post-commit producer (invoked by the installed hook)"," git-hook reference-transaction --phase <p> --stdin-file <f>"," Run the reference-transaction producer (invoked by the hook)"," file-scope-guard Warn-only: compare the branch diff against the declared"," touched-file set (always exits 0; never blocks a PR)","","supervise options:"," --run-id <id> Run/session identifier to supervise (required)"," --wake-interval-ms <n> Deterministic event-poll cadence (clamped 30000..60000)"," --global-timeout-ms <n> Total wall-clock ceiling for the run"," --escalation-cooldown-ms <n> Min gap between escalations for the same worker+reason","","install-git-hooks notes:"," Hooks are LOCAL, unversioned, opportunistic, and bypassable. Missing hooks are a"," degraded optional capability and never prevent PR/CI gate evaluation.","","emit-event options:"," --type <t> Semantic event type (required). One of:",` ${SEMANTIC_EVENT_TYPES.join(", ")}`," --source <s> Logical producer (required)"," --subject <s> Subject the event is about (e.g. ticket key)"," --run-id <s> Run/session identifier"," --worker-id <s> Worker/agent identifier"," --producer <s> Finer-grained producer identity"," --schema-version <n> Event schema version (default 1)"," --time <iso> ISO-8601 event time (default now)"," --confidence <0..1> Confidence score"," --observed-via <s> Channel the event was observed through"," --data-json <json> Normalized data object (allowlisted top-level keys)"," --data-json-stdin Read the complete normalized data object as JSON from stdin"," (mutually exclusive with --data-json; keeps raw payloads and"," secrets out of the process argument list)"," --raw-json <json> Tool-native object; nested under data.raw"," --payload-ref <ref> Reference for large external payloads (-> data.payload_ref)"," --json Print compact JSON result","","send-message options:"," --run-id <s> Run/session identifier (required)"," --worker-id <s> Target worker identifier (required)"," --type <s> Typed message kind, e.g. supervisor.worker_stalled (required)"," --cause-seq <n> Idempotency cause sequence, non-negative integer (required)"," --payload-json <json> Compact payload object (allowlisted top-level keys)"," --payload-json-stdin Read the payload object as JSON from stdin"," (mutually exclusive with --payload-json)"," --available-at <iso> ISO-8601 time the message becomes available (default now)"," --cooldown-ms <n> Per-call cooldown override in ms"," --json Print compact JSON result"," Note: a duplicate idempotency key or a same-type message inside the cooldown"," window does NOT enqueue a second message.","","check-messages options:"," --run-id <s> Run/session identifier (required)"," --worker-id <s> Worker identifier (required)"," --limit <n> Max messages to deliver/ack (default 10, max 100)"," --json Print compact JSON result"," Note: returned messages are ACKNOWLEDGED by this call and are not redelivered.","","doctor / purge options:"," --json Print machine-readable JSON"," --no-deny-probe (doctor only) Skip the deny-enforcement preflight \u2014 no headless"," agent is spawned; the deny_enforcement section reports an"," explicit skipped state (enforcement UNVERIFIED, never enforced)","","Examples:"," conductor emit-event --type run.started --source git-hook --run-id BAPI-393 \\",` --data-json '{"summary":"run started"}'`," conductor emit-event --type git.commit_created --source git-hook \\",` --raw-json '{"branch":"feature/x","sha":"abc123"}'`," conductor emit-event --type ci.failed --source ci \\"," --payload-ref 'file:///tmp/ci-log.txt'"," conductor emit-event --type merge.succeeded --source conductor-merge \\",` --worker-id w1 --data-json '{"summary":"auto-merged","status":"succeeded"}'`," conductor doctor --json"," conductor purge","","epic-tick options:"," --epic-key <KEY> Epic key to supervise (required, non-empty)"," --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)"," --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)","","approve-plan options:"," approve-plan <epic_key> --plan-version N [--json]"," <epic_key> Epic key (e.g. EPIC-405) (required positional)"," --plan-version <n> Strictly positive plan version to approve (required)"," --json Print compact JSON result"," --help Print this usage message","","epic-status options:"," --epic-key <KEY> Epic key to fetch the snapshot for (required)"," --json Print compact JSON result"," --help Print this usage message","","Examples:"," conductor approve-plan EPIC-405 --plan-version 2"," conductor approve-plan EPIC-405 --plan-version 2 --json"," conductor epic-status --epic-key EPIC-405"," conductor epic-status --epic-key EPIC-405 --json"].join(`
5644
+ `)}var VALID_COMMANDS=new Set(["emit-event","supervise","epic-tick","approve-plan","epic-status","send-message","check-messages","doctor","purge","install-git-hooks","git-hook","file-scope-guard"]);function parseConductorArgs(argv){if(argv.length===0)return{kind:"help"};let first=argv[0];return first==="-h"||first==="--help"?{kind:"help"}:first.startsWith("-")?{kind:"error",message:`Unknown option "${first}". Run "conductor --help" for usage.`}:VALID_COMMANDS.has(first)?{kind:"command",command:first,argv:argv.slice(1)}:{kind:"error",message:`Unknown command "${first}". Run "conductor --help" for usage.`}}function tokenizeFlags(argv,valueFlags,boolFlags){let values=new Map,bools=new Set;for(let i=0;i<argv.length;i+=1){let token=argv[i];if(token==="-h"){bools.add("--help");continue}if(!token.startsWith("--"))throw new ConductorValidationError(`Unexpected argument "${token}".`);let eq=token.indexOf("="),name=eq>=0?token.slice(0,eq):token;if(boolFlags.has(name)){bools.add(name);continue}if(!valueFlags.has(name))throw new ConductorValidationError(`Unknown flag "${name}".`);let value;if(eq>=0)value=token.slice(eq+1);else{let next=argv[i+1];if(next===void 0)throw new ConductorValidationError(`Flag "${name}" requires a value.`);value=next,i+=1}values.set(name,value)}return{values,bools}}var EMIT_VALUE_FLAGS=new Set(["--type","--source","--id","--subject","--run-id","--worker-id","--producer","--schema-version","--time","--confidence","--observed-via","--data-json","--raw-json","--payload-ref"]),EMIT_BOOL_FLAGS=new Set(["--json","--help","--data-json-stdin"]);function defaultReadStdin(){return readFileSync3(0,"utf-8")}function parseJsonFlag(raw,flag){try{return JSON.parse(raw)}catch{throw new ConductorValidationError(`Flag "${flag}" must be valid JSON.`)}}function isPlainObject10(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function parseEmitEventArgs(argv,deps={}){let{values,bools}=tokenizeFlags(argv,EMIT_VALUE_FLAGS,EMIT_BOOL_FLAGS);if(bools.has("--help"))return{input:{source:"",type:"run.started"},json:bools.has("--json"),help:!0};let type=values.get("--type"),source=values.get("--source");if(!type)throw new ConductorValidationError('Flag "--type" is required for emit-event.');if(!source)throw new ConductorValidationError('Flag "--source" is required for emit-event.');let data={},dataJsonRaw=values.get("--data-json"),dataJsonStdin=bools.has("--data-json-stdin");if(dataJsonRaw!==void 0&&dataJsonStdin)throw new ConductorValidationError('Flags "--data-json" and "--data-json-stdin" are mutually exclusive; pass the normalized data object exactly one way.');if(dataJsonStdin){let stdinRaw=(deps.readStdin??defaultReadStdin)(),parsed=parseJsonFlag(stdinRaw,"--data-json-stdin");if(!isPlainObject10(parsed))throw new ConductorValidationError('Flag "--data-json-stdin" must be a JSON object.');data={...parsed}}else if(dataJsonRaw!==void 0){let parsed=parseJsonFlag(dataJsonRaw,"--data-json");if(!isPlainObject10(parsed))throw new ConductorValidationError('Flag "--data-json" must be a JSON object.');data={...parsed}}let rawJsonRaw=values.get("--raw-json");if(rawJsonRaw!==void 0){let parsedRaw=parseJsonFlag(rawJsonRaw,"--raw-json");if(!isPlainObject10(parsedRaw))throw new ConductorValidationError('Flag "--raw-json" must be a JSON object.');let existingRaw=isPlainObject10(data.raw)?data.raw:{};data.raw={...existingRaw,...parsedRaw}}let payloadRef=values.get("--payload-ref");payloadRef!==void 0&&(data.payload_ref=payloadRef);let schemaVersionRaw=values.get("--schema-version"),confidenceRaw=values.get("--confidence"),input={source,type,id:values.get("--id"),subject:values.get("--subject"),run_id:values.get("--run-id"),worker_id:values.get("--worker-id"),producer:values.get("--producer"),time:values.get("--time"),observed_via:values.get("--observed-via"),data};if(schemaVersionRaw!==void 0){let n=Number.parseInt(schemaVersionRaw,10);if(!Number.isFinite(n))throw new ConductorValidationError('Flag "--schema-version" must be an integer.');input.schema_version=n}if(confidenceRaw!==void 0){let n=Number.parseFloat(confidenceRaw);if(!Number.isFinite(n))throw new ConductorValidationError('Flag "--confidence" must be a number.');input.confidence=n}return{input,json:bools.has("--json"),help:!1}}async function runEmitEventCommand(argv,deps={}){let parsed=parseEmitEventArgs(argv,deps);if(parsed.help)return console.log(getConductorUsage()),0;let result;try{result=await emitConductorEvent(parsed.input)}catch(error){if(isDuplicateConstraintError2(error)){let dup={ok:!1,reason:"duplicate"};return console.log(parsed.json?JSON.stringify(dup):JSON.stringify(dup,null,2)),0}throw error}return parsed.json?console.log(JSON.stringify(result)):console.log(JSON.stringify(result,null,2)),0}var SEND_MESSAGE_VALUE_FLAGS=new Set(["--run-id","--worker-id","--type","--cause-seq","--payload-json","--available-at","--cooldown-ms"]),SEND_MESSAGE_BOOL_FLAGS=new Set(["--payload-json-stdin","--json","--help"]);function parseSendMessageArgs(argv,deps={}){let{values,bools}=tokenizeFlags(argv,SEND_MESSAGE_VALUE_FLAGS,SEND_MESSAGE_BOOL_FLAGS);if(bools.has("--help"))return{input:{run_id:"",worker_id:"",type:"",cause_seq:0},json:bools.has("--json"),help:!0};let runId=values.get("--run-id"),workerId=values.get("--worker-id"),type=values.get("--type"),causeSeqRaw=values.get("--cause-seq");if(!runId)throw new ConductorValidationError('Flag "--run-id" is required for send-message.');if(!workerId)throw new ConductorValidationError('Flag "--worker-id" is required for send-message.');if(!type)throw new ConductorValidationError('Flag "--type" is required for send-message.');if(causeSeqRaw===void 0)throw new ConductorValidationError('Flag "--cause-seq" is required for send-message.');if(!/^\d+$/.test(causeSeqRaw.trim()))throw new ConductorValidationError('Flag "--cause-seq" must be a non-negative integer.');let causeSeq=Number.parseInt(causeSeqRaw.trim(),10),payload={},payloadInline=values.get("--payload-json"),payloadStdin=bools.has("--payload-json-stdin");if(payloadInline!==void 0&&payloadStdin)throw new ConductorValidationError('Flags "--payload-json" and "--payload-json-stdin" are mutually exclusive; pass the payload object exactly one way.');if(payloadStdin){let readStdin=deps.readStdin??defaultReadStdin,parsed=parseJsonFlag(readStdin(),"--payload-json-stdin");if(!isPlainObject10(parsed))throw new ConductorValidationError('Flag "--payload-json-stdin" must be a JSON object.');payload={...parsed}}else if(payloadInline!==void 0){let parsed=parseJsonFlag(payloadInline,"--payload-json");if(!isPlainObject10(parsed))throw new ConductorValidationError('Flag "--payload-json" must be a JSON object.');payload={...parsed}}let input={run_id:runId,worker_id:workerId,type,cause_seq:causeSeq,payload},availableAt=values.get("--available-at");availableAt!==void 0&&(input.available_at=availableAt);let cooldownRaw=values.get("--cooldown-ms");if(cooldownRaw!==void 0){if(!/^\d+$/.test(cooldownRaw.trim()))throw new ConductorValidationError('Flag "--cooldown-ms" must be a non-negative integer.');input.cooldown_ms=Number.parseInt(cooldownRaw.trim(),10)}return{input,json:bools.has("--json"),help:!1}}async function runSendMessageCommand(argv,deps={}){let parsed=parseSendMessageArgs(argv,deps);if(parsed.help)return console.log(getConductorUsage()),0;let result=await sendWorkerMessage(parsed.input);return parsed.json?console.log(JSON.stringify(result)):console.log([`Message ${result.status}.`,` id: ${result.message.id}`,` type: ${result.message.type}`,` state: ${result.message.state}`].join(`
5645
5645
  `)),0}var CHECK_MESSAGES_VALUE_FLAGS=new Set(["--run-id","--worker-id","--limit"]),CHECK_MESSAGES_BOOL_FLAGS=new Set(["--json","--help"]);function parseCheckMessagesArgs(argv){let{values,bools}=tokenizeFlags(argv,CHECK_MESSAGES_VALUE_FLAGS,CHECK_MESSAGES_BOOL_FLAGS);if(bools.has("--help"))return{input:{run_id:"",worker_id:""},json:bools.has("--json"),help:!0};let runId=values.get("--run-id"),workerId=values.get("--worker-id");if(!runId)throw new ConductorValidationError('Flag "--run-id" is required for check-messages.');if(!workerId)throw new ConductorValidationError('Flag "--worker-id" is required for check-messages.');let input={run_id:runId,worker_id:workerId},limitRaw=values.get("--limit");if(limitRaw!==void 0){if(!/^\d+$/.test(limitRaw.trim()))throw new ConductorValidationError('Flag "--limit" must be a positive integer.');input.limit=Number.parseInt(limitRaw.trim(),10)}return{input,json:bools.has("--json"),help:!1}}async function runCheckMessagesCommand(argv){let parsed=parseCheckMessagesArgs(argv);if(parsed.help)return console.log(getConductorUsage()),0;let result=await checkWorkerMessages(parsed.input);if(parsed.json)return console.log(JSON.stringify(result)),0;let lines=[`Acknowledged ${result.acked_count} of ${result.count} message(s).`];for(let message of result.messages)lines.push(` ${message.id} [${message.type}] -> ${message.state}`);return console.log(lines.join(`
5646
5646
  `)),0}var DIAGNOSTIC_BOOL_FLAGS=new Set(["--json","--help"]),DOCTOR_BOOL_FLAGS=new Set([...DIAGNOSTIC_BOOL_FLAGS,"--no-deny-probe"]);async function runDoctorCommand(argv,deps={}){let{bools}=tokenizeFlags(argv,new Set,DOCTOR_BOOL_FLAGS);if(bools.has("--help"))return console.log(getConductorUsage()),0;let report=await buildConductorDoctorReport(bools.has("--no-deny-probe")?{...deps,skipDenyProbe:!0}:deps);return bools.has("--json")?(console.log(JSON.stringify({...report.ledger,git_hooks:report.git_hooks,epic_tick:report.epic_tick,mcp_profile:report.mcp_profile,native_ledger:report.native_ledger,deny_enforcement:report.deny_enforcement})),0):(console.log(formatConductorDoctorReport(report)),0)}function runInstallGitHooksCommand(argv){let{bools}=tokenizeFlags(argv,new Set,DIAGNOSTIC_BOOL_FLAGS);if(bools.has("--help"))return console.log(getConductorUsage()),0;let result=installConductorGitHooks();if(bools.has("--json"))return console.log(JSON.stringify(result)),0;let lines=["Conductor git hooks install","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",`is git worktree: ${result.is_worktree}`,`hooks dir: ${result.hooks_dir??"n/a"}`];for(let hook of result.installed)lines.push(` ${hook.name}: ${hook.action}${hook.warning?` (${hook.warning})`:""}`);if(result.warnings.length>0){lines.push("warnings:");for(let w of result.warnings)lines.push(` - ${w}`)}return console.log(lines.join(`
5647
5647
  `)),0}var GIT_HOOK_VALUE_FLAGS=new Set(["--phase","--stdin-file"]),GIT_HOOK_BOOL_FLAGS=new Set(["--help"]);function parseGitHookArgs(argv){let subcommand=argv[0];if(subcommand!=="post-commit"&&subcommand!=="reference-transaction")throw new ConductorValidationError(`Unknown git-hook subcommand "${subcommand??""}". Expected "post-commit" or "reference-transaction".`);let{values}=tokenizeFlags(argv.slice(1),GIT_HOOK_VALUE_FLAGS,GIT_HOOK_BOOL_FLAGS);return{subcommand,phase:values.get("--phase"),stdinFile:values.get("--stdin-file")}}async function runGitHookCommand(argv){let parsed;try{parsed=parseGitHookArgs(argv)}catch{return process.stderr.write(`Warning: conductor git-hook invocation was invalid.
@@ -5650,9 +5650,9 @@ Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path42 fr
5650
5650
  Tickets (${state.ticket_statuses.length}):`);for(let t of state.ticket_statuses){let dispatchRef=t.dispatch_run_id?`dispatch_run_id=${t.dispatch_run_id}`:"-";lines.push(` ${t.ticket_key} [${t.status}] ${dispatchRef} remediation=${t.remediation_attempts}/${t.remediation_no_progress_attempts}`)}lines.push(`
5651
5651
  Dispatches (${state.dispatches.length}):`);for(let d of state.dispatches){let runRef=d.run_id?`run_id=${d.run_id}`:"-";lines.push(` ${d.dispatch_key} [${d.status}] ${runRef}`)}return console.log(lines.join(`
5652
5652
  `)),0}catch(error){if(error instanceof BridgeApiError&&error.status===404)return parsed.json?console.log(JSON.stringify({epic_key:parsed.epicKey,status:"unknown",state:null})):console.log("No such epic found."),0;let envelope=toConductorErrorEnvelope(error);return parsed.json?console.log(JSON.stringify(envelope)):console.error(`Error: ${formatConductorErrorLine(envelope)}`),envelope.status>=500?2:1}}var SUPERVISE_VALUE_FLAGS=new Set(["--run-id","--wake-interval-ms","--global-timeout-ms","--escalation-cooldown-ms"]),SUPERVISE_BOOL_FLAGS=new Set(["--help"]);function parsePositiveIntFlag(values,flag){let raw=values.get(flag);if(raw===void 0)return;if(!/^\d+$/.test(raw.trim()))throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);let n=Number.parseInt(raw.trim(),10);if(!Number.isFinite(n))throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);return n}function parseSuperviseArgs(argv){let{values,bools}=tokenizeFlags(argv,SUPERVISE_VALUE_FLAGS,SUPERVISE_BOOL_FLAGS);if(bools.has("--help"))return{runId:"",overrides:{},help:!0};let runIdRaw=values.get("--run-id");if(runIdRaw===void 0||runIdRaw.trim().length===0)throw new ConductorValidationError('Flag "--run-id" is required for supervise and must be non-empty.');let overrides={},wake=parsePositiveIntFlag(values,"--wake-interval-ms");wake!==void 0&&(overrides.wake_interval_ms=wake);let globalTimeout=parsePositiveIntFlag(values,"--global-timeout-ms");globalTimeout!==void 0&&(overrides.global_timeout_ms=globalTimeout);let cooldown=parsePositiveIntFlag(values,"--escalation-cooldown-ms");return cooldown!==void 0&&(overrides.escalation_cooldown_ms=cooldown),{runId:runIdRaw.trim(),overrides,help:!1}}async function runSuperviseCommand(argv){let parsed=parseSuperviseArgs(argv);if(parsed.help)return console.log(getConductorUsage()),0;let config=resolveSupervisorConfig(parsed.overrides);console.log(`[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms global_timeout=${config.global_timeout_ms}ms`);let{runSupervisor:runSupervisor2}=await Promise.resolve().then(()=>(init_supervisor_runtime(),supervisor_runtime_exports));return(await runSupervisor2({run_id:parsed.runId,config})).exit_code}async function runPurgeCommand(argv){let{bools}=tokenizeFlags(argv,new Set,DIAGNOSTIC_BOOL_FLAGS);if(bools.has("--help"))return console.log(getConductorUsage()),0;let result=await purgeConductorLedger();return bools.has("--json")?(console.log(JSON.stringify(result)),0):(console.log([`Conductor ledger purged (existed: ${result.existed}).`,` events: ${result.deleted.events}`,` messages: ${result.deleted.messages}`,` supervisor_projection: ${result.deleted.supervisor_projection}`].join(`
5653
- `)),0)}async function runConductorCli(argv){let parsed=parseConductorArgs(argv);if(parsed.kind==="help")return console.log(getConductorUsage()),0;if(parsed.kind==="error")return console.error(`Error: ${parsed.message}`),1;try{switch(parsed.command){case"emit-event":return await runEmitEventCommand(parsed.argv);case"supervise":return await runSuperviseCommand(parsed.argv);case"epic-tick":return await runEpicTickCommand(parsed.argv);case"approve-plan":return await runApprovePlanCommand(parsed.argv);case"epic-status":return await runEpicStatusCommand(parsed.argv);case"send-message":return await runSendMessageCommand(parsed.argv);case"check-messages":return await runCheckMessagesCommand(parsed.argv);case"doctor":return await runDoctorCommand(parsed.argv);case"purge":return await runPurgeCommand(parsed.argv);case"install-git-hooks":return runInstallGitHooksCommand(parsed.argv);case"git-hook":return await runGitHookCommand(parsed.argv);case"file-scope-guard":return runFileScopeGuardCli();default:return console.error('Error: Unknown command. Run "conductor --help" for usage.'),1}}catch(error){let envelope=toConductorErrorEnvelope(error);return console.error(`Error: ${formatConductorErrorLine(envelope)}`),envelope.status>=500?2:1}}init_pr_ci_producer();import{generateDecisionPageHtml}from"./decision-page-template.js";import{z as z17}from"zod";var ActionableItemSchema=z17.object({id:z17.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores"),question:z17.string().min(1),original_question:z17.string().optional().describe("Optional display-only field: the clarifying question or critique point as originally raised; soft cap ~30 words. Omit it (or pass an empty string) for non-review callers \u2014 the renderer omits the section when it is absent or blank."),why_it_matters:z17.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),recommendation_explanation:z17.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),codebase_evidence:z17.string().optional().describe("Optional display-only field: combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block, which is omitted when this field is absent or blank."),source:z17.string().optional().describe(`Optional source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'. When absent the rendered card emits data-source="".`),recommendation_index:z17.number().int().min(0).describe("0-based index of the recommended option in the options array"),options:z17.array(z17.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),option_consequences:z17.array(z17.string().min(1)).min(2).max(4).describe("Behavioral consequence per branch, parallel to options. Must have 2\u20134 entries; length must equal options.length.")}).superRefine((item,ctx)=>{item.option_consequences.length!==item.options.length&&ctx.addIssue({code:z17.ZodIssueCode.custom,path:["option_consequences"],message:`option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`}),item.recommendation_index>=item.options.length&&ctx.addIssue({code:z17.ZodIssueCode.custom,path:["recommendation_index"],message:`recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`})}),DecisionPageLabelsSchema=z17.object({title:z17.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),intro:z17.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),section_heading:z17.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),improvements_heading:z17.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')}),SystemGoalNfrSchema=z17.object({category:z17.string().min(1).describe("Canonical NFR category, e.g. security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility."),requirement:z17.string().min(1).describe("The non-functional requirement itself."),implication:z17.string().min(1).describe("What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."),status:z17.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved (also surface as an actionable_items card).")}),AcceptanceCriterionSchema=z17.object({id:z17.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores").describe("Stable per-criterion id, e.g. AC-1. Becomes the captured-feedback JSON key."),criterion:z17.string().min(1).describe("What the system must do, stated concretely enough to be verified."),verification:z17.string().min(1).describe("How we would confirm this criterion is met. Required \u2014 a criterion with no way to check it is not yet a criterion."),status:z17.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved.")}),SystemGoalsSchema=z17.object({business_goal:z17.string().min(1).describe("The business goal this work serves."),desired_end_state:z17.string().min(1).describe("The end-state the system should reach."),system_behavior:z17.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),acceptance_criteria:z17.array(AcceptanceCriterionSchema).optional().default([]).describe("What the system must do, as verifiable criteria. Implementation options should be derived from these rather than the reverse."),nfrs:z17.array(SystemGoalNfrSchema).optional().default([])}),ImplementationOrderItemSchema=z17.object({title:z17.string().min(1).describe("Short title of the slice / child ticket."),depends_on:z17.array(z17.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),recommended_after:z17.array(z17.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),rationale:z17.string().min(1).describe("Why this slice sits at this point in the order.")}),DecisionPageInputShape={ticket_key:z17.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z17.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) is the ticket-review decision-capture page and is unaffected by the planning fields. "pre_ticket_planning" additionally renders the read-only system_goals and implementation_order sections for pre-ticket epic/task framing.'),system_goals:SystemGoalsSchema.optional().describe("pre_ticket_planning only: business goal, desired end-state, system behavior, acceptance criteria (what the system must do), and classified NFRs (the standards it must meet). Acceptance criteria and NFRs render with per-item stance controls. Unresolved (open) NFRs should ALSO be passed as actionable_items so the human can decide them."),implementation_order:z17.array(ImplementationOrderItemSchema).optional().describe("pre_ticket_planning epic surfaces only: read-only recommended implementation order (hard depends_on vs soft recommended_after). No Jira links are created from this."),output_subdir:z17.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). Validated strictly: no absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z17.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html and contain no path separators; the .html suffix is required and never auto-appended.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading). Presentation-only; does not change data-testid hooks or the submitted JSON shape."),actionable_items:z17.array(ActionableItemSchema).optional().default([]).describe("Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."),clear_improvements:z17.array(z17.object({id:z17.string().min(1).describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),title:z17.string().min(1),action:z17.string().min(1),confidence:z17.string().min(1),source:z17.string().min(1).describe("Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user \u2014 the confirmed-improvements list shows title/confidence/action only.")})).optional().default([]).describe("Confirmed improvements displayed as informational list, not submitted.")},DecisionPageInputSchema=z17.object(DecisionPageInputShape),DecisionPageLeanInputShape={ticket_key:z17.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z17.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'),output_subdir:z17.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z17.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."),content:z17.record(z17.string(),z17.unknown()).optional().describe("Contains deferred heavy payloads like actionable_items or system_goals.")};import{writeFile as writeFile12,mkdir as mkdir12}from"fs/promises";import path45 from"path";function slugify(text2,maxLength=60){return text2.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-").replace(/-+/g,"-").slice(0,maxLength).replace(/-$/,"")}function sanitizeProviderForFilename(provider){return provider.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")||"provider"}function buildBrainstormResultFilename(envelope,row,subject){let providerSegment=sanitizeProviderForFilename(row.provider),subjectSlug=subject?slugify(subject):"",shortId=envelope.brainstorm_id.slice(0,8);return subjectSlug?`${subjectSlug}-${shortId}-${providerSegment}.md`:`${envelope.brainstorm_id}-${providerSegment}.md`}async function saveBrainstormResultsToDir(envelope,dir,subject){let savedPaths=[];for(let row of envelope.results){let markdown=row.markdown;if(!markdown)continue;let filename=buildBrainstormResultFilename(envelope,row,subject),filePath=path45.join(dir,filename);try{await mkdir12(dir,{recursive:!0}),await writeFile12(filePath,markdown,"utf-8"),savedPaths.push(filePath)}catch{}}return savedPaths}import{createHash as createHash7}from"node:crypto";var PLAN_PROVENANCE_CLASSES=["implementation","documentation","unit_tests","e2e_tests","rendered_ui_review","test_gap_review","final_plan_review"],PLAN_PHASES=["produce","pre_pr_verification","post_pr_gap_close"],PLAN_STEP_DISPOSITIONS=["executed","adapted","escalated","unrun-advisory"],MECHANICAL_ADAPTATION_KINDS=["locator-correction","repository-command-correction","equivalent-implementation-recognized"],ESCALATION_ONLY_CATEGORIES=["design","schema","public-api","dependencies","security"],PLAN_CLASS_OWNERSHIP=Object.freeze({implementation:"produce",documentation:"produce",unit_tests:"pre_pr_verification",e2e_tests:"pre_pr_verification",rendered_ui_review:"pre_pr_verification",test_gap_review:"pre_pr_verification",final_plan_review:"pre_pr_verification"}),PRE_PR_VERIFICATION_LIMITS=Object.freeze({maxCorrectionTurns:3,maxChangedFiles:40,maxDiffLines:2e3}),RENDERED_UI_MAX_CYCLES=3,PlanLedgerError=class extends Error{constructor(message){super(message),this.name="PlanLedgerError"}};function isPlainObject10(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isInteger(value)&&value>=1}function validatePlanMetadata(value){if(value==null)throw new PlanLedgerError("plan metadata is absent; routing requires provenance and must not be inferred from plan prose");if(!isPlainObject10(value))throw new PlanLedgerError(`plan metadata must be an object, received ${Array.isArray(value)?"array":typeof value}`);if(value.version!==1)throw new PlanLedgerError(`unsupported plan metadata version ${String(value.version)}; expected 1`);let rawParts=value.parts;if(!Array.isArray(rawParts)||rawParts.length===0)throw new PlanLedgerError("plan metadata must carry a non-empty parts array");let parts=[],seenIds=new Set,previousEnd=0,previousId="";for(let[index,raw]of rawParts.entries()){if(!isPlainObject10(raw))throw new PlanLedgerError(`plan metadata part at index ${index} is not an object`);let partId=raw.part_id;if(typeof partId!="string"||partId.trim()==="")throw new PlanLedgerError(`plan metadata part at index ${index} has an empty part_id`);if(seenIds.has(partId))throw new PlanLedgerError(`duplicate plan metadata part_id '${partId}'`);seenIds.add(partId);let producer=raw.producer;if(typeof producer!="string"||producer.trim()==="")throw new PlanLedgerError(`plan metadata part '${partId}' has an empty producer`);let provenanceClass=raw.provenance_class;if(typeof provenanceClass!="string"||!PLAN_PROVENANCE_CLASSES.includes(provenanceClass))throw new PlanLedgerError(`plan metadata part '${partId}' has unknown provenance class '${String(provenanceClass)}'`);let startStep=raw.start_step,endStep=raw.end_step;if(!isPositiveInteger2(startStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive start_step ${String(startStep)}`);if(!isPositiveInteger2(endStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive end_step ${String(endStep)}`);if(endStep<startStep)throw new PlanLedgerError(`plan metadata part '${partId}' has a reversed range ${startStep}-${endStep}`);if(startStep<=previousEnd)throw new PlanLedgerError(`plan metadata part '${partId}' range ${startStep}-${endStep} overlaps or precedes '${previousId}' ending at ${previousEnd}; ranges must be disjoint and ascending`);if(startStep>previousEnd+1)throw new PlanLedgerError(`plan metadata part '${partId}' starts at step ${startStep} but '${previousId}' ended at ${previousEnd}; steps ${previousEnd+1}-${startStep-1} are claimed by no part and would be executed by no phase`);previousEnd=endStep,previousId=partId,parts.push({part_id:partId,producer,provenance_class:provenanceClass,start_step:startStep,end_step:endStep,declared_advisory:raw.declared_advisory===!0})}let totalSteps=value.total_steps;if(typeof totalSteps!="number"||!Number.isInteger(totalSteps))throw new PlanLedgerError("plan metadata total_steps must be an integer");if(totalSteps<previousEnd)throw new PlanLedgerError(`plan metadata total_steps ${totalSteps} is below the highest declared step ${previousEnd}`);let declaredClasses=value.provenance_classes,derivedClasses=[];for(let part of parts)derivedClasses.includes(part.provenance_class)||derivedClasses.push(part.provenance_class);if(Array.isArray(declaredClasses)){for(let declared of declaredClasses)if(!derivedClasses.includes(declared))throw new PlanLedgerError(`plan metadata declares provenance class '${String(declared)}' that no part produces`)}return{version:1,parts,provenance_classes:derivedClasses,total_steps:totalSteps}}function resolveOwnedSteps(metadata,phase){if(!PLAN_PHASES.includes(phase))throw new PlanLedgerError(`unknown phase '${phase}'`);let owned=[];for(let part of metadata.parts)if(PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)for(let step=part.start_step;step<=part.end_step;step+=1)owned.push(step);return owned}function resolveOwnedParts(metadata,phase){return metadata.parts.filter(part=>PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)}function assertStepClassCoverage(metadata,ownership=PLAN_CLASS_OWNERSHIP){let unowned=[];for(let part of metadata.parts){let owner=ownership[part.provenance_class];if(owner===void 0){if(part.declared_advisory)continue;unowned.push(`class '${part.provenance_class}' (part '${part.part_id}', steps ${part.start_step}-${part.end_step}) has no executing phase and is not declared advisory`);continue}PLAN_PHASES.includes(owner)||unowned.push(`class '${part.provenance_class}' is mapped to unknown phase '${String(owner)}'`)}if(unowned.length>0)throw new PlanLedgerError(`plan step-class coverage failed \u2014 every class a planner can emit must be executed by some phase or declared advisory in the plan:
5653
+ `)),0)}async function runConductorCli(argv){let parsed=parseConductorArgs(argv);if(parsed.kind==="help")return console.log(getConductorUsage()),0;if(parsed.kind==="error")return console.error(`Error: ${parsed.message}`),1;try{switch(parsed.command){case"emit-event":return await runEmitEventCommand(parsed.argv);case"supervise":return await runSuperviseCommand(parsed.argv);case"epic-tick":return await runEpicTickCommand(parsed.argv);case"approve-plan":return await runApprovePlanCommand(parsed.argv);case"epic-status":return await runEpicStatusCommand(parsed.argv);case"send-message":return await runSendMessageCommand(parsed.argv);case"check-messages":return await runCheckMessagesCommand(parsed.argv);case"doctor":return await runDoctorCommand(parsed.argv);case"purge":return await runPurgeCommand(parsed.argv);case"install-git-hooks":return runInstallGitHooksCommand(parsed.argv);case"git-hook":return await runGitHookCommand(parsed.argv);case"file-scope-guard":return runFileScopeGuardCli();default:return console.error('Error: Unknown command. Run "conductor --help" for usage.'),1}}catch(error){let envelope=toConductorErrorEnvelope(error);return console.error(`Error: ${formatConductorErrorLine(envelope)}`),envelope.status>=500?2:1}}init_pr_ci_producer();import{generateDecisionPageHtml}from"./decision-page-template.js";import{z as z17}from"zod";var ActionableItemSchema=z17.object({id:z17.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores"),question:z17.string().min(1),original_question:z17.string().optional().describe("Optional display-only field: the clarifying question or critique point as originally raised; soft cap ~30 words. Omit it (or pass an empty string) for non-review callers \u2014 the renderer omits the section when it is absent or blank."),why_it_matters:z17.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),recommendation_explanation:z17.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),codebase_evidence:z17.string().optional().describe("Optional display-only field: combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block, which is omitted when this field is absent or blank."),source:z17.string().optional().describe(`Optional source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'. When absent the rendered card emits data-source="".`),recommendation_index:z17.number().int().min(0).describe("0-based index of the recommended option in the options array"),options:z17.array(z17.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),option_consequences:z17.array(z17.string().min(1)).min(2).max(4).describe("Behavioral consequence per branch, parallel to options. Must have 2\u20134 entries; length must equal options.length.")}).superRefine((item,ctx)=>{item.option_consequences.length!==item.options.length&&ctx.addIssue({code:z17.ZodIssueCode.custom,path:["option_consequences"],message:`option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`}),item.recommendation_index>=item.options.length&&ctx.addIssue({code:z17.ZodIssueCode.custom,path:["recommendation_index"],message:`recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`})}),DecisionPageLabelsSchema=z17.object({title:z17.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),intro:z17.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),section_heading:z17.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),improvements_heading:z17.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')}),SystemGoalNfrSchema=z17.object({category:z17.string().min(1).describe("Canonical NFR category, e.g. security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility."),requirement:z17.string().min(1).describe("The non-functional requirement itself."),implication:z17.string().min(1).describe("What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."),status:z17.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved (also surface as an actionable_items card).")}),AcceptanceCriterionSchema=z17.object({id:z17.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores").describe("Stable per-criterion id, e.g. AC-1. Becomes the captured-feedback JSON key."),criterion:z17.string().min(1).describe("What the system must do, stated concretely enough to be verified."),verification:z17.string().min(1).describe("How we would confirm this criterion is met. Required \u2014 a criterion with no way to check it is not yet a criterion."),status:z17.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved.")}),SystemGoalsSchema=z17.object({business_goal:z17.string().min(1).describe("The business goal this work serves."),desired_end_state:z17.string().min(1).describe("The end-state the system should reach."),system_behavior:z17.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),acceptance_criteria:z17.array(AcceptanceCriterionSchema).optional().default([]).describe("What the system must do, as verifiable criteria. Implementation options should be derived from these rather than the reverse."),nfrs:z17.array(SystemGoalNfrSchema).optional().default([])}),ImplementationOrderItemSchema=z17.object({title:z17.string().min(1).describe("Short title of the slice / child ticket."),depends_on:z17.array(z17.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),recommended_after:z17.array(z17.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),rationale:z17.string().min(1).describe("Why this slice sits at this point in the order.")}),DecisionPageInputShape={ticket_key:z17.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z17.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) is the ticket-review decision-capture page and is unaffected by the planning fields. "pre_ticket_planning" additionally renders the read-only system_goals and implementation_order sections for pre-ticket epic/task framing.'),system_goals:SystemGoalsSchema.optional().describe("pre_ticket_planning only: business goal, desired end-state, system behavior, acceptance criteria (what the system must do), and classified NFRs (the standards it must meet). Acceptance criteria and NFRs render with per-item stance controls. Unresolved (open) NFRs should ALSO be passed as actionable_items so the human can decide them."),implementation_order:z17.array(ImplementationOrderItemSchema).optional().describe("pre_ticket_planning epic surfaces only: read-only recommended implementation order (hard depends_on vs soft recommended_after). No Jira links are created from this."),output_subdir:z17.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). Validated strictly: no absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z17.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html and contain no path separators; the .html suffix is required and never auto-appended.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading). Presentation-only; does not change data-testid hooks or the submitted JSON shape."),actionable_items:z17.array(ActionableItemSchema).optional().default([]).describe("Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."),clear_improvements:z17.array(z17.object({id:z17.string().min(1).describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),title:z17.string().min(1),action:z17.string().min(1),confidence:z17.string().min(1),source:z17.string().min(1).describe("Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user \u2014 the confirmed-improvements list shows title/confidence/action only.")})).optional().default([]).describe("Confirmed improvements displayed as informational list, not submitted.")},DecisionPageInputSchema=z17.object(DecisionPageInputShape),DecisionPageLeanInputShape={ticket_key:z17.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z17.enum(["review_decisions","pre_ticket_planning"]).optional().default("review_decisions").describe('Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'),output_subdir:z17.string().optional().default("review").describe('Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),output_filename:z17.string().optional().describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'),labels:DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."),content:z17.record(z17.string(),z17.unknown()).optional().describe("Contains deferred heavy payloads like actionable_items or system_goals.")};import{writeFile as writeFile12,mkdir as mkdir12}from"fs/promises";import path45 from"path";function slugify(text2,maxLength=60){return text2.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-").replace(/-+/g,"-").slice(0,maxLength).replace(/-$/,"")}function sanitizeProviderForFilename(provider){return provider.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")||"provider"}function buildBrainstormResultFilename(envelope,row,subject){let providerSegment=sanitizeProviderForFilename(row.provider),subjectSlug=subject?slugify(subject):"",shortId=envelope.brainstorm_id.slice(0,8);return subjectSlug?`${subjectSlug}-${shortId}-${providerSegment}.md`:`${envelope.brainstorm_id}-${providerSegment}.md`}async function saveBrainstormResultsToDir(envelope,dir,subject){let savedPaths=[];for(let row of envelope.results){let markdown=row.markdown;if(!markdown)continue;let filename=buildBrainstormResultFilename(envelope,row,subject),filePath=path45.join(dir,filename);try{await mkdir12(dir,{recursive:!0}),await writeFile12(filePath,markdown,"utf-8"),savedPaths.push(filePath)}catch{}}return savedPaths}import{createHash as createHash7}from"node:crypto";var PLAN_PROVENANCE_CLASSES=["implementation","documentation","unit_tests","e2e_tests","rendered_ui_review","test_gap_review","final_plan_review"],PLAN_PHASES=["produce","pre_pr_verification","post_pr_gap_close"],PLAN_STEP_DISPOSITIONS=["executed","adapted","escalated","unrun-advisory"],MECHANICAL_ADAPTATION_KINDS=["locator-correction","repository-command-correction","equivalent-implementation-recognized"],ESCALATION_ONLY_CATEGORIES=["design","schema","public-api","dependencies","security"],PLAN_CLASS_OWNERSHIP=Object.freeze({implementation:"produce",documentation:"produce",unit_tests:"pre_pr_verification",e2e_tests:"pre_pr_verification",rendered_ui_review:"pre_pr_verification",test_gap_review:"pre_pr_verification",final_plan_review:"pre_pr_verification"}),PRE_PR_VERIFICATION_LIMITS=Object.freeze({maxCorrectionTurns:3,maxChangedFiles:40,maxDiffLines:2e3}),RENDERED_UI_MAX_CYCLES=3,PlanLedgerError=class extends Error{constructor(message){super(message),this.name="PlanLedgerError"}};function isPlainObject11(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isInteger(value)&&value>=1}function validatePlanMetadata(value){if(value==null)throw new PlanLedgerError("plan metadata is absent; routing requires provenance and must not be inferred from plan prose");if(!isPlainObject11(value))throw new PlanLedgerError(`plan metadata must be an object, received ${Array.isArray(value)?"array":typeof value}`);if(value.version!==1)throw new PlanLedgerError(`unsupported plan metadata version ${String(value.version)}; expected 1`);let rawParts=value.parts;if(!Array.isArray(rawParts)||rawParts.length===0)throw new PlanLedgerError("plan metadata must carry a non-empty parts array");let parts=[],seenIds=new Set,previousEnd=0,previousId="";for(let[index,raw]of rawParts.entries()){if(!isPlainObject11(raw))throw new PlanLedgerError(`plan metadata part at index ${index} is not an object`);let partId=raw.part_id;if(typeof partId!="string"||partId.trim()==="")throw new PlanLedgerError(`plan metadata part at index ${index} has an empty part_id`);if(seenIds.has(partId))throw new PlanLedgerError(`duplicate plan metadata part_id '${partId}'`);seenIds.add(partId);let producer=raw.producer;if(typeof producer!="string"||producer.trim()==="")throw new PlanLedgerError(`plan metadata part '${partId}' has an empty producer`);let provenanceClass=raw.provenance_class;if(typeof provenanceClass!="string"||!PLAN_PROVENANCE_CLASSES.includes(provenanceClass))throw new PlanLedgerError(`plan metadata part '${partId}' has unknown provenance class '${String(provenanceClass)}'`);let startStep=raw.start_step,endStep=raw.end_step;if(!isPositiveInteger2(startStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive start_step ${String(startStep)}`);if(!isPositiveInteger2(endStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive end_step ${String(endStep)}`);if(endStep<startStep)throw new PlanLedgerError(`plan metadata part '${partId}' has a reversed range ${startStep}-${endStep}`);if(startStep<=previousEnd)throw new PlanLedgerError(`plan metadata part '${partId}' range ${startStep}-${endStep} overlaps or precedes '${previousId}' ending at ${previousEnd}; ranges must be disjoint and ascending`);if(startStep>previousEnd+1)throw new PlanLedgerError(`plan metadata part '${partId}' starts at step ${startStep} but '${previousId}' ended at ${previousEnd}; steps ${previousEnd+1}-${startStep-1} are claimed by no part and would be executed by no phase`);previousEnd=endStep,previousId=partId,parts.push({part_id:partId,producer,provenance_class:provenanceClass,start_step:startStep,end_step:endStep,declared_advisory:raw.declared_advisory===!0})}let totalSteps=value.total_steps;if(typeof totalSteps!="number"||!Number.isInteger(totalSteps))throw new PlanLedgerError("plan metadata total_steps must be an integer");if(totalSteps<previousEnd)throw new PlanLedgerError(`plan metadata total_steps ${totalSteps} is below the highest declared step ${previousEnd}`);let declaredClasses=value.provenance_classes,derivedClasses=[];for(let part of parts)derivedClasses.includes(part.provenance_class)||derivedClasses.push(part.provenance_class);if(Array.isArray(declaredClasses)){for(let declared of declaredClasses)if(!derivedClasses.includes(declared))throw new PlanLedgerError(`plan metadata declares provenance class '${String(declared)}' that no part produces`)}return{version:1,parts,provenance_classes:derivedClasses,total_steps:totalSteps}}function resolveOwnedSteps(metadata,phase){if(!PLAN_PHASES.includes(phase))throw new PlanLedgerError(`unknown phase '${phase}'`);let owned=[];for(let part of metadata.parts)if(PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)for(let step=part.start_step;step<=part.end_step;step+=1)owned.push(step);return owned}function resolveOwnedParts(metadata,phase){return metadata.parts.filter(part=>PLAN_CLASS_OWNERSHIP[part.provenance_class]===phase)}function assertStepClassCoverage(metadata,ownership=PLAN_CLASS_OWNERSHIP){let unowned=[];for(let part of metadata.parts){let owner=ownership[part.provenance_class];if(owner===void 0){if(part.declared_advisory)continue;unowned.push(`class '${part.provenance_class}' (part '${part.part_id}', steps ${part.start_step}-${part.end_step}) has no executing phase and is not declared advisory`);continue}PLAN_PHASES.includes(owner)||unowned.push(`class '${part.provenance_class}' is mapped to unknown phase '${String(owner)}'`)}if(unowned.length>0)throw new PlanLedgerError(`plan step-class coverage failed \u2014 every class a planner can emit must be executed by some phase or declared advisory in the plan:
5654
5654
  ${unowned.join(`
5655
- `)}`)}function validateRecordShape(record,metadata){if(!isPositiveInteger2(record.stepId))throw new PlanLedgerError(`ledger record has a non-positive stepId ${String(record.stepId)}`);if(!PLAN_STEP_DISPOSITIONS.includes(record.disposition))throw new PlanLedgerError(`step ${record.stepId} has unsupported disposition '${String(record.disposition)}'; expected one of ${PLAN_STEP_DISPOSITIONS.join(", ")}`);if(record.disposition==="adapted"){let adaptation=record.adaptation;if(!adaptation)throw new PlanLedgerError(`step ${record.stepId} is 'adapted' but carries no adaptation record`);if(!MECHANICAL_ADAPTATION_KINDS.includes(adaptation.kind))throw new PlanLedgerError(`step ${record.stepId} uses unapproved adaptation kind '${String(adaptation.kind)}'; only ${MECHANICAL_ADAPTATION_KINDS.join(", ")} are mechanical \u2014 anything touching ${ESCALATION_ONLY_CATEGORIES.join(", ")} must escalate`);for(let field of["originalPremise","correction","rationale"]){let value=adaptation[field];if(typeof value!="string"||value.trim()==="")throw new PlanLedgerError(`step ${record.stepId} adaptation is missing required evidence '${field}'`)}}else if(record.adaptation)throw new PlanLedgerError(`step ${record.stepId} carries an adaptation record but its disposition is '${record.disposition}'`);if(record.disposition==="escalated"&&(typeof record.escalationReason!="string"||record.escalationReason.trim()===""))throw new PlanLedgerError(`step ${record.stepId} is 'escalated' but gives no blocking reason; an escalation must never read as completion`);if(record.disposition==="unrun-advisory"){let owningPart=metadata.parts.find(part=>record.stepId>=part.start_step&&record.stepId<=part.end_step);if(!owningPart||!owningPart.declared_advisory)throw new PlanLedgerError(`step ${record.stepId} claims 'unrun-advisory' but its plan part is not declared advisory; advisory status must be declared in the plan, never asserted by the phase that could not run it`);if(typeof record.advisoryExplanation!="string"||record.advisoryExplanation.trim()==="")throw new PlanLedgerError(`step ${record.stepId} is 'unrun-advisory' but gives no explanation of what was not run`)}if(record.renderedUi){let{cycles,cycleCount}=record.renderedUi;if(!Array.isArray(cycles)||cycles.length===0)throw new PlanLedgerError(`step ${record.stepId} reports rendered-UI work with no cycle rubric; the per-cycle table is the evidence that the loop actually ran`);if(!isPositiveInteger2(cycleCount)||cycleCount>RENDERED_UI_MAX_CYCLES)throw new PlanLedgerError(`step ${record.stepId} reports ${String(cycleCount)} rendered-UI cycles; the plan's cap is ${RENDERED_UI_MAX_CYCLES}`);for(let cycle of cycles){if(!Array.isArray(cycle.rubric)||cycle.rubric.length===0)throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} has an empty rubric`);if(!Array.isArray(cycle.acceptedFixes))throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} is missing its acceptedFixes list`)}}}function validatePhaseResult(result,metadata,phase){if(!result||typeof result!="object")throw new PlanLedgerError("phase result is missing");if(result.version!==1)throw new PlanLedgerError(`unsupported phase result version ${String(result.version)}; expected 1`);if(result.phase!==phase)throw new PlanLedgerError(`phase result declares phase '${String(result.phase)}' but was produced for '${phase}'`);if(!Array.isArray(result.records))throw new PlanLedgerError(`phase '${phase}' result has no records array`);let owned=new Set(resolveOwnedSteps(metadata,phase)),settled=new Set;for(let record of result.records){if(validateRecordShape(record,metadata),!owned.has(record.stepId))throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId}, which it does not own`);if(settled.has(record.stepId))throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId} more than once`);settled.add(record.stepId)}let missing=[...owned].filter(step=>!settled.has(step)).sort((a,b)=>a-b);if(missing.length>0)throw new PlanLedgerError(`phase '${phase}' left owned steps unsettled: ${missing.join(", ")}. Every owned step must end as executed, adapted, escalated, or declared-advisory \u2014 there is no silent skip.`);return result.records.map(record=>({...record,phase}))}var SETTLED_DISPOSITIONS=new Set(["executed","adapted"]);function mergePlanStepRecords(existing,incoming){let merged=[...existing];for(let record of incoming){let priorIndex=[...merged].map((entry,index)=>({entry,index})).filter(({entry})=>entry.stepId===record.stepId).pop()?.index;if(priorIndex!==void 0){let prior=merged[priorIndex],priorWasSettled=SETTLED_DISPOSITIONS.has(prior.disposition),wouldRegress=priorWasSettled&&!SETTLED_DISPOSITIONS.has(record.disposition),isRerun=priorWasSettled&&SETTLED_DISPOSITIONS.has(record.disposition);if((wouldRegress||isRerun)&&!record.invalidatedBy)throw new PlanLedgerError(`step ${record.stepId} is already recorded as '${prior.disposition}'; re-recording it as '${record.disposition}' requires an explicit invalidatedBy note stating what invalidated the prior evidence`)}merged.push({...record})}return merged}var REDACTED3="[REDACTED]",SECRET_PATTERNS2=[[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi,`$1 ${REDACTED3}`],[/\b(authorization)\s*[:=]\s*\S+/gi,`$1: ${REDACTED3}`],[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g,REDACTED3],[/\bgh[pousr]_[A-Za-z0-9]{16,}/g,REDACTED3],[/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,`$1=${REDACTED3}`],[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi,`$1${REDACTED3}@`]];function redactSecrets3(value){let output=value;for(let[pattern,replacement]of SECRET_PATTERNS2)output=output.replace(pattern,replacement);return output}function redactDeep(value){if(typeof value=="string")return redactSecrets3(value);if(Array.isArray(value))return value.map(entry=>redactDeep(entry));if(isPlainObject10(value)){let output={};for(let[key,entry]of Object.entries(value))output[key]=redactDeep(entry);return output}return value}function sanitizePlanStepRecord(record){return redactDeep(record)}function sanitizePlanStepRecords(records){return records.map(record=>sanitizePlanStepRecord(record))}var ROUTED_PHASE_INSTRUCTIONS=Object.freeze({"execute-plan.md":"produce","execute-plan-verification.md":"pre_pr_verification","verify-plan.md":"post_pr_gap_close"});function phaseForInstructionFile(instructionFile){if(instructionFile)return ROUTED_PHASE_INSTRUCTIONS[instructionFile]}function extractPlanMetadata(result){let candidate=findPlanMetadataCandidate(result,0);if(candidate!==void 0)return validatePlanMetadata(candidate)}function findPlanMetadataCandidate(value,depth){if(!(depth>6||value===null||value===void 0)){if(typeof value=="string"){let trimmed=value.trim();if(!trimmed.startsWith("{")&&!trimmed.includes("plan_metadata"))return;try{return findPlanMetadataCandidate(JSON.parse(trimmed),depth+1)}catch{return}}if(Array.isArray(value)){for(let entry of value){let found=findPlanMetadataCandidate(entry,depth+1);if(found!==void 0)return found}return}if(typeof value=="object"){let record=value;if(record.plan_metadata!==void 0&&record.plan_metadata!==null)return record.plan_metadata;if(record.version!==void 0&&Array.isArray(record.parts))return record;for(let entry of Object.values(record)){let found=findPlanMetadataCandidate(entry,depth+1);if(found!==void 0)return found}}}}function buildPhaseRoutingContext(metadata,phase,ledger,checkpoint){return{phase,ownedSteps:resolveOwnedSteps(metadata,phase),ownedRanges:resolveOwnedParts(metadata,phase).map(part=>({partId:part.part_id,provenanceClass:part.provenance_class,startStep:part.start_step,endStep:part.end_step,declaredAdvisory:part.declared_advisory})),ledger:sanitizePlanStepRecords(ledger),...checkpoint?{checkpoint}:{}}}function injectPhaseRoutingContext(instruction,context){let block=JSON.stringify(context,null,2);return`${instruction}
5655
+ `)}`)}function validateRecordShape(record,metadata){if(!isPositiveInteger2(record.stepId))throw new PlanLedgerError(`ledger record has a non-positive stepId ${String(record.stepId)}`);if(!PLAN_STEP_DISPOSITIONS.includes(record.disposition))throw new PlanLedgerError(`step ${record.stepId} has unsupported disposition '${String(record.disposition)}'; expected one of ${PLAN_STEP_DISPOSITIONS.join(", ")}`);if(record.disposition==="adapted"){let adaptation=record.adaptation;if(!adaptation)throw new PlanLedgerError(`step ${record.stepId} is 'adapted' but carries no adaptation record`);if(!MECHANICAL_ADAPTATION_KINDS.includes(adaptation.kind))throw new PlanLedgerError(`step ${record.stepId} uses unapproved adaptation kind '${String(adaptation.kind)}'; only ${MECHANICAL_ADAPTATION_KINDS.join(", ")} are mechanical \u2014 anything touching ${ESCALATION_ONLY_CATEGORIES.join(", ")} must escalate`);for(let field of["originalPremise","correction","rationale"]){let value=adaptation[field];if(typeof value!="string"||value.trim()==="")throw new PlanLedgerError(`step ${record.stepId} adaptation is missing required evidence '${field}'`)}}else if(record.adaptation)throw new PlanLedgerError(`step ${record.stepId} carries an adaptation record but its disposition is '${record.disposition}'`);if(record.disposition==="escalated"&&(typeof record.escalationReason!="string"||record.escalationReason.trim()===""))throw new PlanLedgerError(`step ${record.stepId} is 'escalated' but gives no blocking reason; an escalation must never read as completion`);if(record.disposition==="unrun-advisory"){let owningPart=metadata.parts.find(part=>record.stepId>=part.start_step&&record.stepId<=part.end_step);if(!owningPart||!owningPart.declared_advisory)throw new PlanLedgerError(`step ${record.stepId} claims 'unrun-advisory' but its plan part is not declared advisory; advisory status must be declared in the plan, never asserted by the phase that could not run it`);if(typeof record.advisoryExplanation!="string"||record.advisoryExplanation.trim()==="")throw new PlanLedgerError(`step ${record.stepId} is 'unrun-advisory' but gives no explanation of what was not run`)}if(record.renderedUi){let{cycles,cycleCount}=record.renderedUi;if(!Array.isArray(cycles)||cycles.length===0)throw new PlanLedgerError(`step ${record.stepId} reports rendered-UI work with no cycle rubric; the per-cycle table is the evidence that the loop actually ran`);if(!isPositiveInteger2(cycleCount)||cycleCount>RENDERED_UI_MAX_CYCLES)throw new PlanLedgerError(`step ${record.stepId} reports ${String(cycleCount)} rendered-UI cycles; the plan's cap is ${RENDERED_UI_MAX_CYCLES}`);for(let cycle of cycles){if(!Array.isArray(cycle.rubric)||cycle.rubric.length===0)throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} has an empty rubric`);if(!Array.isArray(cycle.acceptedFixes))throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} is missing its acceptedFixes list`)}}}function validatePhaseResult(result,metadata,phase){if(!result||typeof result!="object")throw new PlanLedgerError("phase result is missing");if(result.version!==1)throw new PlanLedgerError(`unsupported phase result version ${String(result.version)}; expected 1`);if(result.phase!==phase)throw new PlanLedgerError(`phase result declares phase '${String(result.phase)}' but was produced for '${phase}'`);if(!Array.isArray(result.records))throw new PlanLedgerError(`phase '${phase}' result has no records array`);let owned=new Set(resolveOwnedSteps(metadata,phase)),settled=new Set;for(let record of result.records){if(validateRecordShape(record,metadata),!owned.has(record.stepId))throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId}, which it does not own`);if(settled.has(record.stepId))throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId} more than once`);settled.add(record.stepId)}let missing=[...owned].filter(step=>!settled.has(step)).sort((a,b)=>a-b);if(missing.length>0)throw new PlanLedgerError(`phase '${phase}' left owned steps unsettled: ${missing.join(", ")}. Every owned step must end as executed, adapted, escalated, or declared-advisory \u2014 there is no silent skip.`);return result.records.map(record=>({...record,phase}))}var SETTLED_DISPOSITIONS=new Set(["executed","adapted"]);function mergePlanStepRecords(existing,incoming){let merged=[...existing];for(let record of incoming){let priorIndex=[...merged].map((entry,index)=>({entry,index})).filter(({entry})=>entry.stepId===record.stepId).pop()?.index;if(priorIndex!==void 0){let prior=merged[priorIndex],priorWasSettled=SETTLED_DISPOSITIONS.has(prior.disposition),wouldRegress=priorWasSettled&&!SETTLED_DISPOSITIONS.has(record.disposition),isRerun=priorWasSettled&&SETTLED_DISPOSITIONS.has(record.disposition);if((wouldRegress||isRerun)&&!record.invalidatedBy)throw new PlanLedgerError(`step ${record.stepId} is already recorded as '${prior.disposition}'; re-recording it as '${record.disposition}' requires an explicit invalidatedBy note stating what invalidated the prior evidence`)}merged.push({...record})}return merged}var REDACTED3="[REDACTED]",SECRET_PATTERNS2=[[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi,`$1 ${REDACTED3}`],[/\b(authorization)\s*[:=]\s*\S+/gi,`$1: ${REDACTED3}`],[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g,REDACTED3],[/\bgh[pousr]_[A-Za-z0-9]{16,}/g,REDACTED3],[/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,`$1=${REDACTED3}`],[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi,`$1${REDACTED3}@`]];function redactSecrets3(value){let output=value;for(let[pattern,replacement]of SECRET_PATTERNS2)output=output.replace(pattern,replacement);return output}function redactDeep(value){if(typeof value=="string")return redactSecrets3(value);if(Array.isArray(value))return value.map(entry=>redactDeep(entry));if(isPlainObject11(value)){let output={};for(let[key,entry]of Object.entries(value))output[key]=redactDeep(entry);return output}return value}function sanitizePlanStepRecord(record){return redactDeep(record)}function sanitizePlanStepRecords(records){return records.map(record=>sanitizePlanStepRecord(record))}var ROUTED_PHASE_INSTRUCTIONS=Object.freeze({"execute-plan.md":"produce","execute-plan-verification.md":"pre_pr_verification","verify-plan.md":"post_pr_gap_close"});function phaseForInstructionFile(instructionFile){if(instructionFile)return ROUTED_PHASE_INSTRUCTIONS[instructionFile]}function extractPlanMetadata(result){let candidate=findPlanMetadataCandidate(result,0);if(candidate!==void 0)return validatePlanMetadata(candidate)}function findPlanMetadataCandidate(value,depth){if(!(depth>6||value===null||value===void 0)){if(typeof value=="string"){let trimmed=value.trim();if(!trimmed.startsWith("{")&&!trimmed.includes("plan_metadata"))return;try{return findPlanMetadataCandidate(JSON.parse(trimmed),depth+1)}catch{return}}if(Array.isArray(value)){for(let entry of value){let found=findPlanMetadataCandidate(entry,depth+1);if(found!==void 0)return found}return}if(typeof value=="object"){let record=value;if(record.plan_metadata!==void 0&&record.plan_metadata!==null)return record.plan_metadata;if(record.version!==void 0&&Array.isArray(record.parts))return record;for(let entry of Object.values(record)){let found=findPlanMetadataCandidate(entry,depth+1);if(found!==void 0)return found}}}}function buildPhaseRoutingContext(metadata,phase,ledger,checkpoint){return{phase,ownedSteps:resolveOwnedSteps(metadata,phase),ownedRanges:resolveOwnedParts(metadata,phase).map(part=>({partId:part.part_id,provenanceClass:part.provenance_class,startStep:part.start_step,endStep:part.end_step,declaredAdvisory:part.declared_advisory})),ledger:sanitizePlanStepRecords(ledger),...checkpoint?{checkpoint}:{}}}function injectPhaseRoutingContext(instruction,context){let block=JSON.stringify(context,null,2);return`${instruction}
5656
5656
 
5657
5657
  ---
5658
5658
 
@@ -5733,4 +5733,4 @@ ${content.slice(0,MAX_INLINE_TEXT_LENGTH)}
5733
5733
  ${content}`),{content:[{type:"text",text:resultText}]}}case"list":{let{ticket_number,include_ai_generated}=args,params={repo_name:REPO_NAME};include_ai_generated&&(params.include_ai_generated="true");let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`,params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text2=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text2=await truncateAndSaveIfNeeded2(text2,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text:text2}]}}case"delete":{let{ticket_number,file_name}=args,url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`,{repo_name:REPO_NAME,file_name}),resp=await fetch(url,{method:"DELETE",headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("request_plan_generation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request an implementation plan for a ticket. A non-forced request against a fresh existing plan is reused \u2014 returns 202 with reused: true, no new work. Otherwise starts an async background job; results are NOT immediate, typically 5-12 minutes (well under the 900s give-up). get_plan retrieves the result later, or set wait_for_result to block and return it directly. Returns 202 if accepted, 404 if the ticket does not exist in Jira, 403 if unauthorized. Set force to true only to deliberately regenerate an existing plan \u2014 never for polling or timeout recovery.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,force:z18.boolean().optional().describe("Deliberately regenerate the plan even when a fresh one is stored. Omit for normal recovery-safe requests. Default: false.")}},async args=>requestTicketArtifact("plan",args));var directToolDeps={repoName:REPO_NAME,buildUrl,buildGetUrl,getPostHeaders,getHeaders:getGetHeaders,handleResponse};registerTool("request_ticket_update",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async AI rewrite of a ticket's description from its own content and reference material. Returns 202 immediately with an opaque reference; poll get_ticket_state for the outcome. A rewrite changing over 60% of the description is HELD, not applied \u2014 get_ticket_update_review returns the proposal. Works on both Jira and local ticket backends.",inputSchema:{ticket_number:commonFields.ticket_number}},async args=>runRequestTicketUpdate(args,directToolDeps));registerTool("request_estimate",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async development estimation for ONE ticket (use estimate_epic for an epic or key group). Returns 202 immediately; poll get_ticket_state for the outcome and stored estimate. recreate=true regenerates instead of reusing a stored estimate. Works on Jira and local backends; in local mode the estimate is stored, not posted to a tracker.",inputSchema:{ticket_number:commonFields.ticket_number,recreate:z18.boolean().optional().describe("Regenerate rather than reuse a stored estimate. Default: false.")}},async args=>runRequestEstimate(args,directToolDeps));registerTool("get_ticket_update_review",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the description rewrite that request_ticket_update HELD for review (over the 60% change threshold). Returns the proposed markdown so you can review and apply it yourself. Returns 404 when no held proposal exists \u2014 that means nothing was held, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number}},async args=>runGetTicketUpdateReview(args,directToolDeps));registerTool("estimate_epic",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Estimate a Jira Epic or ticket-key group. Exactly one of epic_key/ticket_keys (never both). allow_partial permits a partial result on child failures (default: fail-closed).",inputSchema:{epic_key:z18.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z18.array(z18.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z18.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("request_architecture",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of an architecture plan for a ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_architecture tool retrieves the generated architecture plan later (call get_architecture with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("architecture",args));registerTool("request_prd",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a Product Requirements Document (PRD) for a ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-4 minutes depending on ticket complexity. The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("prd",args));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd, fsd, or prd) for a ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z18.enum(["tdd","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:z18.string().optional().describe("Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."),provider:z18.string().optional().describe("Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence.")}},async args=>requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("get_doc",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated design document for a ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z18.enum(["tdd","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),args));registerTool("request_clarifying_questions",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of clarifying questions or debugging guidance for a ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_clarifying_questions tool retrieves the generated questions later (call get_clarifying_questions with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns them directly. For bug tickets, the result may be debugging guidance instead of clarifying questions. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("clarifying_questions",args));registerTool("get_ticket_critique",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated ticket quality critique for a ticket. This tool only fetches an existing critique \u2014 it does NOT start or trigger generation. If no critique exists yet (or you need a fresh one), call `request_ticket_critique` first; it starts the async generation and this `get_ticket_critique` tool retrieves the result. Returns markdown text with a structured critique covering Standards Conformance Analysis, Standards Deviations, and Suggested Improvements. Returns a 404 / not-found response when no critique is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("ticket_critique",args));registerTool("request_ticket_critique",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START (or refresh) async generation of a ticket critique for a ticket. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 1-5 minutes. The matching get_ticket_critique tool retrieves the generated critique later (call get_ticket_critique with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until the result is ready (typically 1-5 minutes) instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("ticket_critique",args));registerTool("request_ticket_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two requests sequentially. This triggers an asynchronous background job \u2014 results are NOT immediate. Processing typically takes 2-6 minutes. Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized. Set wait_for_result to true to block until both documents are ready and receive them concatenated.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider,rounds:z18.union([z18.literal(1),z18.literal(2),z18.literal("1"),z18.literal("2"),z18.literal("")]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing.")}},async args=>requestTicketReview(args));registerTool("request_reimplement_context",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"START async processing of new attachments and context assembly for a previously-implemented Jira ticket. Use this for follow-up requests on tickets that have already been through the plan+implement cycle. This triggers an asynchronous background job to process new attachments/images. The matching get_reimplement_context tool retrieves the assembled context later (call get_reimplement_context with the same ticket_number) \u2014 unless you set wait_for_result, in which case this call blocks and returns it directly. Set wait_for_result to true to block until the context is ready instead of returning immediately.",inputSchema:{ticket_number:commonFields.ticket_number,wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,second_opinion:commonFields.second_opinion,provider:commonFields.provider}},async args=>requestTicketArtifact("reimplement_context",args));registerTool("get_reimplement_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-assembled reimplement context for a ticket. This tool only fetches an existing result \u2014 it does NOT start or trigger processing. If the reimplement context does not exist yet (or you need a fresh one), call `request_reimplement_context` first; it starts the async processing and this `get_reimplement_context` tool retrieves the result. Returns a markdown document with new/changed information diffed against stored state, the original ticket description, and the existing implementation plan. Returns a 404 / not-found response when processing is not yet complete \u2014 that means processing has not finished, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("reimplement_context",args));registerTool("track_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit the ticket itself in the configured ticket backend: not the summary, description, comments, attachments, or status. Works for a ticket from either backend \u2014 it needs only the ticket key. Already tracked is a safe no-op: it upserts the description and repo_name without error. After create_ticket this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. To mutate the ticket use `update_ticket_description`, `add_comment` (requires Jira backend), or `update_jira_status`. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,description:z18.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")}},async({ticket_number,description})=>{let payload={repo_name:REPO_NAME};description!==void 0&&(payload.description=description);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/track`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_ticket_state",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z18.array(z18.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")}},async({ticket_number,fields})=>{let payload={repo_name:REPO_NAME,fields},resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). The ticket must be tracked via track_ticket first, or a 404 is returned. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/state`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_ticket_state_tree",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the repository's live ticket dependency tree: what each ticket is blocked on, what it blocks, and how far each work attempt got. Omit ticket_key for the whole repo, or supply one for its subtree. Read-only: it cannot assert state or declare a dependency.",inputSchema:{ticket_key:commonFields.ticket_number.optional().describe("Optional subtree selector; omit for the whole repository tree.")}},async({ticket_key})=>{let params={repo_name:REPO_NAME};ticket_key&&(params.ticket_key=ticket_key);let url=buildGetUrl("/ticket-state-tree",params),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_jira_transitions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List the workflow transitions available for a ticket in the configured ticket backend: each transition's id, name, and target status. Jira returns the project's workflow transitions; local reports its four ('To Do', 'In Progress', 'In Review', 'Done').",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-transitions`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("update_jira_status",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:`Transition a ticket to a target status in the configured ticket backend. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). Local accepts only 'To Do', 'In Progress', 'In Review', 'Done'. transition_id takes precedence over target_status. Pass target_status as "auto" for server-side LLM resolution of the correct post-PR status; if auto-resolve finds no match it returns status: skipped (not an error). Returns the from/to status, or an error listing available transitions if no match is found.`,inputSchema:{ticket_number:commonFields.ticket_number,target_status:z18.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),transition_id:z18.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")}},async({ticket_number,target_status,transition_id})=>{let payload={repo_name:REPO_NAME};target_status!==void 0&&(payload.target_status=target_status),transition_id!==void 0&&(payload.transition_id=transition_id);let resp=await fetch(buildUrl(`/tickets/${encodeURIComponent(ticket_number)}/jira-status`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("resolve_target_status",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",inputSchema:{ticket_number:commonFields.ticket_number,force_rerun:z18.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")}},async({ticket_number,force_rerun})=>{let payload={repo_name:REPO_NAME,ticket_number};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-target-status"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var BASE_BRANCH_CONFIG_FIELD="base_branch",VALID_CONFIG_FIELDS=["review_instructions","documentation_instructions","architecture_instructions","tdd_document_instructions","fsd_document_instructions","prd_document_instructions","unit_testing_instructions","e2e_testing_instructions","unit_testing_stack","e2e_testing_stack","frontend_correctness_standards","backend_correctness_standards","template_correctness_standards","style_correctness_standards","design_principles","post_pr_target_status","ci_check_config","ci_followup_config","allow_mutating_smoke_ops","selected_mcp_slugs",BASE_BRANCH_CONFIG_FIELD,"difficulty_model_routing_enabled","difficulty_model_tier_overrides","speed_vs_quality","sfcc_log_filter_rules","disable_sfcc_mcp_awareness","enable_regression_checks","ai_automation_level","ticket_backend_mode","jira_ticket_key","working_in","version_control_system","version","project_description","custom_directories","exclude_directories","exclude_file_extensions"].join(", "),TICKET_BACKEND_MODE_HINT='ticket_backend_mode is repository config ("jira" | "local"; absent/null = jira), never a ticket-tool argument.';registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list.",inputSchema:z18.discriminatedUnion("operation",[z18.object({operation:z18.literal("get"),field_name:z18.string().describe(`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. ${TICKET_BACKEND_MODE_HINT} Valid options: ${VALID_CONFIG_FIELDS}`)}).strict(),z18.object({operation:z18.literal("update"),field_name:z18.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. ${TICKET_BACKEND_MODE_HINT} Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`),value:z18.union([z18.string(),z18.boolean(),z18.array(z18.string()),z18.array(z18.record(z18.string(),z18.unknown())),z18.record(z18.string(),z18.union([z18.string(),z18.null()]))]).optional().describe(`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The sfcc_log_filter_rules field takes a JSON array of SFCC filter-rule objects, each shaped {type, match, value, priority} (e.g. [{"type": "exclude", "match": "keyword", "value": "favicon", "priority": 500}]) \u2014 pass an array of objects, not a string; an empty array clears the overlay. The backend validates each rule. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`),file_path:z18.string().optional().describe("Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."),only_if_null:z18.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL or still holds an unmodified Bridge-seeded default (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest.")}).strict(),z18.object({operation:z18.literal("list")}).strict()])},async args=>{switch(args.operation){case"get":{let{field_name}=args,url=buildGetUrl(`/config-field/${encodeURIComponent(field_name)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}case"update":{let{field_name,value,file_path,only_if_null}=args,withGuard=v=>only_if_null===!0?{repo_name:REPO_NAME,value:v,only_if_null:!0}:{repo_name:REPO_NAME,value:v};if(["selected_mcp_slugs"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of slug strings.`})}]};let arrayValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["difficulty_model_tier_overrides"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`})}]};let objectValue=value===void 0?{}:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(objectValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["sfcc_log_filter_rules"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a JSON array field; file_path updates are not supported. Pass value as an array of rule objects.`})}]};let arrayOfObjectsValue=value===void 0?[]:value,resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(arrayOfObjectsValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}if(["allow_mutating_smoke_ops","difficulty_model_routing_enabled"].includes(field_name)){if(file_path)return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`'${field_name}' is a scalar boolean field; file_path updates are not supported. Pass value: true or value: false.`})}]};let boolValue=!1;if(typeof value=="boolean")boolValue=value;else if(typeof value=="string"){let normalized=value.trim().toLowerCase();if(normalized==="true")boolValue=!0;else if(normalized==="false"||normalized==="")boolValue=!1;else return{isError:!0,content:[{type:"text",text:JSON.stringify({error:`Invalid value for '${field_name}': '${value}'. Expected true or false.`})}]}}let resp2=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(boolValue))});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let finalValue=null,note="";if(value||file_path){let resolved=await resolveTextOrFile(typeof value=="string"?value:void 0,file_path,"value");if(!resolved.ok)return resolved.errorResponse;finalValue=resolved.text,note=resolved.note}let resp=await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify(withGuard(finalValue))});return{content:[{type:"text",text:await handleResponse(resp)+note}]}}case"list":{let url=buildGetUrl("/config-fields",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}}default:return{content:[{type:"text",text:JSON.stringify({error:"Unknown operation"})}]}}});registerTool("get_my_role",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:'Check the role, auth source, and account type for the current API key. Returns JSON {role: "admin"|"member"|null, source: "user_access"|"legacy", customer_type: "b2b"|"b2c"}. Use it to check admin permissions before config_field "update" calls (non-admin user_access keys are blocked) and to gate b2b-only onboarding steps.',inputSchema:{}},async()=>{let url=buildGetUrl("/my-role",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("invite_member",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:'Invite a teammate to THIS configured project by minting a scoped user_access key. Admin-only (a member key or a key scoped to another repo is rejected server-side). repo_name is resolved from the session \u2014 do NOT pass it. Args: email (required), name?, role ("member"|"admin", default member). Returns {id, api_key}; the plaintext api_key is shown EXACTLY ONCE \u2014 relay it verbatim so the admin can distribute it out-of-band.',inputSchema:{email:z18.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z18.string().optional().describe("Optional display name for the teammate."),role:z18.enum(["member","admin"]).optional().default("member").describe('Role for the minted key. Defaults to "member"; pass "admin" only to deliberately opt this invite up to admin.')}},async({email,name,role})=>{try{let resp=await fetch(buildApiUrl("/setup/keys/mint"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,email,name,role:role??"member"})});return{content:[{type:"text",text:await handleResponse(resp)}]}}catch{return{content:[{type:"text",text:JSON.stringify({error:"SERVICE_UNAVAILABLE",status:503,message:"The connection to Bridge API failed before the invite could be submitted. Retry inviting the teammate."})}]}}});registerTool("get_install_manifest",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Read the easy-install configuration manifest for the configured repository in one call. Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent guidance, examples, and validation summary), a list of deferred fields (owned by /learn-repository or set deliberately), an integrations checklist (presence booleans only \u2014 credential values are never returned; direct humans to the setup UI, never transport secrets), a next_step pointer, done_criteria, a command_contract_version (compare it to the /install-bridge command's stated contract version to detect a stale scaffolded command copy), and a signed snapshot_token. Pass that exact snapshot_token to apply_install_manifest; tokens expire after 24 hours (re-read the manifest for a fresh one). Prefer this over many individual config_field reads during install bootstrap. The response also carries an additive, secret-free capability report: readiness dimensions configured / learned / indexed (indexed true|false|null \u2014 null means indeterminate, never read as indexed), plus tool_capabilities, the COMPLETE grouped catalog \u2014 ordered groups of tools, one per registered MCP tool, keyed by physical tool id, in server order, each with display_name, description, how_to_use, retrieval_of, profile, availability, availability_text, effect, missing, semantics and variants. Render availability_text verbatim; effect (BLOCK/DEGRADE) is INTERNAL \u2014 never display it. Render variants (e.g. create_doc's tdd/fsd/prd) beneath their physical tool, never as separate tools; a non-empty retrieval_of marks a retrieval half, never rendered on its own. profile names the MCP profile owning a registration, NOT whether it is active locally \u2014 Bridge cannot observe that. workflows lists curated non-MCP slash commands. concise_tool_capabilities projects both into two sections (available_now, needs_setup) of mixed tool/workflow items; render as given, never recompute. locked_tools / unlocked_tools are LEGACY policy-case arrays, NOT the complete tool inventory \u2014 use tool_capabilities. Clients never recompute catalog membership or dependency relationships. Read-only; registers nothing.",inputSchema:{save_locally:commonFields.save_locally}},async({save_locally})=>{let url=buildGetUrl("/config/install-manifest",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text2=await handleResponse(resp);if(ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text2);text2=text2+note}return{content:[{type:"text",text:text2}]}});registerTool("apply_install_manifest",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). Fields the manifest marks requires_confirmation (e.g. project_description, selected_mcp_slugs) MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. The apply is partial-tolerant: fields that fail validation (or are not bootstrap-eligible) land in the rejected bucket while the valid fields still commit \u2014 a rejected field is reported, not fatal, so do not retry the whole call for one rejection. HTTP 422 is reserved for snapshot-token problems (invalid, expired after 24h, or signed with a since-rotated API key): re-read the manifest and retry once with the fresh token.',inputSchema:{snapshot_token:z18.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z18.record(z18.string(),z18.any()).describe('Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.')}},async({snapshot_token,fields})=>{let resp=await fetch(buildUrl("/config/apply-install-manifest"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,snapshot_token,fields})}),text2=await handleResponse(resp);try{let wiResp=await fetch(buildGetUrl("/config-field/working_in",{repo_name:REPO_NAME}),{headers:await getGetHeaders()});if(wiResp.ok&&(await wiResp.json()).value==="Salesforce Commerce Cloud"){let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text2+=`
5734
5734
 
5735
5735
  \u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`)}}catch{}return{content:[{type:"text",text:text2}]}});registerTool("persist_routing_credential",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",inputSchema:{repo_name:z18.string().describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process.")}},async({repo_name})=>{let repoName=typeof repo_name=="string"?repo_name.trim():"",deps=buildCredentialStoreWriteDeps(),storePath=getPrimaryCredentialStorePath(deps);if(repoName.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,message:"Cannot persist routing credential: repo_name is required. Pass the repo name this install is configuring.",path:storePath})}]};let target=`bapi:${repoName}`,apiKey=await getResolvedApiKeyForRepo(repoName);if(apiKey.length===0)return{content:[{type:"text",text:JSON.stringify({ok:!1,target,path:storePath,message:`No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`})}]};let result=await upsertBapiCredential(repoName,apiKey,deps);return result.ok?{content:[{type:"text",text:JSON.stringify({ok:!0,action:result.action,target:result.target,path:result.path,migratedFallback:result.migratedFallback,message:`Stored routing credential for ${result.target} at ${result.path}.`})}]}:{content:[{type:"text",text:JSON.stringify({ok:!1,target:result.target,path:result.path,kind:result.kind,message:`Failed to persist routing credential for ${result.target}: ${result.error} You can rerun /install-bridge or migrate manually.`})}]}});function formatDeepResearchProviderReason(meta){if(!meta)return"";let parts=[],reason=meta.incomplete_details?.reason;reason&&parts.push(`provider reason: ${reason}`);let errMsg=meta.error?.message,errCode=meta.error?.code;return(errMsg||errCode)&&(errCode&&errMsg?parts.push(`provider error: ${errCode}: ${errMsg}`):errMsg?parts.push(`provider error: ${errMsg}`):errCode&&parts.push(`provider error: ${errCode}`)),parts.length?` (${parts.join("; ")})`:""}function _safeIsoMs(value){if(!value)return null;let ms=new Date(value).getTime();return Number.isNaN(ms)?null:ms}function formatDeepResearchElapsed(createdAt,lastPollAt){let createdMs=_safeIsoMs(createdAt);if(createdMs===null)return"";let now=Date.now(),startedMs=Math.max(0,now-createdMs),startedMin=Math.floor(startedMs/6e4),lastPollMs=_safeIsoMs(lastPollAt),pollSuffix="";return lastPollMs!==null&&(pollSuffix=`, last poll ${Math.max(0,Math.floor((now-lastPollMs)/1e3))}s ago`),` (running ${startedMin}m${pollSuffix})`}function formatDeepResearchFailure(body){let kind=body.error_kind||body.error_message||"Unknown error",reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`}function formatDeepResearchStatus(body,taskId){let elapsed=formatDeepResearchElapsed(body.created_at,body.last_poll_at),reason=formatDeepResearchProviderReason(body.provider_status_meta);return`Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`}registerTool("request_deep_research",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",inputSchema:{query:z18.string().describe("The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"),context:z18.string().optional().describe("Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"),ticket_number:commonFields.ticket_number.optional(),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally}},async({query,context,ticket_number,wait_for_result,save_locally})=>{let submitPayload={repo_name:REPO_NAME,query};context&&(submitPayload.context=context),ticket_number&&(submitPayload.ticket_number=ticket_number);let submitResp=await fetch(buildUrl("/deep-research"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let taskId=(await submitResp.json()).task_id;if(!wait_for_result)return{content:[{type:"text",text:`Deep research submitted (task_id: ${taskId}). Processing typically takes 2-10 minutes. Use get_deep_research with task_id ${taskId} to retrieve the result once processing completes.`}]};let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,lastStatus="queued",latestStatusBody=null;for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3);console.error(`Deep research in progress... (elapsed: ${elapsed}s, status: ${lastStatus})`);let statusUrl=buildGetUrl(`/deep-research/${taskId}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok){let errorText=await handleResponse(statusResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error polling deep research status: ${errorText}`})}]}}let statusBody=await statusResp.json();if(lastStatus=statusBody.status,latestStatusBody=statusBody,lastStatus==="completed")break;if(lastStatus==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}if(lastStatus!=="completed"){let statusSuffix=latestStatusBody?` ${formatDeepResearchStatus(latestStatusBody,taskId)}`:"";return{content:[{type:"text",text:`Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`}]}}let resultUrl=buildGetUrl(`/deep-research/${taskId}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok){let errorText=await handleResponse(resultResp);return{content:[{type:"text",text:JSON.stringify({error:"INTERNAL_ERROR",status:500,message:`Error retrieving deep research result: ${errorText}`})}]}}let resultText=await resultResp.text();if(save_locally){let slug=slugify(query),note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${taskId}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});registerTool("get_deep_research",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",inputSchema:{task_id:z18.number().describe("The task ID returned by request_deep_research."),query_slug:z18.string().optional().describe("Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."),save_locally:commonFields.save_locally}},async({task_id,query_slug,save_locally})=>{let statusUrl=buildGetUrl(`/deep-research/${task_id}/status`,{repo_name:REPO_NAME}),statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(statusBody.status==="failed")return{content:[{type:"text",text:formatDeepResearchFailure(statusBody)}]};if(statusBody.status!=="completed")return{content:[{type:"text",text:formatDeepResearchStatus(statusBody,task_id)}]};let resultUrl=buildGetUrl(`/deep-research/${task_id}/result`,{repo_name:REPO_NAME}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let resultText=await resultResp.text();if(save_locally){let slug=query_slug||"research",note=await saveLocally(await getDocsPath("deep-research"),`${slug}-${task_id}.md`,resultText);resultText+=note}return{content:[{type:"text",text:resultText}]}});var BRAINSTORM_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isBrainstormTerminalStatus(status){return BRAINSTORM_TERMINAL_STATUSES.has(status)}async function pollBrainstormUntilTerminal(brainstormId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"brainstorm_id",handleValue:brainstormId,recoveryGetUrl:buildGetUrl(`/brainstorms/${brainstormId}/result`,{repo_name:repoName}),retrievalToolName:"get_council"};for(;Date.now()-startTime<MAX_TIMEOUT_MS;){await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs));let elapsed=Math.round((Date.now()-startTime)/1e3),statusUrl=buildGetUrl(`/brainstorms/${brainstormId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${brainstormId} status poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation2=`Council ${brainstormId} stopped polling after ${MAX_CONSECUTIVE_POLL_FAILURES} consecutive connection failures.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation2,recovery)}}Date.now()-startTime>6e4&&(pollIntervalMs=3e4);continue}if(consecutiveFetchFailures=0,!statusResp.ok)return{kind:"status",envelope:latest};if(latest=await statusResp.json(),latest.rows.every(row=>isBrainstormTerminalStatus(row.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${brainstormId} timed out after ${Math.round(MAX_TIMEOUT_MS/1e3)} seconds. The task may still be processing on the server.`;return{kind:"giveup",text:formatRecoverablePollGiveUp(situation,recovery)}}async function saveBrainstormResultsLocally(envelope,subject){let dir=await getDocsPath("brainstorm");return saveBrainstormResultsToDir(envelope,dir,subject)}function formatBrainstormToolResponse(envelope,savedPaths){let lines=[];lines.push(`# Council ${envelope.brainstorm_id}`),lines.push(`Repo: ${envelope.repo_name}`),lines.push("");for(let row of envelope.results)lines.push(`## ${row.provider} \u2014 status: ${row.status}`),lines.push(`error_kind: ${row.error_kind??"null"}`),row.error_message&&lines.push(`error_message: ${row.error_message}`),row.markdown&&(lines.push(""),lines.push(row.markdown)),lines.push("");if(savedPaths.length>0){lines.push("---"),lines.push("Saved files:");for(let p of savedPaths)lines.push(`- ${p}`)}return lines.join(`
5736
- `)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start an async council that fans out a task to multiple opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_council to retrieve. Generates and persists a retrievable artifact.",inputSchema:{task_description:z18.string().describe("Free-form description of the task for the council to weigh in on. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z18.array(z18.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z18.string().optional().describe("Optional caller-supplied concerns to surface to the council agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_brainstorm_id:z18.string().optional().describe("Optional brainstorm_id (the id field returned by an earlier council) to refine. When provided, the prior council's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z18.enum(["technical","design","discovery","general"]).optional().describe("Preferred council-mode selector for new callers. 'technical' (default) is the implementation/architecture council; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped discovery questions for early/vague tasks. 'general' convenes the council from the supplied brief alone, with no indexed repository context required, unlike 'technical'/'discovery'. Takes precedence over the legacy boolean design field."),design:z18.boolean().optional().describe('Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'),lenses:z18.array(z18.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z18.boolean().optional().describe("Opt-in to trigger a second cross-examination debate round between providers (default off). When true, after round 1 completes each provider critiques the OTHER provider(s)' round-1 output, and the critique is appended to that provider's markdown under a '## Cross-examination' section.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_brainstorm_id,mode,design,lenses,debate})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_brainstorm_id&&(submitPayload.prior_brainstorm_request_id=prior_brainstorm_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let submitResp;try{submitResp=await fetch(buildUrl("/brainstorms"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request_council tool")}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (brainstorm_id: ${submitBody.brainstorm_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`}]};let pollOutcome=await pollBrainstormUntilTerminal(submitBody.brainstorm_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (brainstorm_id: ${submitBody.brainstorm_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope,task_description)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to retrieve the result envelope for a previously submitted council by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new council \u2014 use request_council first if none exists. Returns not-found when still processing.",inputSchema:{brainstorm_id:z18.string().describe("The brainstorm_id (UUID) returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({brainstorm_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/brainstorms/${brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z18.string().describe("The source branch name for the pull request"),base_branch:z18.string().describe("The target/destination branch name for the pull request"),title:z18.string().describe("The title of the pull request"),body:z18.string().optional().describe("The description/body of the pull request")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z18.string().describe("Git commit SHA to discover checks for"),force_rerun:z18.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text2=await handleResponse(resp);try{JSON.parse(text2).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text:text2}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z18.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text2=await handleResponse(resp);try{let parsed=JSON.parse(text2);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text:text2}]}});async function checkCiConfigAndDisablePoll(){try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&(pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: ci_check_config is null"))}else pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: could not read ci_check_config")}catch(err){pollCiChecksTool.disable(),console.error(`poll_ci_checks disabled: ${err}`)}}await checkCiConfigAndDisablePoll();registerTool("get_docs_dir",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, which requires shell access and may be blocked on some AI coding platforms.",inputSchema:{}},async()=>({content:[{type:"text",text:await getDocsDir()}]}));async function buildPipelineOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}async function buildChainOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,chainRecipes:CHAIN_RECIPES,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z18.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z18.record(z18.string(),z18.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z18.array(z18.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z18.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z18.union([z18.literal(1),z18.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",rounds:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea)),(rounds===1||rounds===2)&&(mergedVariables.rounds=String(rounds));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED});return{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os21.tmpdir(),entries;try{entries=await readdir6(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path47.join(tmpDir,entry);try{let info=await stat11(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm4(fullPath,{recursive:!0,force:!0})}catch{}}}registerTool("materialize_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',inputSchema:{base_branch:z18.string().optional().describe(`Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z18.string().optional().describe("Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z18.string().optional().describe('Pass "true" to skip fetch/materialization and fall back to the local project root as-is.')}},async({base_branch,base_sha,no_refresh_base})=>{let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp4(path47.join(os21.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path47.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm4(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink4(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm4(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}});registerTool("cleanup_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:"Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",inputSchema:{fresh_base_root:z18.string().describe("The fresh_base_root path returned by a prior materialize_fresh_base call.")}},async({fresh_base_root})=>{let allowedPrefix=path47.join(os21.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path47.resolve(fresh_base_root),resolvedTmpDir=path47.resolve(os21.tmpdir()),isDirectChildOfTmpDir=path47.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path47.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm4(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})});ACTIVE_GROUPS.has("pipeline-authoring")&&(registerTool("list_pipelines",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"List all available pipeline recipes with their names, descriptions, and required variables. No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",inputSchema:{}},async()=>{await ensureCustomPipelinesLoaded();let list=Object.entries(PIPELINES2).map(([key,pipeline])=>({name:key,description:pipeline.description??"",variables:(pipeline.variables??[]).filter(v=>v!=="docs_dir"&&v!=="idea_hash"),source:userPipelineKeys.has(key)?"user":"bundled"}));return{content:[{type:"text",text:JSON.stringify(list,null,2)}]}}),registerTool("run_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",inputSchema:{pipeline:z18.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z18.record(z18.string(),z18.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."),auto_approve:z18.union([z18.boolean(),z18.literal("true"),z18.literal("false")]).optional().describe("When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."),ttl_seconds:z18.number().int().positive().optional().describe("Override the default 24-hour idle TTL for this run. Must be a positive integer.")}},async input=>{let result=await runPipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("resume_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",inputSchema:{pipeline_run_id:z18.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),agent_result:z18.string().describe("The string the paused instruction's ## Return section asked you to produce")}},async input=>{let result=await resumePipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("list_pipeline_runs",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",inputSchema:{status:z18.enum(["running","paused","completed","failed","expired"]).optional().describe("Optional status filter")}},async input=>{let result=await listPipelineRuns(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("delete_pipeline_run",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",inputSchema:{pipeline_run_id:z18.string().describe("UUID of the pipeline run to delete.")}},async input=>{let result=await deletePipelineRun(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}));registerTool("run_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",inputSchema:{idea:z18.string().optional(),idea_file:z18.string().optional(),auto_approve:z18.union([z18.boolean(),z18.literal("true"),z18.literal("false")]).optional(),scheduled_at:z18.string().optional(),max_children:z18.number().int().positive().optional(),allow_duplicate:z18.boolean().optional(),agent:z18.enum(["claude"]).optional(),ttl_seconds:z18.number().int().positive().optional()}},async input=>{let{idea,idea_file,...rest}=input;if(idea!==void 0&&idea_file!==void 0)return{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:"Provide exactly one of `idea` or `idea_file`, not both."})}]};let resolved=await resolveTextOrFile(idea,idea_file,"idea");if(!resolved.ok)return resolved.errorResponse;let result=await runFullAutomation(await buildChainOrchestratorDeps(),{idea:resolved.text,...rest});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("resume_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",inputSchema:{chain_run_id:z18.string(),agent_result:z18.string()}},async input=>{let result=await resumeFullAutomation(await buildChainOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path47.posix.isAbsolute(value)||path47.win32.isAbsolute(value)||path47.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path47.resolve(await getDocsDir()),resolvedTarget=path47.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path47.sep)?{ok:!0,docsPath:path47.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}var DECISION_PAGE_CONTENT_CONTRACT="Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options).",DECISION_PAGE_CONTENT_EXAMPLE='{"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}';function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);if(input.content===void 0)return validationError2(`No \`content\` supplied. All decision data must be nested under \`content\` \u2014 root-level actionable_items / system_goals / clear_improvements / implementation_order are dropped by the lean input schema. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z18.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path47.resolve(path47.dirname(fileURLToPath4(import.meta.url)),"../"),assetsDir;try{await stat11(path47.join(projectRootForAssets,"design-assets")),assetsDir=path47.join(projectRootForAssets,"design-assets")}catch{assetsDir=path47.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile16(path47.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile16(path47.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir13(docsPath,{recursive:!0}),await writeFile13(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var updateStatusManager=createUpdateStatusManager({warn:message=>console.error(message),onLateStale:()=>{try{server.server.sendToolListChanged()}catch{}}});updateStatusManager.start();var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,createUpdateAdvisoryListHandler(gate.handleList,()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed())),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");if(!toolSurfaceGate)try{let protocolServer=server.server,capturedOriginal=null,handler=createUpdateAdvisoryListHandler((request,extra)=>capturedOriginal?capturedOriginal(request,extra):Promise.resolve({tools:[]}),()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed());capturedOriginal=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,handler)}catch{}var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} starting on stdio, waiting for an MCP client. To set up a project, run: npx -y @bridge_gpt/mcp-server install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
5736
+ `)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start an async council that fans out a task to multiple opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_council to retrieve. Generates and persists a retrievable artifact.",inputSchema:{task_description:z18.string().describe("Free-form description of the task for the council to weigh in on. Sent verbatim \u2014 this tool does NOT read task_description from a file."),repo_name:commonFields.repo_name,ticket_number:commonFields.ticket_number.optional(),providers:z18.array(z18.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."),concerns:z18.string().optional().describe("Optional caller-supplied concerns to surface to the council agents."),wait_for_result:commonFields.wait_for_result,save_locally:commonFields.save_locally,prior_brainstorm_id:z18.string().optional().describe("Optional brainstorm_id (the id field returned by an earlier council) to refine. When provided, the prior council's completed opinion-provider markdowns are concatenated and supplied as prior context."),mode:z18.enum(["technical","design","discovery","general"]).optional().describe("Preferred council-mode selector for new callers. 'technical' (default) is the implementation/architecture council; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped discovery questions for early/vague tasks. 'general' convenes the council from the supplied brief alone, with no indexed repository context required, unlike 'technical'/'discovery'. Takes precedence over the legacy boolean design field."),design:z18.boolean().optional().describe('Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'),lenses:z18.array(z18.string()).optional().describe("Optional reasoning lenses (e.g. 'simplicity', 'robustness', 'blast-radius') assigned one per provider, applies to technical/design modes only. Omitting this defaults to an automatic Simplicity + Extensibility pair."),debate:z18.boolean().optional().describe("Opt-in second cross-examination round (default off; ignored in discovery mode). Each provider critiques the others' round-1 output, appended under '## Cross-examination'. Costs an extra round and measured no better than the default \u2014 prefer omitting it.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_brainstorm_id,mode,design,lenses,debate})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,effectiveProviders=providers!==void 0?providers:["openai","gemini"],shouldWait=wait_for_result===!0,shouldSave=save_locally!==!1,submitPayload={repo_name:effectiveRepo,task_description,providers:effectiveProviders};ticket_number&&(submitPayload.ticket_number=ticket_number),concerns&&(submitPayload.concerns=concerns),prior_brainstorm_id&&(submitPayload.prior_brainstorm_request_id=prior_brainstorm_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let submitResp;try{submitResp=await fetch(buildUrl("/brainstorms"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure("the request_council tool")}]}}if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let submitBody=await submitResp.json();if(!shouldWait)return{content:[{type:"text",text:`Council submitted (brainstorm_id: ${submitBody.brainstorm_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`}]};let pollOutcome=await pollBrainstormUntilTerminal(submitBody.brainstorm_id,effectiveRepo);if(pollOutcome.kind==="giveup")return{content:[{type:"text",text:pollOutcome.text}]};if(!pollOutcome.envelope)return{content:[{type:"text",text:`Council could not confirm terminal status (brainstorm_id: ${submitBody.brainstorm_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope,task_description)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to retrieve the result envelope for a previously submitted council by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new council \u2014 use request_council first if none exists. Returns not-found when still processing.",inputSchema:{brainstorm_id:z18.string().describe("The brainstorm_id (UUID) returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({brainstorm_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/brainstorms/${brainstorm_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveBrainstormResultsLocally(envelope)),{content:[{type:"text",text:formatBrainstormToolResponse(envelope,savedPaths)}]}});registerTool("create_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",inputSchema:{head_branch:z18.string().describe("The source branch name for the pull request"),base_branch:z18.string().describe("The target/destination branch name for the pull request"),title:z18.string().describe("The title of the pull request"),body:z18.string().optional().describe("The description/body of the pull request")}},async({head_branch,base_branch,title,body})=>{let payload={repo_name:REPO_NAME,head_branch,base_branch,title};body!==void 0&&(payload.body=body);let resp=await fetch(buildUrl("/vcs/pull-requests"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var resolveCiChecksTool=registerTool("resolve_ci_checks",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z18.string().describe("Git commit SHA to discover checks for"),force_rerun:z18.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")}},async({commit_ref,force_rerun})=>{let payload={repo_name:REPO_NAME,commit_ref};force_rerun!==void 0&&(payload.force_rerun=force_rerun);let resp=await fetch(buildUrl("/resolve-ci-checks"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)}),text2=await handleResponse(resp);try{JSON.parse(text2).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text:text2}]}}),pollCiChecksTool=registerTool("poll_ci_checks",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",inputSchema:{commit_ref:z18.string().describe("Git commit SHA to poll CI checks for")}},async({commit_ref})=>{let url=buildGetUrl("/poll-ci-checks",{repo_name:REPO_NAME,commit_ref}),resp=await fetch(url,{headers:await getGetHeaders()}),text2=await handleResponse(resp);try{let parsed=JSON.parse(text2);parsed!==null&&typeof parsed=="object"&&!("error"in parsed)&&(Array.isArray(parsed.checks)||typeof parsed.all_complete=="boolean")&&observePrCiFromPollResponse(commit_ref,parsed,{resolveRunId:resolveDispatchRunIdForBinding}).catch(()=>{})}catch{}return{content:[{type:"text",text:text2}]}});async function checkCiConfigAndDisablePoll(){try{let url=buildGetUrl("/config-field/ci_check_config",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(resp.ok){let value=(await resp.json()).value;value==null&&(pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: ci_check_config is null"))}else pollCiChecksTool.disable(),console.error("poll_ci_checks disabled: could not read ci_check_config")}catch(err){pollCiChecksTool.disable(),console.error(`poll_ci_checks disabled: ${err}`)}}await checkCiConfigAndDisablePoll();registerTool("get_docs_dir",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, which requires shell access and may be blocked on some AI coding platforms.",inputSchema:{}},async()=>({content:[{type:"text",text:await getDocsDir()}]}));async function buildPipelineOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}async function buildChainOrchestratorDeps(){return await ensureCustomPipelinesLoaded(),{baseUrl:BASE_URL,apiKey:await getResolvedApiKey(),repoName:REPO_NAME,docsDir:await getDocsDir(),pipelines:PIPELINES2,chainRecipes:CHAIN_RECIPES,instructions:INSTRUCTIONS2,toolHandlers:TOOL_HANDLERS,includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED}}registerTool("get_pipeline_recipe",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z18.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z18.record(z18.string(),z18.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z18.array(z18.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z18.boolean().optional().describe("When true, auto-approve all approval-gated steps (skips the commit/push pause for implement-ticket; skips the HTML decision page for review-ticket, picking each item's recommended option). Pass via this top-level parameter."),rounds:z18.union([z18.literal(1),z18.literal(2)]).optional().describe("Round count (1|2); wins over adaptive routing. Omit for backend auto-routing.")}},async({pipeline:pipelineName,variables,skip_steps,auto_approve,rounds})=>{await ensureCustomPipelinesLoaded();let pipelineDef=PIPELINES2[pipelineName];if(!pipelineDef){let available=Object.keys(PIPELINES2).join(", ");return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[404]??"NOT_FOUND",status:404,message:`Pipeline "${pipelineName}" not found. Available pipelines: ${available||"(none)"}`})}]}}if(variables&&"auto_approve"in variables)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:"Pass auto_approve via the top-level parameter, not via the variables map."})}]};try{let mergedVariables={docs_dir:await getDocsDir(),provider:"",rounds:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea)),(rounds===1||rounds===2)&&(mergedVariables.rounds=String(rounds));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED});return{content:[{type:"text",text:JSON.stringify(recipe,null,2)}]}}catch(err){let message=err instanceof Error?err.message:String(err),isServerError=message.includes("not found in bundled instructions"),status=isServerError?500:400,code=isServerError?"PIPELINE_DATA_ERROR":ERROR_CODES[400]??"BAD_REQUEST";return{content:[{type:"text",text:JSON.stringify({error:code,status,message})}]}}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os21.tmpdir(),entries;try{entries=await readdir6(tmpDir)}catch{return}let now=Date.now();for(let entry of entries){if(!entry.startsWith(REVIEW_WORKSPACE_PREFIX))continue;let fullPath=path47.join(tmpDir,entry);try{let info=await stat11(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm4(fullPath,{recursive:!0,force:!0})}catch{}}}registerTool("materialize_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:'Materialize a pinned origin/<base_branch> tree via git archive into a unique temp dir, without mutating the working tree, index, stash, or branches. Returns { base_sha, fresh_base_root }. base_branch precedence: param > config > "main". no_refresh_base: "true" skips the fetch, returning the local project root with base_sha "local-stale".',inputSchema:{base_branch:z18.string().optional().describe(`Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z18.string().optional().describe("Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z18.string().optional().describe('Pass "true" to skip fetch/materialization and fall back to the local project root as-is.')}},async({base_branch,base_sha,no_refresh_base})=>{let projectRoot=await getProjectRoot();if(no_refresh_base==="true")return{content:[{type:"text",text:JSON.stringify({base_sha:"local-stale",fresh_base_root:projectRoot})}]};let startTicketsDeps={...createDefaultStartTicketsDeps(),cwd:projectRoot},resolvedBaseSha=(base_sha??"").trim(),effectiveBaseBranch=(base_branch??"").trim();if(effectiveBaseBranch.length===0)try{let access2={repoName:REPO_NAME,apiKey:await getResolvedApiKey(),baseUrl:BASE_URL},configValue=await fetchStartTicketsConfigField(access2,BASE_BRANCH_CONFIG_FIELD);typeof configValue=="string"&&configValue.trim().length>0&&(effectiveBaseBranch=configValue.trim())}catch{}effectiveBaseBranch.length===0&&(effectiveBaseBranch="main");let branchError=validateBranchName(effectiveBaseBranch);if(branchError)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_branch '${effectiveBaseBranch}': ${branchError}`})}]};if(resolvedBaseSha.length===0){let fetchResult=await fetchAndResolveBaseSha(startTicketsDeps,effectiveBaseBranch);if(!fetchResult.ok)return{content:[{type:"text",text:JSON.stringify({error:"FETCH_FAILED",status:502,message:fetchResult.error,base_branch:effectiveBaseBranch})}]};resolvedBaseSha=fetchResult.base_sha}else if(!/^[0-9a-f]{7,40}$/i.test(resolvedBaseSha))return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Invalid base_sha '${resolvedBaseSha}': must be a hex commit SHA.`})}]};let tempDir;try{tempDir=await mkdtemp4(path47.join(os21.tmpdir(),REVIEW_WORKSPACE_PREFIX))}catch(err){let message=err instanceof Error?err.message:String(err);return{content:[{type:"text",text:JSON.stringify({error:"TEMP_DIR_FAILED",status:500,message:`Failed to create a review workspace temp directory: ${message}`})}]}}let archivePath=path47.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm4(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"ARCHIVE_FAILED",status:500,message:`git archive of ${resolvedBaseSha} failed: ${archiveResult.stderr||archiveResult.stdout}`,base_sha:resolvedBaseSha})}]};let extractResult=await startTicketsDeps.runCommand("tar",["-xf",archivePath],{cwd:tempDir});return await unlink4(archivePath).catch(()=>{}),extractResult.exitCode!==0?(await rm4(tempDir,{recursive:!0,force:!0}).catch(()=>{}),{content:[{type:"text",text:JSON.stringify({error:"EXTRACT_FAILED",status:500,message:`tar extraction of the archived base tree failed: ${extractResult.stderr||extractResult.stdout}`,base_sha:resolvedBaseSha})}]}):{content:[{type:"text",text:JSON.stringify({base_sha:resolvedBaseSha,base_branch:effectiveBaseBranch,fresh_base_root:tempDir})}]}});registerTool("cleanup_fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:"Remove a review workspace temp directory previously returned by materialize_fresh_base. Strictly namespace-scoped: refuses to delete any path outside the OS temp dir's 'bridge-review-' prefix.",inputSchema:{fresh_base_root:z18.string().describe("The fresh_base_root path returned by a prior materialize_fresh_base call.")}},async({fresh_base_root})=>{let allowedPrefix=path47.join(os21.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path47.resolve(fresh_base_root),resolvedTmpDir=path47.resolve(os21.tmpdir()),isDirectChildOfTmpDir=path47.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path47.basename(resolvedTarget).startsWith(REVIEW_WORKSPACE_PREFIX);return!isDirectChildOfTmpDir||!hasReviewPrefix?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`Refusing to delete '${fresh_base_root}': it is outside the review workspace namespace ('${allowedPrefix}*').`})}]}:(await rm4(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})});ACTIVE_GROUPS.has("pipeline-authoring")&&(registerTool("list_pipelines",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"List all available pipeline recipes with their names, descriptions, and required variables. No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",inputSchema:{}},async()=>{await ensureCustomPipelinesLoaded();let list=Object.entries(PIPELINES2).map(([key,pipeline])=>({name:key,description:pipeline.description??"",variables:(pipeline.variables??[]).filter(v=>v!=="docs_dir"&&v!=="idea_hash"),source:userPipelineKeys.has(key)?"user":"bundled"}));return{content:[{type:"text",text:JSON.stringify(list,null,2)}]}}),registerTool("run_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",inputSchema:{pipeline:z18.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z18.record(z18.string(),z18.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."),auto_approve:z18.union([z18.boolean(),z18.literal("true"),z18.literal("false")]).optional().describe("When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."),ttl_seconds:z18.number().int().positive().optional().describe("Override the default 24-hour idle TTL for this run. Must be a positive integer.")}},async input=>{let result=await runPipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("resume_pipeline",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",inputSchema:{pipeline_run_id:z18.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),agent_result:z18.string().describe("The string the paused instruction's ## Return section asked you to produce")}},async input=>{let result=await resumePipeline(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("list_pipeline_runs",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",inputSchema:{status:z18.enum(["running","paused","completed","failed","expired"]).optional().describe("Optional status filter")}},async input=>{let result=await listPipelineRuns(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}),registerTool("delete_pipeline_run",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",inputSchema:{pipeline_run_id:z18.string().describe("UUID of the pipeline run to delete.")}},async input=>{let result=await deletePipelineRun(await buildPipelineOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}}));registerTool("run_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",inputSchema:{idea:z18.string().optional(),idea_file:z18.string().optional(),auto_approve:z18.union([z18.boolean(),z18.literal("true"),z18.literal("false")]).optional(),scheduled_at:z18.string().optional(),max_children:z18.number().int().positive().optional(),allow_duplicate:z18.boolean().optional(),agent:z18.enum(["claude"]).optional(),ttl_seconds:z18.number().int().positive().optional()}},async input=>{let{idea,idea_file,...rest}=input;if(idea!==void 0&&idea_file!==void 0)return{content:[{type:"text",text:JSON.stringify({error:"BAD_REQUEST",status:400,message:"Provide exactly one of `idea` or `idea_file`, not both."})}]};let resolved=await resolveTextOrFile(idea,idea_file,"idea");if(!resolved.ok)return resolved.errorResponse;let result=await runFullAutomation(await buildChainOrchestratorDeps(),{idea:resolved.text,...rest});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("resume_full_automation",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",inputSchema:{chain_run_id:z18.string(),agent_result:z18.string()}},async input=>{let result=await resumeFullAutomation(await buildChainOrchestratorDeps(),input);return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path47.posix.isAbsolute(value)||path47.win32.isAbsolute(value)||path47.isAbsolute(value)}function validateDecisionPageOutputSubdir(value){return value.trim().length===0?"Invalid output_subdir: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_subdir: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_subdir "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:isPlatformAbsolutePath(value)?`Invalid output_subdir "${value}": must be a relative path, not an absolute path.`:value.includes("\\")?`Invalid output_subdir "${value}": backslashes are not allowed; use "/" to separate nested directories.`:value.split(/[/\\]/).some(segment=>segment==="..")?`Invalid output_subdir "${value}": must not contain ".." path segments.`:null}function validateDecisionPageOutputFilename(value){return value.trim().length===0?"Invalid output_filename: must not be empty or whitespace-only.":value.includes("\0")?"Invalid output_filename: must not contain null bytes.":containsUnsafeEncodedPathToken(value)?`Invalid output_filename "${value}": must not contain encoded path tokens (%2e, %2f, %5c).`:value.includes("/")||value.includes("\\")?`Invalid output_filename "${value}": must not contain path separators.`:value==="."||value===".."?`Invalid output_filename "${value}": must be a real filename, not "." or "..".`:value.endsWith(".html")?null:`Invalid output_filename "${value}": must end with the ".html" suffix.`}async function resolveDecisionPageOutputTarget(outputSubdir,outputFilename){let subdirError=validateDecisionPageOutputSubdir(outputSubdir);if(subdirError)return{ok:!1,message:subdirError};let filenameError=validateDecisionPageOutputFilename(outputFilename);if(filenameError)return{ok:!1,message:filenameError};let docsBase=path47.resolve(await getDocsDir()),resolvedTarget=path47.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path47.sep)?{ok:!0,docsPath:path47.dirname(resolvedTarget),filePath:resolvedTarget}:{ok:!1,message:"Invalid output target: the resolved output path must stay under the docs directory."}}var DECISION_PAGE_CONTENT_CONTRACT="Expected shape: content.actionable_items[n] must have id, question, why_it_matters, recommendation_explanation, options (2-4 strings), option_consequences (same length as options), recommendation_index (0-based within options).",DECISION_PAGE_CONTENT_EXAMPLE='{"ticket_key":"BAPI-123","content":{"actionable_items":[{"id":"D-1","question":"Which approach?","why_it_matters":"Affects performance.","recommendation_explanation":"Option A is safer.","options":["A","B"],"option_consequences":["Safe path.","Risky path."],"recommendation_index":0}]}}';function formatDecisionPageValidationError(err){let first=err.issues[0],pathStr=first?.path?.length?first.path.join("."):"(root)",msg=first?.message??"Unknown validation error";return`Validation error at "${pathStr}": ${msg}. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`}registerTool("generate_decision_page",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Use to generate a local, review-shaped HTML decision page for capturing user decisions. Returns the local file path and a summary of the rendered items.",inputSchema:DecisionPageLeanInputShape},async input=>{let validationError2=message=>({content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",status:400,message})}]});if(!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key))return validationError2(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);if(input.content===void 0)return validationError2(`No \`content\` supplied. All decision data must be nested under \`content\` \u2014 root-level actionable_items / system_goals / clear_improvements / implementation_order are dropped by the lean input schema. ${DECISION_PAGE_CONTENT_CONTRACT} Example: ${DECISION_PAGE_CONTENT_EXAMPLE}`);let rawPayload={...input.content||{},ticket_key:input.ticket_key,artifact_type:input.artifact_type,output_subdir:input.output_subdir,output_filename:input.output_filename,labels:input.labels},parsed;try{parsed=DecisionPageInputSchema.parse(rawPayload)}catch(err){if(err instanceof z18.ZodError)return validationError2(formatDecisionPageValidationError(err));throw err}let hasPlanningContent=parsed.system_goals!==void 0||(parsed.implementation_order?.length??0)>0;if(parsed.actionable_items.length===0&&!hasPlanningContent)return{content:[{type:"text",text:JSON.stringify({status:"no_decisions_needed",ticket_key:parsed.ticket_key,clear_improvements_count:parsed.clear_improvements.length})}]};let seenIds=new Set;for(let item of parsed.actionable_items){if(seenIds.has(item.id))return validationError2(`Duplicate actionable_items id: "${item.id}"`);seenIds.add(item.id);let noneLabel=item.options.find(label=>label.toLowerCase()==="none of these");if(noneLabel)return validationError2(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`)}let seenCiIds=new Set;for(let ci of parsed.clear_improvements){if(seenCiIds.has(ci.id))return validationError2(`Duplicate clear_improvements id: "${ci.id}"`);seenCiIds.add(ci.id)}let seenNfrCategories=new Set;for(let nfr of parsed.system_goals?.nfrs??[]){if(seenNfrCategories.has(nfr.category))return validationError2(`Duplicate system_goals.nfrs category: "${nfr.category}"`);seenNfrCategories.add(nfr.category)}let seenAcIds=new Set;for(let ac of parsed.system_goals?.acceptance_criteria??[]){if(seenAcIds.has(ac.id))return validationError2(`Duplicate system_goals.acceptance_criteria id: "${ac.id}"`);seenAcIds.add(ac.id)}let outputSubdir=parsed.output_subdir??"review",outputFilename=parsed.output_filename??`${parsed.ticket_key}-decisions.html`,outputTarget=await resolveDecisionPageOutputTarget(outputSubdir,outputFilename);if(!outputTarget.ok)return validationError2(outputTarget.message);let projectRootForAssets=await getProjectRoot(),pkgRoot=path47.resolve(path47.dirname(fileURLToPath4(import.meta.url)),"../"),assetsDir;try{await stat11(path47.join(projectRootForAssets,"design-assets")),assetsDir=path47.join(projectRootForAssets,"design-assets")}catch{assetsDir=path47.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile16(path47.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile16(path47.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir13(docsPath,{recursive:!0}),await writeFile13(filePath,html,"utf-8"),{content:[{type:"text",text:JSON.stringify({status:"decision_page_generated",file_path:filePath,artifact_type:parsed.artifact_type,actionable_items_count:parsed.actionable_items.length,clear_improvements_count:parsed.clear_improvements.length,system_goals_nfr_count:parsed.system_goals?.nfrs?.length??0,system_goals_acceptance_criteria_count:parsed.system_goals?.acceptance_criteria?.length??0,implementation_order_count:parsed.implementation_order?.length??0})}]}});var updateStatusManager=createUpdateStatusManager({warn:message=>console.error(message),onLateStale:()=>{try{server.server.sendToolListChanged()}catch{}}});updateStatusManager.start();var toolSurfaceGate=null;if(TOOL_SURFACE_GATING_ENABLED&&toolSurfaceStartupProbe)try{let protocolServer=server.server,capturedOriginalListHandler=null,gate=createToolSurfaceGate({startupProbe:toolSurfaceStartupProbe,advertised:ADVERTISED,originalListHandler:(request,extra)=>capturedOriginalListHandler?capturedOriginalListHandler(request,extra):Promise.resolve({tools:[]}),freshProbe:()=>runToolSurfaceProbe(),notify:()=>server.server.sendToolListChanged(),logger:message=>console.error(message),lifecycleController:toolSurfaceLifecycle});capturedOriginalListHandler=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,createUpdateAdvisoryListHandler(gate.handleList,()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed())),toolSurfaceGate=gate;let existingOnClose=server.server.onclose?.bind(server.server);server.server.onclose=()=>{try{gate.close()}finally{existingOnClose?.()}}}catch{toolSurfaceGate=null,toolSurfaceLifecycle.abort(),console.error("tool-surface gating: reason=disabled subtype=sdk-incompatible hidden=0 revision=n/a hidden_tools=[]")}else TOOL_SURFACE_GATING_ENABLED||console.error("tool-surface gating: reason=kill-switch subtype=n/a hidden=0 revision=n/a hidden_tools=[]");if(!toolSurfaceGate)try{let protocolServer=server.server,capturedOriginal=null,handler=createUpdateAdvisoryListHandler((request,extra)=>capturedOriginal?capturedOriginal(request,extra):Promise.resolve({tools:[]}),()=>updateAdvisoryFor(updateStatusManager.getStatus()),()=>updateStatusManager.markListServed());capturedOriginal=installToolSurfaceListOverride(protocolServer,ListToolsRequestSchema,handler)}catch{}var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} starting on stdio, waiting for an MCP client. To set up a project, run: npx -y @bridge_gpt/mcp-server install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};